using System; using System.Collections.Specialized; using System.IO; using System.Text; using System.Threading.Tasks; using System.Web; using Microsoft.AspNetCore.Mvc; using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Web.Framework; using StardewModdingAPI.Web.Framework.LogParsing; using StardewModdingAPI.Web.Framework.LogParsing.Models; using StardewModdingAPI.Web.Framework.Storage; using StardewModdingAPI.Web.ViewModels; namespace StardewModdingAPI.Web.Controllers { /// Provides a web UI and API for parsing SMAPI log files. internal class LogParserController : Controller { /********* ** Fields *********/ /// Provides access to raw data storage. private readonly IStorageProvider Storage; /********* ** Public methods *********/ /*** ** Constructor ***/ /// Construct an instance. /// Provides access to raw data storage. public LogParserController(IStorageProvider storage) { this.Storage = storage; } /*** ** Web UI ***/ /// Render the log parser UI. /// The stored file ID. /// How to render the log view. /// Whether to reset the log expiry. [HttpGet] [Route("log")] [Route("log/{id}")] public async Task Index(string? id = null, LogViewFormat format = LogViewFormat.Default, bool renew = false) { // fresh page if (string.IsNullOrWhiteSpace(id)) return this.View("Index", this.GetModel(id)); // fetch log StoredFileInfo file = await this.Storage.GetAsync(id, renew); // render view switch (format) { case LogViewFormat.Default: case LogViewFormat.RawView: { ParsedLog log = file.Success ? new LogParser().Parse(file.Content) : new ParsedLog { IsValid = false, Error = file.Error }; return this.View("Index", this.GetModel(id, uploadWarning: file.Warning, expiry: file.Expiry).SetResult(log, showRaw: format == LogViewFormat.RawView)); } case LogViewFormat.RawDownload: { string content = file.Error ?? file.Content ?? string.Empty; return this.File(Encoding.UTF8.GetBytes(content), "plain/text", $"SMAPI log ({id}).txt"); } default: throw new InvalidOperationException($"Unknown log view format '{format}'."); } } /*** ** JSON ***/ /// Save raw log data. [HttpPost, AllowLargePosts] [Route("log")] public async Task PostAsync() { // get raw log text // note: avoid this.Request.Form, which fails if any mod logged a null character. string? input; { using StreamReader reader = new StreamReader(this.Request.Body); NameValueCollection parsed = HttpUtility.ParseQueryString(await reader.ReadToEndAsync()); input = parsed["input"]; if (string.IsNullOrWhiteSpace(input)) return this.View("Index", this.GetModel(null, uploadError: "The log file seems to be empty.")); } // upload log UploadResult uploadResult = await this.Storage.SaveAsync(input); if (!uploadResult.Succeeded) return this.View("Index", this.GetModel(null, uploadError: uploadResult.UploadError)); // redirect to view return this.Redirect(this.Url.PlainAction("Index", "LogParser", new { id = uploadResult.ID })!); } /********* ** Private methods *********/ /// Build a log parser model. /// The stored file ID. /// When the uploaded file will no longer be available. /// A non-blocking warning while uploading the log. /// An error which occurred while uploading the log. private LogParserModel GetModel(string? pasteID, DateTimeOffset? expiry = null, string? uploadWarning = null, string? uploadError = null) { Platform? platform = this.DetectClientPlatform(); return new LogParserModel(pasteID, platform) { UploadWarning = uploadWarning, UploadError = uploadError, Expiry = expiry }; } /// Detect the viewer's OS. /// Returns the viewer OS if known, else null. private Platform? DetectClientPlatform() { string userAgent = this.Request.Headers["User-Agent"]; switch (userAgent) { case string ua when ua.Contains("Windows"): return Platform.Windows; case string ua when ua.Contains("Android"): // check for Android before Linux because Android user agents also contain Linux return Platform.Android; case string ua when ua.Contains("Linux"): return Platform.Linux; case string ua when ua.Contains("Mac"): return Platform.Mac; default: return null; } } } }