From 6e82bb103afa71b6c624e6947094793b891a4d93 Mon Sep 17 00:00:00 2001 From: Mikal Stordal Date: Tue, 19 Dec 2023 14:24:42 +0100 Subject: [PATCH] refactor: split up preview update and update func. split up the preview update and update web ui theme functionality into two separeate endpoints, one for previewing an update and one to actually update. also modernised the internals of the web ui theme provider a bit. --- Shoko.Server/API/WebUI/WebUIThemeProvider.cs | 174 +++++++++--------- .../API/v3/Controllers/WebUIController.cs | 32 +++- 2 files changed, 112 insertions(+), 94 deletions(-) diff --git a/Shoko.Server/API/WebUI/WebUIThemeProvider.cs b/Shoko.Server/API/WebUI/WebUIThemeProvider.cs index 7f494c621..12b38ca91 100644 --- a/Shoko.Server/API/WebUI/WebUIThemeProvider.cs +++ b/Shoko.Server/API/WebUI/WebUIThemeProvider.cs @@ -82,57 +82,54 @@ public static bool RemoveTheme(ThemeDefinition theme) /// The updated theme metadata. public static async Task UpdateTheme(ThemeDefinition theme, bool preview = false) { + // Return the local theme if we don't have an update url. if (string.IsNullOrEmpty(theme.URL)) - { - throw new ValidationException("Update URL is empty."); - } + if (preview) + throw new ValidationException("No update URL in existing theme definition."); + else + return theme; if (!(Uri.TryCreate(theme.URL, UriKind.Absolute, out var updateUrl) && (updateUrl.Scheme == Uri.UriSchemeHttp || updateUrl.Scheme == Uri.UriSchemeHttps))) - { - throw new ValidationException("Invalid update URL."); - } + throw new ValidationException("Invalid update URL in existing theme definition."); - using (var httpClient = new HttpClient()) - { - httpClient.Timeout = TimeSpan.FromMinutes(1); - var response = await httpClient.GetAsync(updateUrl.AbsoluteUri); - - // Check if the response was a success. - if (response.StatusCode != HttpStatusCode.OK) - throw new HttpRequestException("Failed to retrieve theme file."); - - // Check if the response is using the correct content-type. - var contentType = response.Content.Headers.ContentType?.MediaType; - if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType)) - throw new HttpRequestException("Invalid content-type. Expected JSON."); - - // Simple sanity check before parsing the response content. - var content = await response.Content.ReadAsStringAsync(); - content = content?.Trim(); - if (string.IsNullOrWhiteSpace(content) || content[0] != '{' || content[content.Length - 1] != '}') - throw new HttpRequestException("Invalid theme file format."); - - // Try to parse the updated theme. - var updatedTheme = ThemeDefinition.FromJson(content, theme.ID, theme.FileName, preview); - if (updatedTheme == null) - throw new HttpRequestException("Failed to parse the updated theme."); - - // Save the updated theme file if we're not pre-viewing. - if (!preview) - { - var dirPath = Path.Combine(Utils.ApplicationPath, "themes"); - if (!Directory.Exists(dirPath)) - Directory.CreateDirectory(dirPath); + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromMinutes(1); + var response = await httpClient.GetAsync(updateUrl.AbsoluteUri); - var filePath = Path.Combine(dirPath, theme.FileName); - await File.WriteAllTextAsync(filePath, content); + // Check if the response was a success. + if (response.StatusCode != HttpStatusCode.OK) + throw new HttpRequestException("Failed to retrieve theme file."); - if (ThemeDict != null && !ThemeDict.TryAdd(theme.ID, updatedTheme)) - ThemeDict[theme.ID] = updatedTheme; - } + // Check if the response is using the correct content-type. + var contentType = response.Content.Headers.ContentType?.MediaType; + if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType)) + throw new HttpRequestException("Invalid content-type. Expected JSON."); - return updatedTheme; + // Simple sanity check before parsing the response content. + var content = await response.Content.ReadAsStringAsync(); + content = content?.Trim(); + if (string.IsNullOrWhiteSpace(content) || content[0] != '{' || content[^1] != '}') + throw new HttpRequestException("Invalid theme file format."); + + // Try to parse the updated theme. + var updatedTheme = ThemeDefinition.FromJson(content, theme.ID, theme.FileName, preview) ?? + throw new HttpRequestException("Failed to parse the updated theme."); + + // Save the updated theme file if we're not pre-viewing. + if (!preview) + { + var dirPath = Path.Combine(Utils.ApplicationPath, "themes"); + if (!Directory.Exists(dirPath)) + Directory.CreateDirectory(dirPath); + + var filePath = Path.Combine(dirPath, theme.FileName); + await File.WriteAllTextAsync(filePath, content); + + if (ThemeDict != null && !ThemeDict.TryAdd(theme.ID, updatedTheme)) + ThemeDict[theme.ID] = updatedTheme; } + + return updatedTheme; } /// @@ -165,48 +162,45 @@ public static async Task InstallTheme(string url, bool preview if (string.IsNullOrEmpty(fileName) || !FileNameRegex.IsMatch(fileName)) throw new ValidationException("Invalid theme file name."); - using (var httpClient = new HttpClient()) - { - httpClient.Timeout = TimeSpan.FromMinutes(1); - var response = await httpClient.GetAsync(updateUrl.AbsoluteUri); + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromMinutes(1); + var response = await httpClient.GetAsync(updateUrl.AbsoluteUri); - // Check if the response was a success. - if (response.StatusCode != HttpStatusCode.OK) - throw new HttpRequestException("Failed to retrieve theme file."); + // Check if the response was a success. + if (response.StatusCode != HttpStatusCode.OK) + throw new HttpRequestException("Failed to retrieve theme file."); - // Check if the response is using the correct content-type. - var contentType = response.Content.Headers.ContentType?.MediaType; - if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType)) - throw new HttpRequestException("Invalid content-type. Expected JSON."); + // Check if the response is using the correct content-type. + var contentType = response.Content.Headers.ContentType?.MediaType; + if (string.IsNullOrEmpty(contentType) || !AllowedMIMEs.Contains(contentType)) + throw new HttpRequestException("Invalid content-type. Expected JSON."); - // Simple sanity check before parsing the response content. - var content = await response.Content.ReadAsStringAsync(); - content = content?.Trim(); - if (string.IsNullOrWhiteSpace(content) || content[0] != '{' || content[content.Length - 1] != '}') - throw new HttpRequestException("Invalid theme file format."); + // Simple sanity check before parsing the response content. + var content = await response.Content.ReadAsStringAsync(); + content = content?.Trim(); + if (string.IsNullOrWhiteSpace(content) || content[0] != '{' || content[^1] != '}') + throw new HttpRequestException("Invalid theme file format."); - // Try to parse the new theme. - var id = FileNameToID(fileName); - var theme = ThemeDefinition.FromJson(content, id, fileName + extName, preview); - if (theme == null) - throw new HttpRequestException("Failed to parse the new theme."); - - // Save the new theme file if we're not pre-viewing. - if (!preview) - { - var dirPath = Path.Combine(Utils.ApplicationPath, "themes"); - if (!Directory.Exists(dirPath)) - Directory.CreateDirectory(dirPath); + // Try to parse the new theme. + var id = FileNameToID(fileName); + var theme = ThemeDefinition.FromJson(content, id, fileName + extName, preview) ?? + throw new HttpRequestException("Failed to parse the new theme."); - var filePath = Path.Combine(dirPath, fileName + extName); - await File.WriteAllTextAsync(filePath, content); + // Save the new theme file if we're not pre-viewing. + if (!preview) + { + var dirPath = Path.Combine(Utils.ApplicationPath, "themes"); + if (!Directory.Exists(dirPath)) + Directory.CreateDirectory(dirPath); - if (ThemeDict != null && !ThemeDict.TryAdd(id, theme)) - ThemeDict[id] = theme; - } + var filePath = Path.Combine(dirPath, fileName + extName); + await File.WriteAllTextAsync(filePath, content); - return theme; + if (ThemeDict != null && !ThemeDict.TryAdd(id, theme)) + ThemeDict[id] = theme; } + + return theme; } public class ThemeDefinitionInput @@ -344,9 +338,7 @@ public ThemeDefinition(ThemeDefinitionInput input, string id, string fileName, b } public string ToCSS() - { - return $".theme-{ID} {{{string.Join(" ", Values.Select(pair => $" --{pair.Key}: {pair.Value};"))} }}"; - } + => $".theme-{ID} {{{string.Join(" ", Values.Select(pair => $" --{pair.Key}: {pair.Value};"))} }}"; internal static IReadOnlyList FromDirectory(string dirPath) { @@ -379,7 +371,7 @@ internal static IReadOnlyList FromDirectory(string dirPath) return null; // Safely try to read - string? fileContents = null; + string? fileContents; try { fileContents = File.ReadAllText(filePath)?.Trim(); @@ -389,7 +381,7 @@ internal static IReadOnlyList FromDirectory(string dirPath) return null; } // Simple sanity check before parsing the file contents. - if (string.IsNullOrWhiteSpace(fileContents) || fileContents[0] != '{' || fileContents[fileContents.Length - 1] != '}') + if (string.IsNullOrWhiteSpace(fileContents) || fileContents[0] != '{' || fileContents[^1] != '}') return null; var id = FileNameToID(fileName); @@ -415,17 +407,17 @@ internal static IReadOnlyList FromDirectory(string dirPath) } private static string FileNameToID(string fileName) - { - return fileName.ToLowerInvariant().Replace('_', '-'); - } + => fileName.ToLowerInvariant().Replace('_', '-'); private static string NameFromID(string id) - { - return string.Join(' ', id.Replace('_', '-').Replace('-', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(segment => segment[0..1].ToUpperInvariant() + segment[1..].ToLowerInvariant())); - } + => string.Join( + ' ', + id.Replace('_', '-') + .Replace('-', ' ') + .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(segment => segment[0..1].ToUpperInvariant() + segment[1..].ToLowerInvariant()) + ); public static string ToCSS(this IEnumerable list) - { - return string.Join(" ", list.Select(theme => theme.ToCSS())); - } + => string.Join(" ", list.Select(theme => theme.ToCSS())); } diff --git a/Shoko.Server/API/v3/Controllers/WebUIController.cs b/Shoko.Server/API/v3/Controllers/WebUIController.cs index 7457e0141..5cb42ce2b 100644 --- a/Shoko.Server/API/v3/Controllers/WebUIController.cs +++ b/Shoko.Server/API/v3/Controllers/WebUIController.cs @@ -160,16 +160,42 @@ public ActionResult RemoveTheme([FromRoute] string themeID) return NoContent(); } + /// + /// Preview the update to a theme by its ID. + /// + /// The ID of the theme to update. + /// The preview of the updated theme. + [ResponseCache(Duration = 60 /* 1 minute in seconds */)] + [HttpGet("Theme/{themeID}/Update")] + public async Task> PreviewUpdatedTheme([FromRoute] string themeID) + { + var theme = WebUIThemeProvider.GetTheme(themeID, true); + if (theme == null) + return NotFound("A theme with the given id was not found."); + + try + { + theme = await WebUIThemeProvider.UpdateTheme(theme, true); + return new WebUITheme(theme); + } + catch (ValidationException valEx) + { + return BadRequest(valEx.Message); + } + catch (HttpRequestException httpEx) + { + return InternalError(httpEx.Message); + } + } /// /// Updates a theme by its ID. /// /// The ID of the theme to update. - /// Flag indicating whether to enable preview mode. /// The updated theme. [Authorize("admin")] [HttpPost("Theme/{themeID}/Update")] - public async Task> UpdateTheme([FromRoute] string themeID, [FromQuery] bool preview = false) + public async Task> UpdateTheme([FromRoute] string themeID) { var theme = WebUIThemeProvider.GetTheme(themeID, true); if (theme == null) @@ -177,7 +203,7 @@ public async Task> UpdateTheme([FromRoute] string theme try { - theme = await WebUIThemeProvider.UpdateTheme(theme, preview); + theme = await WebUIThemeProvider.UpdateTheme(theme, false); return new WebUITheme(theme); } catch (ValidationException valEx)