summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Controllers/LogParserController.cs
blob: a3bcf4c33f55f6b412632af1f1cb28a10653f31f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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
{
    /// <summary>Provides a web UI and API for parsing SMAPI log files.</summary>
    internal class LogParserController : Controller
    {
        /*********
        ** Fields
        *********/
        /// <summary>Provides access to raw data storage.</summary>
        private readonly IStorageProvider Storage;


        /*********
        ** Public methods
        *********/
        /***
        ** Constructor
        ***/
        /// <summary>Construct an instance.</summary>
        /// <param name="storage">Provides access to raw data storage.</param>
        public LogParserController(IStorageProvider storage)
        {
            this.Storage = storage;
        }

        /***
        ** Web UI
        ***/
        /// <summary>Render the log parser UI.</summary>
        /// <param name="id">The stored file ID.</param>
        /// <param name="format">How to render the log view.</param>
        /// <param name="renew">Whether to reset the log expiry.</param>
        [HttpGet]
        [Route("log")]
        [Route("log/{id}")]
        public async Task<ActionResult> 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
        ***/
        /// <summary>Save raw log data.</summary>
        [HttpPost, AllowLargePosts]
        [Route("log")]
        public async Task<ActionResult> 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
        *********/
        /// <summary>Build a log parser model.</summary>
        /// <param name="pasteID">The stored file ID.</param>
        /// <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, DateTimeOffset? expiry = null, string? uploadWarning = null, string? uploadError = null)
        {
            Platform? platform = this.DetectClientPlatform();

            return new LogParserModel(pasteID, platform)
            {
                UploadWarning = uploadWarning,
                UploadError = uploadError,
                Expiry = expiry
            };
        }

        /// <summary>Detect the viewer's OS.</summary>
        /// <returns>Returns the viewer OS if known, else null.</returns>
        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;
            }
        }
    }
}