Skip to content

Commit

Permalink
refactor: split up preview update and update func.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
revam committed Dec 19, 2023
1 parent 5c3958e commit 6e82bb1
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 94 deletions.
174 changes: 83 additions & 91 deletions Shoko.Server/API/WebUI/WebUIThemeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,57 +82,54 @@ public static bool RemoveTheme(ThemeDefinition theme)
/// <returns>The updated theme metadata.</returns>
public static async Task<ThemeDefinition> 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;
}

/// <summary>
Expand Down Expand Up @@ -165,48 +162,45 @@ public static async Task<ThemeDefinition> 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
Expand Down Expand Up @@ -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<ThemeDefinition> FromDirectory(string dirPath)
{
Expand Down Expand Up @@ -379,7 +371,7 @@ internal static IReadOnlyList<ThemeDefinition> FromDirectory(string dirPath)
return null;

// Safely try to read
string? fileContents = null;
string? fileContents;
try
{
fileContents = File.ReadAllText(filePath)?.Trim();
Expand All @@ -389,7 +381,7 @@ internal static IReadOnlyList<ThemeDefinition> 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);
Expand All @@ -415,17 +407,17 @@ internal static IReadOnlyList<ThemeDefinition> 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<ThemeDefinition> list)
{
return string.Join(" ", list.Select(theme => theme.ToCSS()));
}
=> string.Join(" ", list.Select(theme => theme.ToCSS()));
}
32 changes: 29 additions & 3 deletions Shoko.Server/API/v3/Controllers/WebUIController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,24 +160,50 @@ public ActionResult RemoveTheme([FromRoute] string themeID)

return NoContent();
}
/// <summary>
/// Preview the update to a theme by its ID.
/// </summary>
/// <param name="themeID">The ID of the theme to update.</param>
/// <returns>The preview of the updated theme.</returns>
[ResponseCache(Duration = 60 /* 1 minute in seconds */)]
[HttpGet("Theme/{themeID}/Update")]
public async Task<ActionResult<WebUITheme>> 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);
}
}

/// <summary>
/// Updates a theme by its ID.
/// </summary>
/// <param name="themeID">The ID of the theme to update.</param>
/// <param name="preview">Flag indicating whether to enable preview mode.</param>
/// <returns>The updated theme.</returns>
[Authorize("admin")]
[HttpPost("Theme/{themeID}/Update")]
public async Task<ActionResult<WebUITheme>> UpdateTheme([FromRoute] string themeID, [FromQuery] bool preview = false)
public async Task<ActionResult<WebUITheme>> UpdateTheme([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, preview);
theme = await WebUIThemeProvider.UpdateTheme(theme, false);
return new WebUITheme(theme);
}
catch (ValidationException valEx)
Expand Down

0 comments on commit 6e82bb1

Please sign in to comment.