Merge pull request #9061 from Bond-009/ct

pull/7672/merge
Bond-009 1 year ago committed by GitHub
commit df8346cd63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -91,6 +91,7 @@ public class ImageController : BaseJellyfinApiController
[Authorize] [Authorize]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@ -110,6 +111,11 @@ public class ImageController : BaseJellyfinApiController
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
} }
if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
@ -121,7 +127,7 @@ public class ImageController : BaseJellyfinApiController
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
} }
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path) .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@ -145,6 +151,7 @@ public class ImageController : BaseJellyfinApiController
[Authorize] [Authorize]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
@ -164,6 +171,11 @@ public class ImageController : BaseJellyfinApiController
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image."); return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
} }
if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
@ -175,7 +187,7 @@ public class ImageController : BaseJellyfinApiController
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false); await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
} }
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType ?? string.Empty))); user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path) .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
@ -342,6 +354,7 @@ public class ImageController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImage( public async Task<ActionResult> SetItemImage(
@ -354,6 +367,11 @@ public class ImageController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
@ -379,6 +397,7 @@ public class ImageController : BaseJellyfinApiController
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[AcceptsImageFile] [AcceptsImageFile]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
public async Task<ActionResult> SetItemImageByIndex( public async Task<ActionResult> SetItemImageByIndex(
@ -392,6 +411,11 @@ public class ImageController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
if (!TryGetImageExtensionFromContentType(Request.ContentType, out _))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
@ -1763,22 +1787,14 @@ public class ImageController : BaseJellyfinApiController
[AcceptsImageFile] [AcceptsImageFile]
public async Task<ActionResult> UploadCustomSplashscreen() public async Task<ActionResult> UploadCustomSplashscreen()
{ {
if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension))
{
return BadRequest("Incorrect ContentType.");
}
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
await using (memoryStream.ConfigureAwait(false)) await using (memoryStream.ConfigureAwait(false))
{ {
var mimeType = MediaTypeHeaderValue.Parse(Request.ContentType).MediaType;
if (!mimeType.HasValue)
{
return BadRequest("Error reading mimetype from uploaded image");
}
var extension = MimeTypes.ToExtension(mimeType.Value);
if (string.IsNullOrEmpty(extension))
{
return BadRequest("Error converting mimetype to an image extension");
}
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
brandingOptions.SplashscreenLocation = filePath; brandingOptions.SplashscreenLocation = filePath;
@ -2106,4 +2122,23 @@ public class ImageController : BaseJellyfinApiController
return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain); return PhysicalFile(imagePath, imageContentType ?? MediaTypeNames.Text.Plain);
} }
internal static bool TryGetImageExtensionFromContentType(string? contentType, [NotNullWhen(true)] out string? extension)
{
extension = null;
if (string.IsNullOrEmpty(contentType))
{
return false;
}
if (MediaTypeHeaderValue.TryParse(contentType, out var parsedValue)
&& parsedValue.MediaType.HasValue
&& MimeTypes.IsImage(parsedValue.MediaType.Value))
{
extension = MimeTypes.ToExtension(parsedValue.MediaType.Value);
return extension is not null;
}
return false;
}
} }

@ -117,7 +117,9 @@ namespace MediaBrowser.Model.Net
// Type image // Type image
{ "image/jpeg", ".jpg" }, { "image/jpeg", ".jpg" },
{ "image/tiff", ".tiff" },
{ "image/x-png", ".png" }, { "image/x-png", ".png" },
{ "image/x-icon", ".ico" },
// Type text // Type text
{ "text/plain", ".txt" }, { "text/plain", ".txt" },
@ -178,5 +180,8 @@ namespace MediaBrowser.Model.Net
var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault(); var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault();
return string.IsNullOrEmpty(extension) ? null : "." + extension; return string.IsNullOrEmpty(extension) ? null : "." + extension;
} }
public static bool IsImage(ReadOnlySpan<char> mimeType)
=> mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase);
} }
} }

@ -0,0 +1,36 @@
using System;
using Jellyfin.Api.Controllers;
using Xunit;
namespace Jellyfin.Api.Tests.Controllers;
public static class ImageControllerTests
{
[Theory]
[InlineData("image/apng", ".apng")]
[InlineData("image/avif", ".avif")]
[InlineData("image/bmp", ".bmp")]
[InlineData("image/gif", ".gif")]
[InlineData("image/x-icon", ".ico")]
[InlineData("image/jpeg", ".jpg")]
[InlineData("image/png", ".png")]
[InlineData("image/png; charset=utf-8", ".png")]
[InlineData("image/svg+xml", ".svg")]
[InlineData("image/tiff", ".tiff")]
[InlineData("image/webp", ".webp")]
public static void TryGetImageExtensionFromContentType_Valid_True(string contentType, string extension)
{
Assert.True(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex));
Assert.Equal(extension, ex);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("text/html")]
public static void TryGetImageExtensionFromContentType_InValid_False(string contentType)
{
Assert.False(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex));
Assert.Null(ex);
}
}

@ -127,9 +127,10 @@ namespace Jellyfin.Model.Tests.Net
[InlineData("image/jpeg", ".jpg")] [InlineData("image/jpeg", ".jpg")]
[InlineData("image/png", ".png")] [InlineData("image/png", ".png")]
[InlineData("image/svg+xml", ".svg")] [InlineData("image/svg+xml", ".svg")]
[InlineData("image/tiff", ".tif")] [InlineData("image/tiff", ".tiff")]
[InlineData("image/vnd.microsoft.icon", ".ico")] [InlineData("image/vnd.microsoft.icon", ".ico")]
[InlineData("image/webp", ".webp")] [InlineData("image/webp", ".webp")]
[InlineData("image/x-icon", ".ico")]
[InlineData("image/x-png", ".png")] [InlineData("image/x-png", ".png")]
[InlineData("text/css", ".css")] [InlineData("text/css", ".css")]
[InlineData("text/csv", ".csv")] [InlineData("text/csv", ".csv")]

Loading…
Cancel
Save