mirror of https://github.com/jellyfin/jellyfin.git
Verify ContentType of uploaded images
This commit is contained in:
parent
65f6c2e2fd
commit
60f41b80f6
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue