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)