diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2019-12-02 22:48:00 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2019-12-02 22:48:00 -0500 |
commit | d34f369d35290bca96cc7225d9765d1a8a66fa8b (patch) | |
tree | 141bc6b30760bf5745b7adb9ce60f0cf6c8fa720 /src/SMAPI.Web | |
parent | a3f21685049cabf2d824c8060dc0b1de47e9449e (diff) | |
parent | 1128451acf56cf479864047c0bb8bb18e232fa00 (diff) | |
download | SMAPI-d34f369d35290bca96cc7225d9765d1a8a66fa8b.tar.gz SMAPI-d34f369d35290bca96cc7225d9765d1a8a66fa8b.tar.bz2 SMAPI-d34f369d35290bca96cc7225d9765d1a8a66fa8b.zip |
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI.Web')
24 files changed, 307 insertions, 242 deletions
diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs index b2eb9a87..40599abc 100644 --- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs +++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs @@ -5,14 +5,12 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Schema; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Clients.Pastebin; using StardewModdingAPI.Web.Framework.Compression; -using StardewModdingAPI.Web.Framework.ConfigModels; using StardewModdingAPI.Web.ViewModels.JsonValidator; namespace StardewModdingAPI.Web.Controllers @@ -23,18 +21,12 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Fields *********/ - /// <summary>The site config settings.</summary> - private readonly SiteConfig Config; - /// <summary>The underlying Pastebin client.</summary> private readonly IPastebinClient Pastebin; /// <summary>The underlying text compression helper.</summary> private readonly IGzipHelper GzipHelper; - /// <summary>The section URL for the schema validator.</summary> - private string SectionUrl => this.Config.JsonValidatorUrl; - /// <summary>The supported JSON schemas (names indexed by ID).</summary> private readonly IDictionary<string, string> SchemaFormats = new Dictionary<string, string> { @@ -57,12 +49,10 @@ namespace StardewModdingAPI.Web.Controllers ** Constructor ***/ /// <summary>Construct an instance.</summary> - /// <param name="siteConfig">The context config settings.</param> /// <param name="pastebin">The Pastebin API client.</param> /// <param name="gzipHelper">The underlying text compression helper.</param> - public JsonValidatorController(IOptions<SiteConfig> siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper) + public JsonValidatorController(IPastebinClient pastebin, IGzipHelper gzipHelper) { - this.Config = siteConfig.Value; this.Pastebin = pastebin; this.GzipHelper = gzipHelper; } @@ -81,7 +71,7 @@ namespace StardewModdingAPI.Web.Controllers { schemaName = this.NormalizeSchemaName(schemaName); - var result = new JsonValidatorModel(this.SectionUrl, id, schemaName, this.SchemaFormats); + var result = new JsonValidatorModel(id, schemaName, this.SchemaFormats); if (string.IsNullOrWhiteSpace(id)) return this.View("Index", result); @@ -142,7 +132,7 @@ namespace StardewModdingAPI.Web.Controllers public async Task<ActionResult> PostAsync(JsonValidatorRequestModel request) { if (request == null) - return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, null, this.SchemaFormats).SetUploadError("The request seems to be invalid.")); + return this.View("Index", new JsonValidatorModel(null, null, this.SchemaFormats).SetUploadError("The request seems to be invalid.")); // normalize schema name string schemaName = this.NormalizeSchemaName(request.SchemaName); @@ -150,7 +140,7 @@ namespace StardewModdingAPI.Web.Controllers // get raw log text string input = request.Content; if (string.IsNullOrWhiteSpace(input)) - return this.View("Index", new JsonValidatorModel(this.SectionUrl, null, schemaName, this.SchemaFormats).SetUploadError("The JSON file seems to be empty.")); + return this.View("Index", new JsonValidatorModel(null, schemaName, this.SchemaFormats).SetUploadError("The JSON file seems to be empty.")); // upload log input = this.GzipHelper.CompressString(input); @@ -158,12 +148,10 @@ namespace StardewModdingAPI.Web.Controllers // handle errors if (!result.Success) - return this.View("Index", new JsonValidatorModel(this.SectionUrl, result.ID, schemaName, this.SchemaFormats).SetUploadError($"Pastebin error: {result.Error ?? "unknown error"}")); + return this.View("Index", new JsonValidatorModel(result.ID, schemaName, this.SchemaFormats).SetUploadError($"Pastebin error: {result.Error ?? "unknown error"}")); // redirect to view - UriBuilder uri = new UriBuilder(new Uri(this.SectionUrl)); - uri.Path = $"{uri.Path.TrimEnd('/')}/{schemaName}/{result.ID}"; - return this.Redirect(uri.Uri.ToString()); + return this.Redirect(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = schemaName, id = result.ID })); } diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs index f7f19cd8..318b34d0 100644 --- a/src/SMAPI.Web/Controllers/LogParserController.cs +++ b/src/SMAPI.Web/Controllers/LogParserController.cs @@ -1,6 +1,13 @@ using System; +using System.IO; using System.Linq; +using System.Text; using System.Threading.Tasks; +using Amazon; +using Amazon.Runtime; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.S3.Transfer; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit.Utilities; @@ -20,8 +27,8 @@ namespace StardewModdingAPI.Web.Controllers /********* ** Fields *********/ - /// <summary>The site config settings.</summary> - private readonly SiteConfig Config; + /// <summary>The API client settings.</summary> + private readonly ApiClientsConfig ClientsConfig; /// <summary>The underlying Pastebin client.</summary> private readonly IPastebinClient Pastebin; @@ -37,12 +44,12 @@ namespace StardewModdingAPI.Web.Controllers ** Constructor ***/ /// <summary>Construct an instance.</summary> - /// <param name="siteConfig">The context config settings.</param> + /// <param name="clientsConfig">The API client settings.</param> /// <param name="pastebin">The Pastebin API client.</param> /// <param name="gzipHelper">The underlying text compression helper.</param> - public LogParserController(IOptions<SiteConfig> siteConfig, IPastebinClient pastebin, IGzipHelper gzipHelper) + public LogParserController(IOptions<ApiClientsConfig> clientsConfig, IPastebinClient pastebin, IGzipHelper gzipHelper) { - this.Config = siteConfig.Value; + this.ClientsConfig = clientsConfig.Value; this.Pastebin = pastebin; this.GzipHelper = gzipHelper; } @@ -66,8 +73,9 @@ namespace StardewModdingAPI.Web.Controllers PasteInfo paste = await this.GetAsync(id); ParsedLog log = paste.Success ? new LogParser().Parse(paste.Content) - : new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error }; - return this.View("Index", this.GetModel(id).SetResult(log, raw)); + : new ParsedLog { IsValid = false, Error = paste.Error }; + + return this.View("Index", this.GetModel(id, uploadWarning: paste.Warning, expiry: paste.Expiry).SetResult(log, raw)); } /*** @@ -85,16 +93,12 @@ namespace StardewModdingAPI.Web.Controllers // upload log input = this.GzipHelper.CompressString(input); - SavePasteResult result = await this.Pastebin.PostAsync($"SMAPI log {DateTime.UtcNow:s}", input); - - // handle errors - if (!result.Success) - return this.View("Index", this.GetModel(result.ID, uploadError: $"Pastebin error: {result.Error ?? "unknown error"}")); + var uploadResult = await this.TrySaveLog(input); + if (!uploadResult.Succeeded) + return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError)); // redirect to view - UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl)); - uri.Path = uri.Path.TrimEnd('/') + '/' + result.ID; - return this.Redirect(uri.Uri.ToString()); + return this.Redirect(this.Url.PlainAction("Index", "LogParser", new { id = uploadResult.ID })); } @@ -105,19 +109,115 @@ namespace StardewModdingAPI.Web.Controllers /// <param name="id">The Pastebin paste ID.</param> private async Task<PasteInfo> GetAsync(string id) { - PasteInfo response = await this.Pastebin.GetAsync(id); - response.Content = this.GzipHelper.DecompressString(response.Content); - return response; + // get from Amazon S3 + if (Guid.TryParseExact(id, "N", out Guid _)) + { + var credentials = new BasicAWSCredentials(accessKey: this.ClientsConfig.AmazonAccessKey, secretKey: this.ClientsConfig.AmazonSecretKey); + + using (IAmazonS3 s3 = new AmazonS3Client(credentials, RegionEndpoint.GetBySystemName(this.ClientsConfig.AmazonRegion))) + { + try + { + using (GetObjectResponse response = await s3.GetObjectAsync(this.ClientsConfig.AmazonLogBucket, $"logs/{id}")) + using (Stream responseStream = response.ResponseStream) + using (StreamReader reader = new StreamReader(responseStream)) + { + DateTime expiry = response.Expiration.ExpiryDateUtc; + string pastebinError = response.Metadata["x-amz-meta-pastebin-error"]; + string content = this.GzipHelper.DecompressString(reader.ReadToEnd()); + + return new PasteInfo + { + Success = true, + Content = content, + Expiry = expiry, + Warning = pastebinError + }; + } + } + catch (AmazonServiceException ex) + { + return ex.ErrorCode == "NoSuchKey" + ? new PasteInfo { Error = "There's no log with that ID." } + : new PasteInfo { Error = $"Could not fetch that log from AWS S3 ({ex.ErrorCode}: {ex.Message})." }; + } + } + } + + // get from PasteBin + else + { + PasteInfo response = await this.Pastebin.GetAsync(id); + response.Content = this.GzipHelper.DecompressString(response.Content); + return response; + } + } + + /// <summary>Save a log to Pastebin or Amazon S3, if available.</summary> + /// <param name="content">The content to upload.</param> + /// <returns>Returns metadata about the save attempt.</returns> + private async Task<UploadResult> TrySaveLog(string content) + { + // save to PasteBin + string uploadError; + { + SavePasteResult result = await this.Pastebin.PostAsync($"SMAPI log {DateTime.UtcNow:s}", content); + if (result.Success) + return new UploadResult(true, result.ID, null); + + uploadError = $"Pastebin error: {result.Error ?? "unknown error"}"; + } + + // fallback to S3 + try + { + var credentials = new BasicAWSCredentials(accessKey: this.ClientsConfig.AmazonAccessKey, secretKey: this.ClientsConfig.AmazonSecretKey); + + using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(content))) + using (IAmazonS3 s3 = new AmazonS3Client(credentials, RegionEndpoint.GetBySystemName(this.ClientsConfig.AmazonRegion))) + using (TransferUtility uploader = new TransferUtility(s3)) + { + string id = Guid.NewGuid().ToString("N"); + + var uploadRequest = new TransferUtilityUploadRequest + { + BucketName = this.ClientsConfig.AmazonLogBucket, + Key = $"logs/{id}", + InputStream = stream, + Metadata = + { + // note: AWS will lowercase keys and prefix 'x-amz-meta-' + ["smapi-uploaded"] = DateTime.UtcNow.ToString("O"), + ["pastebin-error"] = uploadError + } + }; + + await uploader.UploadAsync(uploadRequest); + + return new UploadResult(true, id, uploadError); + } + } + catch (Exception ex) + { + return new UploadResult(false, null, $"{uploadError}\n{ex.Message}"); + } } /// <summary>Build a log parser model.</summary> /// <param name="pasteID">The paste ID.</param> - /// <param name="uploadError">An error which occurred while uploading the log to Pastebin.</param> - private LogParserModel GetModel(string pasteID, string uploadError = null) + /// <param name="expiry">When the uploaded file will no longer be available.</param> + /// <param name="uploadWarning">A non-blocking warning while uploading the log.</param> + /// <param name="uploadError">An error which occurred while uploading the log.</param> + private LogParserModel GetModel(string pasteID, DateTime? expiry = null, string uploadWarning = null, string uploadError = null) { - string sectionUrl = this.Config.LogParserUrl; Platform? platform = this.DetectClientPlatform(); - return new LogParserModel(sectionUrl, pasteID, platform) { UploadError = uploadError }; + + return new LogParserModel(pasteID, platform) + { + UploadWarning = uploadWarning, + UploadError = uploadError, + Expiry = expiry + }; } /// <summary>Detect the viewer's OS.</summary> @@ -143,5 +243,36 @@ namespace StardewModdingAPI.Web.Controllers return null; } } + + /// <summary>The result of an attempt to upload a file.</summary> + private class UploadResult + { + /********* + ** Accessors + *********/ + /// <summary>Whether the file upload succeeded.</summary> + public bool Succeeded { get; } + + /// <summary>The file ID, if applicable.</summary> + public string ID { get; } + + /// <summary>The upload error, if any.</summary> + public string UploadError { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="succeeded">Whether the file upload succeeded.</param> + /// <param name="id">The file ID, if applicable.</param> + /// <param name="uploadError">The upload error, if any.</param> + public UploadResult(bool succeeded, string id, string uploadError) + { + this.Succeeded = succeeded; + this.ID = id; + this.UploadError = uploadError; + } + } } } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index fe220eb5..3e3b81c8 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -11,6 +11,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.Caching.Mods; using StardewModdingAPI.Web.Framework.Caching.Wiki; using StardewModdingAPI.Web.Framework.Clients.Chucklefish; @@ -49,9 +50,6 @@ namespace StardewModdingAPI.Web.Controllers /// <summary>The internal mod metadata list.</summary> private readonly ModDatabase ModDatabase; - /// <summary>The web URL for the compatibility list.</summary> - private readonly string CompatibilityPageUrl; - /********* ** Public methods @@ -70,7 +68,6 @@ namespace StardewModdingAPI.Web.Controllers { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); ModUpdateCheckConfig config = configProvider.Value; - this.CompatibilityPageUrl = config.CompatibilityPageUrl; this.WikiCache = wikiCache; this.ModCache = modCache; @@ -205,7 +202,7 @@ namespace StardewModdingAPI.Web.Controllers // get unofficial version if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version)) - unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); + unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}"); // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) @@ -215,7 +212,7 @@ namespace StardewModdingAPI.Web.Controllers if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version)) - ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}") + ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods")}#{wikiEntry.Anchor}") : null; } else diff --git a/src/SMAPI.Web/Framework/BeanstalkEnvPropsConfigProvider.cs b/src/SMAPI.Web/Framework/BeanstalkEnvPropsConfigProvider.cs deleted file mode 100644 index fe27fe2f..00000000 --- a/src/SMAPI.Web/Framework/BeanstalkEnvPropsConfigProvider.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Web.Framework -{ - /// <summary>Reads configuration values from the AWS Beanstalk environment properties file (if present).</summary> - /// <remarks>This is a workaround for AWS Beanstalk injection not working with .NET Core apps.</remarks> - internal class BeanstalkEnvPropsConfigProvider : ConfigurationProvider, IConfigurationSource - { - /********* - ** Fields - *********/ - /// <summary>The absolute path to the container configuration file on an Amazon EC2 instance.</summary> - private const string ContainerConfigPath = @"C:\Program Files\Amazon\ElasticBeanstalk\config\containerconfiguration"; - - - /********* - ** Public methods - *********/ - /// <summary>Build the configuration provider for this source.</summary> - /// <param name="builder">The configuration builder.</param> - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return new BeanstalkEnvPropsConfigProvider(); - } - - /// <summary>Load the environment properties.</summary> - public override void Load() - { - this.Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - // get Beanstalk config file - FileInfo file = new FileInfo(BeanstalkEnvPropsConfigProvider.ContainerConfigPath); - if (!file.Exists) - return; - - // parse JSON - JObject jsonRoot = (JObject)JsonConvert.DeserializeObject(File.ReadAllText(file.FullName)); - if (jsonRoot["iis"]?["env"] is JArray jsonProps) - { - foreach (string prop in jsonProps.Values<string>()) - { - string[] parts = prop.Split('=', 2); // key=value - if (parts.Length == 2) - this.Data[parts[0]] = parts[1]; - } - } - } - } -} diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs index 955156eb..bb2de356 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PasteInfo.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Web.Framework.Clients.Pastebin { /// <summary>The response for a get-paste request.</summary> @@ -9,7 +11,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin /// <summary>The fetched paste content (if <see cref="Success"/> is <c>true</c>).</summary> public string Content { get; set; } - /// <summary>The error message (if saving failed).</summary> + /// <summary>When the file will no longer be available.</summary> + public DateTime? Expiry { get; set; } + + /// <summary>The error message if saving succeeded, but a non-blocking issue was encountered.</summary> + public string Warning { get; set; } + + /// <summary>The error message if saving failed.</summary> public string Error { get; set; } } } diff --git a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs index 2e8a8c68..d695aab6 100644 --- a/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Pastebin/PastebinClient.cs @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Pastebin } catch (Exception ex) { - return new PasteInfo { Error = ex.ToString() }; + return new PasteInfo { Error = $"Pastebin error: {ex}" }; } } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index 121690c5..7119ef03 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -14,6 +14,22 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /**** + ** Amazon Web Services + ****/ + /// <summary>The access key for AWS authentication.</summary> + public string AmazonAccessKey { get; set; } + + /// <summary>The secret key for AWS authentication.</summary> + public string AmazonSecretKey { get; set; } + + /// <summary>The AWS region endpoint (like 'us-east-1').</summary> + public string AmazonRegion { get; set; } + + /// <summary>The AWS bucket in which to store temporary uploaded logs.</summary> + public string AmazonLogBucket { get; set; } + + + /**** ** Chucklefish ****/ /// <summary>The base URL for the Chucklefish mod site.</summary> diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs index ab935bb3..46073eb8 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs @@ -11,8 +11,5 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// <summary>The number of minutes failed update checks should be cached before refetching them.</summary> public int ErrorCacheMinutes { get; set; } - - /// <summary>The web URL for the wiki compatibility list.</summary> - public string CompatibilityPageUrl { get; set; } } } diff --git a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs index bc6e868a..d379c423 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs @@ -6,18 +6,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /********* ** Accessors *********/ - /// <summary>The root URL for the app.</summary> - public string RootUrl { get; set; } - - /// <summary>The root URL for the log parser.</summary> - public string LogParserUrl { get; set; } - - /// <summary>The root URL for the JSON validator.</summary> - public string JsonValidatorUrl { get; set; } - - /// <summary>The root URL for the mod list.</summary> - public string ModListUrl { get; set; } - /// <summary>Whether to show SMAPI beta versions on the main page, if any.</summary> public bool BetaEnabled { get; set; } diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs new file mode 100644 index 00000000..d7707924 --- /dev/null +++ b/src/SMAPI.Web/Framework/Extensions.cs @@ -0,0 +1,28 @@ +using JetBrains.Annotations; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace StardewModdingAPI.Web.Framework +{ + /// <summary>Provides extensions on ASP.NET Core types.</summary> + public static class Extensions + { + /// <summary>Get a URL with the absolute path for an action method. Unlike <see cref="IUrlHelper.Action"/>, only the specified <paramref name="values"/> are added to the URL without merging values from the current HTTP request.</summary> + /// <param name="helper">The URL helper to extend.</param> + /// <param name="action">The name of the action method.</param> + /// <param name="controller">The name of the controller.</param> + /// <param name="values">An object that contains route values.</param> + /// <returns>The generated URL.</returns> + public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null) + { + RouteValueDictionary valuesDict = new RouteValueDictionary(values); + foreach (var value in helper.ActionContext.RouteData.Values) + { + if (!valuesDict.ContainsKey(value.Key)) + valuesDict[value.Key] = null; // explicitly remove it from the URL + } + + return helper.Action(action, controller, valuesDict); + } + } +} diff --git a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs deleted file mode 100644 index 920632ab..00000000 --- a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRewriteSubdomainRule.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Rewrite; - -namespace StardewModdingAPI.Web.Framework.RewriteRules -{ - /// <summary>Rewrite requests to prepend the subdomain portion (if any) to the path.</summary> - /// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" />.</remarks> - internal class ConditionalRewriteSubdomainRule : IRule - { - /********* - ** Accessors - *********/ - /// <summary>A predicate which indicates when the rule should be applied.</summary> - private readonly Func<HttpRequest, bool> ShouldRewrite; - - - /********* - ** Public methods - *********/ - /// <summary>Construct an instance.</summary> - /// <param name="shouldRewrite">A predicate which indicates when the rule should be applied.</param> - public ConditionalRewriteSubdomainRule(Func<HttpRequest, bool> shouldRewrite = null) - { - this.ShouldRewrite = shouldRewrite ?? (req => true); - } - - /// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary> - /// <param name="context">The rewrite context.</param> - public void ApplyRule(RewriteContext context) - { - HttpRequest request = context.HttpContext.Request; - - // check condition - if (!this.ShouldRewrite(request)) - return; - - // get host parts - string host = request.Host.Host; - string[] parts = host.Split('.'); - if (parts.Length < 2) - return; - - // prepend to path - request.Path = $"/{parts[0]}{request.Path}"; - } - } -} diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 8a7ca741..2c56fa75 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -12,16 +12,17 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="AWSSDK.S3" Version="3.3.108.4" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.7.7" /> <PackageReference Include="Hangfire.Mongo" Version="0.6.5" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.16" /> <PackageReference Include="Humanizer.Core" Version="2.7.9" /> + <PackageReference Include="JetBrains.Annotations" Version="2019.1.3" /> <PackageReference Include="Markdig" Version="0.18.0" /> <PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.1" /> <PackageReference Include="MongoDB.Driver" Version="2.9.3" /> <PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.11" /> <PackageReference Include="Pathoschild.FluentNexus" Version="0.8.0" /> diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 8110b696..53823771 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -48,7 +48,7 @@ namespace StardewModdingAPI.Web .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .Add(new BeanstalkEnvPropsConfigProvider()) + .AddEnvironmentVariables() .Build(); } @@ -58,6 +58,7 @@ namespace StardewModdingAPI.Web { // init basic services services + .Configure<ApiClientsConfig>(this.Configuration.GetSection("ApiClients")) .Configure<BackgroundServicesConfig>(this.Configuration.GetSection("BackgroundServices")) .Configure<ModCompatibilityListConfig>(this.Configuration.GetSection("ModCompatibilityList")) .Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck")) @@ -172,8 +173,7 @@ namespace StardewModdingAPI.Web .UseCors(policy => policy .AllowAnyHeader() .AllowAnyMethod() - .WithOrigins("https://smapi.io", "https://*.smapi.io", "https://*.edge.smapi.io") - .SetIsOriginAllowedToAllowWildcardSubdomains() + .WithOrigins("https://smapi.io") ) .UseRewriter(this.GetRedirectRules()) .UseStaticFiles() // wwwroot folder @@ -201,23 +201,13 @@ namespace StardewModdingAPI.Web shouldRewrite: req => req.Host.Host != "localhost" && !req.Path.StartsWithSegments("/api") - && !req.Host.Host.StartsWith("api.") - )); - - // convert subdomain.smapi.io => smapi.io/subdomain for routing - redirects.Add(new ConditionalRewriteSubdomainRule( - shouldRewrite: req => - req.Host.Host != "localhost" - && (req.Host.Host.StartsWith("api.") || req.Host.Host.StartsWith("json.") || req.Host.Host.StartsWith("log.") || req.Host.Host.StartsWith("mods.")) - && !req.Path.StartsWithSegments("/content") - && !req.Path.StartsWithSegments("/favicon.ico") )); // shortcut redirects redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0")); redirects.Add(new RedirectToUrlRule(@"^/(?:buildmsg|package)(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1")); // buildmsg deprecated, remove when SDV 1.4 is released redirects.Add(new RedirectToUrlRule(@"^/community\.?$", "https://stardewvalleywiki.com/Modding:Community")); - redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://mods.smapi.io")); + redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://smapi.io/mods")); redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index")); redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI")); redirects.Add(new RedirectToUrlRule(@"^/troubleshoot(.*)$", "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1")); diff --git a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs index 2d13bf23..5b18331f 100644 --- a/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs +++ b/src/SMAPI.Web/ViewModels/JsonValidator/JsonValidatorModel.cs @@ -9,9 +9,6 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator /********* ** Accessors *********/ - /// <summary>The root URL for the log parser controller.</summary> - public string SectionUrl { get; set; } - /// <summary>The paste ID.</summary> public string PasteID { get; set; } @@ -44,13 +41,11 @@ namespace StardewModdingAPI.Web.ViewModels.JsonValidator public JsonValidatorModel() { } /// <summary>Construct an instance.</summary> - /// <param name="sectionUrl">The root URL for the log parser controller.</param> /// <param name="pasteID">The paste ID.</param> /// <param name="schemaName">The schema name with which the JSON was validated.</param> /// <param name="schemaFormats">The supported JSON schemas (names indexed by ID).</param> - public JsonValidatorModel(string sectionUrl, string pasteID, string schemaName, IDictionary<string, string> schemaFormats) + public JsonValidatorModel(string pasteID, string schemaName, IDictionary<string, string> schemaFormats) { - this.SectionUrl = sectionUrl; this.PasteID = pasteID; this.SchemaName = schemaName; this.SchemaFormats = schemaFormats; diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs index f4c5214b..bea79eae 100644 --- a/src/SMAPI.Web/ViewModels/LogParserModel.cs +++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -19,9 +20,6 @@ namespace StardewModdingAPI.Web.ViewModels /********* ** Accessors *********/ - /// <summary>The root URL for the log parser controller.</summary> - public string SectionUrl { get; set; } - /// <summary>The paste ID.</summary> public string PasteID { get; set; } @@ -34,12 +32,18 @@ namespace StardewModdingAPI.Web.ViewModels /// <summary>Whether to show the raw unparsed log.</summary> public bool ShowRaw { get; set; } - /// <summary>An error which occurred while uploading the log to Pastebin.</summary> + /// <summary>A non-blocking warning while uploading the log.</summary> + public string UploadWarning { get; set; } + + /// <summary>An error which occurred while uploading the log.</summary> public string UploadError { get; set; } /// <summary>An error which occurred while parsing the log file.</summary> public string ParseError => this.ParsedLog?.Error; + /// <summary>When the uploaded file will no longer be available.</summary> + public DateTime? Expiry { get; set; } + /********* ** Public methods @@ -48,12 +52,10 @@ namespace StardewModdingAPI.Web.ViewModels public LogParserModel() { } /// <summary>Construct an instance.</summary> - /// <param name="sectionUrl">The root URL for the log parser controller.</param> /// <param name="pasteID">The paste ID.</param> /// <param name="platform">The viewer's detected OS, if known.</param> - public LogParserModel(string sectionUrl, string pasteID, Platform? platform) + public LogParserModel(string pasteID, Platform? platform) { - this.SectionUrl = sectionUrl; this.PasteID = pasteID; this.DetectedPlatform = platform; this.ParsedLog = null; diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index f42dde3b..ec9cfafe 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -1,4 +1,5 @@ @using Microsoft.Extensions.Options +@using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.ConfigModels @inject IOptions<SiteConfig> SiteConfig @model StardewModdingAPI.Web.ViewModels.IndexModel @@ -44,14 +45,14 @@ } <div><a href="https://stardewvalleywiki.com/Modding:Player_Guide" class="secondary-cta">Player guide</a></div> <div class="sublinks"> - <a href="https://github.com/Pathoschild/SMAPI">source code</a> | <a href="@(new UriBuilder(SiteConfig.Value.RootUrl) { Path = "privacy" }.Uri)">privacy</a> + <a href="https://github.com/Pathoschild/SMAPI">source code</a> | <a href="@Url.PlainAction("Privacy", "Index")">privacy</a> </div> <img id="pufferchick" src="Content/images/pufferchick.png" /> </div> <h2 id="help">Get help</h2> <ul> - <li><a href="@SiteConfig.Value.ModListUrl">Mod compatibility list</a></li> + <li><a href="@Url.PlainAction("Index", "Mods")">Mod compatibility list</a></li> <li>Get help <a href="https://smapi.io/community">on Discord or in the forums</a></li> </ul> @@ -61,7 +62,7 @@ <div class="github-description"> @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) </div> - <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@SiteConfig.Value.ModListUrl">mod compatibility list</a> for more info.</p> + <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p> } else { @@ -70,13 +71,13 @@ else <div class="github-description"> @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) </div> - <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@SiteConfig.Value.ModListUrl">mod compatibility list</a> for more info.</p> + <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p> <h3>SMAPI @Model.BetaVersion.Version?</h3> <div class="github-description"> @Html.Raw(Markdig.Markdown.ToHtml(Model.BetaVersion.Description)) </div> - <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@SiteConfig.Value.ModListUrl">mod compatibility list</a> for more info.</p> + <p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="@Url.PlainAction("Index", "Mods")">mod compatibility list</a> for more info.</p> } <h2 id="donate">Support SMAPI ♥</h2> @@ -105,10 +106,12 @@ else <p> Special thanks to + <a href="https://www.nexusmods.com/users/65566526?tab=user+files">bwdy</a>, hawkfalcon, <a href="https://twitter.com/iKeychain">iKeychain</a>, jwdred, <a href="https://www.nexusmods.com/users/12252523">Karmylla</a>, + <a href="https://www.nexusmods.com/stardewvalley/users/51777556">minervamaga</a>, Pucklynn, Renorien, Robby LaFarge, diff --git a/src/SMAPI.Web/Views/Index/Privacy.cshtml b/src/SMAPI.Web/Views/Index/Privacy.cshtml index 914384a8..7327de3d 100644 --- a/src/SMAPI.Web/Views/Index/Privacy.cshtml +++ b/src/SMAPI.Web/Views/Index/Privacy.cshtml @@ -1,4 +1,5 @@ @using Microsoft.Extensions.Options +@using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.ConfigModels @inject IOptions<SiteConfig> SiteConfig @{ @@ -8,7 +9,7 @@ <link rel="stylesheet" href="~/Content/css/privacy.css" /> } -← <a href="@SiteConfig.Value.RootUrl">back to SMAPI page</a> +← <a href="@Url.PlainAction("Index", "Index")">back to SMAPI page</a> <p>SMAPI is an <a href="https://github.com/Pathoschild/SMAPI">open-source</a> and non-profit project. Your privacy is important, so this page explains what information SMAPI uses and transmits. <strong>This page is informational only, it's not a legal document.</strong></p> @@ -34,7 +35,7 @@ </ol> <h3>Log parser</h3> -<p>The <a href="https://log.smapi.io/">log parser page</a> lets you store a log file for analysis and sharing. The log data is stored indefinitely in an obfuscated form as unlisted pastes in <a href="https://pastebin.com/">Pastebin</a>. No personal information is stored by the log parser beyond what you choose to upload, but see <em><a href="#web-logging">web logging</a></em> and the <a href="https://pastebin.com/doc_privacy_statement">Pastebin Privacy Statement</a>.</p> +<p>The <a href="https://smapi.io/log">log parser page</a> lets you store a log file for analysis and sharing. The log data is stored indefinitely in an obfuscated form as unlisted pastes in <a href="https://pastebin.com/">Pastebin</a>. No personal information is stored by the log parser beyond what you choose to upload, but see <em><a href="#web-logging">web logging</a></em> and the <a href="https://pastebin.com/doc_privacy_statement">Pastebin Privacy Statement</a>.</p> <h3>Multiplayer sync</h3> <p>As part of its multiplayer API, SMAPI transmits basic context to players you connect to (mainly your OS, SMAPI version, game version, and installed mods). This is used to enable multiplayer features like inter-mod messages, compatibility checks, etc. Although this information is normally hidden from players, it may be visible due to mods or configuration changes.</p> diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml index 3143fad9..de6b06a2 100644 --- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml +++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml @@ -1,10 +1,11 @@ +@using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.ViewModels.JsonValidator @model JsonValidatorModel @{ // get view data - string curPageUrl = new Uri(new Uri(Model.SectionUrl), $"{Model.SchemaName}/{Model.PasteID}").ToString(); - string newUploadUrl = Model.SchemaName != null ? new Uri(new Uri(Model.SectionUrl), Model.SchemaName).ToString() : Model.SectionUrl; + string curPageUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName, id = Model.PasteID }); + string newUploadUrl = this.Url.PlainAction("Index", "JsonValidator", new { schemaName = Model.SchemaName }); string schemaDisplayName = null; bool isValidSchema = Model.SchemaName != null && Model.SchemaFormats.TryGetValue(Model.SchemaName, out schemaDisplayName) && schemaDisplayName != "None"; @@ -34,8 +35,8 @@ <script src="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/lang/sunlight.javascript.min.js" crossorigin="anonymous"></script> <script src="~/Content/js/json-validator.js"></script> <script> - $(function () { - smapi.jsonValidator(@Json.Serialize(Model.SectionUrl), @Json.Serialize(Model.PasteID)); + $(function() { + smapi.jsonValidator(@Json.Serialize(this.Url.PlainAction("Index", "JsonValidator", values: null)), @Json.Serialize(Model.PasteID)); }); </script> } @@ -70,7 +71,7 @@ else if (Model.PasteID != null) @if (Model.Content == null) { <h2>Upload a JSON file</h2> - <form action="@Model.SectionUrl" method="post"> + <form action="@this.Url.PlainAction("PostAsync", "JsonValidator")" method="post"> <ol> <li> Choose the JSON format:<br /> diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index f98ffdf9..439167bc 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -1,5 +1,7 @@ +@using Humanizer @using Newtonsoft.Json @using StardewModdingAPI.Toolkit.Utilities +@using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.LogParsing.Models @model StardewModdingAPI.Web.ViewModels.LogParserModel @@ -18,7 +20,7 @@ { <meta name="robots" content="noindex" /> } - <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20190515" /> + <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20191127" /> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script> <script src="~/Content/js/log-parser.js?r=20190515"></script> @@ -31,7 +33,7 @@ showSections: @Json.Serialize(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false), noFormatting), showLevels: @Json.Serialize(defaultFilters, noFormatting), enableFilters: @Json.Serialize(!Model.ShowRaw) - }, '@Model.SectionUrl'); + }, '@this.Url.PlainAction("Index", "LogParser", values: null)'); }); </script> } @@ -48,8 +50,8 @@ else if (Model.ParseError != null) { <div class="banner error" v-pre> <strong>Oops, couldn't parse that log. (Make sure you upload the log file, not the console text.)</strong><br /> - Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br /> - (Or <a href="@Model.SectionUrl">upload a new log</a>.)<br /> + Share this URL when asking for help: <code>https://@this.Context.Request.Host.ToUriComponent()@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }))</code><br /> + (Or <a href="@this.Url.PlainAction("Index", "LogParser", values: null)">upload a new log</a>.)<br /> <br /> <small v-pre>Error details: @Model.ParseError</small> </div> @@ -57,8 +59,20 @@ else if (Model.ParseError != null) else if (Model.ParsedLog?.IsValid == true) { <div class="banner success" v-pre> - <strong>Share this link to let someone else see the log:</strong> <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br /> - (Or <a href="@Model.SectionUrl">upload a new log</a>.) + <strong>Share this link to let someone else see the log:</strong> <code>https://@this.Context.Request.Host.ToUriComponent()@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID })</code><br /> + (Or <a href="@this.Url.PlainAction("Index", "LogParser", values: null)">upload a new log</a>.) + </div> +} + +@* save warnings *@ +@if (Model.UploadWarning != null || Model.Expiry != null) +{ + <div class="save-metadata" v-pre> + @if (Model.Expiry != null) + { + <text>This log will expire in @((DateTime.UtcNow - Model.Expiry.Value).Humanize()). </text> + } + <!--@Model.UploadWarning--> </div> } @@ -71,7 +85,7 @@ else if (Model.ParsedLog?.IsValid == true) @foreach (Platform platform in new[] { Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows }) { <li> - <input type="radio" name="os" value="@platform" id="os-@platform" checked="@(Model.DetectedPlatform == platform)"/> + <input type="radio" name="os" value="@platform" id="os-@platform" checked="@(Model.DetectedPlatform == platform)" /> <label for="os-@platform">@platform</label> </li> } @@ -114,7 +128,7 @@ else if (Model.ParsedLog?.IsValid == true) </div> <h2>How do I share my log?</h2> - <form action="@Model.SectionUrl" method="post"> + <form action="@this.Url.PlainAction("PostAsync", "LogParser")" method="post"> <ol> <li> Drag the file onto this textbox (or paste the text in):<br /> @@ -151,7 +165,7 @@ else if (Model.ParsedLog?.IsValid == true) <div class="content-packs"> @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate)) { - <text>+ @contentPack.Name</text><br/> + <text>+ @contentPack.Name</text><br /> } </div> } @@ -173,7 +187,7 @@ else if (Model.ParsedLog?.IsValid == true) <div> @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate)) { - <a href="@contentPack.UpdateLink" target="_blank">@contentPack.Version → @contentPack.UpdateVersion</a><br/> + <a href="@contentPack.UpdateLink" target="_blank">@contentPack.Version → @contentPack.UpdateVersion</a><br /> } </div> } @@ -309,12 +323,12 @@ else if (Model.ParsedLog?.IsValid == true) } </table> - <small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))?raw=true">view raw log</a></small> + <small><a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, raw = true })">view raw log</a></small> } else { <pre v-pre>@Model.ParsedLog.RawText</pre> - <small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))">view parsed log</a></small> + <small><a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID })">view parsed log</a></small> } </div> } diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 87a22f06..17f1f673 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -1,4 +1,5 @@ @using Microsoft.Extensions.Options +@using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.ConfigModels @inject IOptions<SiteConfig> SiteConfig @@ -15,15 +16,15 @@ <div id="sidebar"> <h4>SMAPI</h4> <ul> - <li><a href="@SiteConfig.Value.RootUrl">About SMAPI</a></li> + <li><a href="@Url.PlainAction("Index", "Index")">About SMAPI</a></li> <li><a href="https://stardewvalleywiki.com/Modding:Index">Modding docs</a></li> </ul> <h4>Tools</h4> <ul> - <li><a href="@SiteConfig.Value.ModListUrl">Mod compatibility</a></li> - <li><a href="@SiteConfig.Value.LogParserUrl">Log parser</a></li> - <li><a href="@SiteConfig.Value.JsonValidatorUrl">JSON validator</a></li> + <li><a href="@Url.PlainAction("Index", "Mods")">Mod compatibility</a></li> + <li><a href="@Url.PlainAction("Index", "LogParser", values: null)">Log parser</a></li> + <li><a href="@Url.PlainAction("Index", "JsonValidator", values: null)">JSON validator</a></li> </ul> </div> <div id="content-column"> diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index baf7efb7..6b32f4ab 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -9,15 +9,14 @@ */ { "Site": { - "RootUrl": "http://localhost:59482/", - "ModListUrl": "http://localhost:59482/mods/", - "LogParserUrl": "http://localhost:59482/log/", - "JsonValidatorUrl": "http://localhost:59482/json/", "BetaEnabled": false, "BetaBlurb": null }, "ApiClients": { + "AmazonAccessKey": null, + "AmazonSecretKey": null, + "GitHubUsername": null, "GitHubPassword": null, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 674bb672..f81587ef 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -16,17 +16,18 @@ }, "Site": { - "RootUrl": null, // see top note - "ModListUrl": null, // see top note - "LogParserUrl": null, // see top note - "JsonValidatorUrl": null, // see top note - "BetaEnabled": null, // see top note - "BetaBlurb": null // see top note + "BetaEnabled": null, + "BetaBlurb": null }, "ApiClients": { "UserAgent": "SMAPI/{0} (+https://smapi.io)", + "AmazonAccessKey": null, + "AmazonSecretKey": null, + "AmazonRegion": "us-east-1", + "AmazonLogBucket": "smapi-log-parser", + "ChucklefishBaseUrl": "https://community.playstarbound.com", "ChucklefishModPageUrlFormat": "resources/{0}", @@ -34,27 +35,27 @@ "GitHubBaseUrl": "https://api.github.com", "GitHubAcceptHeader": "application/vnd.github.v3+json", - "GitHubUsername": null, // see top note - "GitHubPassword": null, // see top note + "GitHubUsername": null, + "GitHubPassword": null, "ModDropApiUrl": "https://www.moddrop.com/api/mods/data", "ModDropModPageUrl": "https://www.moddrop.com/sdv/mod/{0}", - "NexusApiKey": null, // see top note + "NexusApiKey": null, "NexusBaseUrl": "https://www.nexusmods.com/stardewvalley/", "NexusModUrlFormat": "mods/{0}", "NexusModScrapeUrlFormat": "mods/{0}?tab=files", "PastebinBaseUrl": "https://pastebin.com/", - "PastebinUserKey": null, // see top note - "PastebinDevKey": null // see top note + "PastebinUserKey": null, + "PastebinDevKey": null }, "MongoDB": { - "Host": null, // see top note - "Username": null, // see top note - "Password": null, // see top note - "Database": null // see top note + "Host": null, + "Username": null, + "Password": null, + "Database": null }, "ModCompatibilityList": { @@ -67,7 +68,6 @@ "ModUpdateCheck": { "SuccessCacheMinutes": 60, - "ErrorCacheMinutes": 5, - "CompatibilityPageUrl": "https://mods.smapi.io" + "ErrorCacheMinutes": 5 } } diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css index d5013207..4d4ab326 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css +++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css @@ -47,6 +47,12 @@ table caption { background: #FCC; } +.save-metadata { + margin-top: 1em; + font-size: 0.8em; + opacity: 0.3; +} + /********* ** Log metadata & filters *********/ diff --git a/src/SMAPI.Web/wwwroot/Content/js/json-validator.js b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js index 5499cef6..76b5f6d4 100644 --- a/src/SMAPI.Web/wwwroot/Content/js/json-validator.js +++ b/src/SMAPI.Web/wwwroot/Content/js/json-validator.js @@ -142,6 +142,7 @@ smapi.jsonValidator = function (sectionUrl, pasteID) { }); // upload form + var submit = $("#submit"); var input = $("#input"); if (input.length) { // disable submit if it's empty |