From 59920b4052d60b27b9434058df308c3f30f541c4 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 15 Feb 2023 18:05:49 +0100 Subject: Make exact match primary video --- .../Jellyfin.Naming.Tests/Video/MultiVersionTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) (limited to 'tests') diff --git a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs index 287d881a8..02e6f6368 100644 --- a/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/MultiVersionTests.cs @@ -323,6 +323,25 @@ namespace Jellyfin.Naming.Tests.Video Assert.Single(result[0].AlternateVersions); } + [Fact] + public void TestMultiVersion12() + { + var files = new[] + { + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", + @"/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv" + }; + + var result = VideoListResolver.Resolve( + files.Select(i => VideoResolver.Resolve(i, false, _namingOptions)).OfType().ToList(), + _namingOptions).ToList(); + + Assert.Single(result); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016).mkv", result[0].Files[0].Path); + Assert.Single(result[0].AlternateVersions); + Assert.Equal("/movies/X-Men Apocalypse (2016)/X-Men Apocalypse (2016) - 1080p.mkv", result[0].AlternateVersions[0].Path); + } + [Fact] public void Resolve_GivenFolderNameWithBracketsAndHyphens_GroupsBasedOnFolderName() { -- cgit v1.2.3 From 60f41b80f66cc866c018c58e4567726cb6dac90d Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 10 Jan 2023 17:02:23 +0100 Subject: Verify ContentType of uploaded images --- Jellyfin.Api/Controllers/ImageController.cs | 65 +++++++++++++++++----- MediaBrowser.Model/Net/MimeTypes.cs | 5 ++ .../Controllers/ImageControllerTests.cs | 36 ++++++++++++ 3 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs (limited to 'tests') diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index aecdf00dc..3c5f18af5 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -91,6 +91,7 @@ public class ImageController : BaseJellyfinApiController [Authorize] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", 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."); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -121,7 +127,7 @@ public class ImageController : BaseJellyfinApiController 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 .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) @@ -145,6 +151,7 @@ public class ImageController : BaseJellyfinApiController [Authorize] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", 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."); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out string? extension)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -175,7 +187,7 @@ public class ImageController : BaseJellyfinApiController 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 .SaveImage(memoryStream, mimeType, user.ProfileImage.Path) @@ -342,6 +354,7 @@ public class ImageController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task SetItemImage( @@ -354,6 +367,11 @@ public class ImageController : BaseJellyfinApiController return NotFound(); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out _)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -379,6 +397,7 @@ public class ImageController : BaseJellyfinApiController [Authorize(Policy = Policies.RequiresElevation)] [AcceptsImageFile] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] public async Task SetItemImageByIndex( @@ -392,6 +411,11 @@ public class ImageController : BaseJellyfinApiController return NotFound(); } + if (!TryGetImageExtensionFromContentType(Request.ContentType, out _)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); await using (memoryStream.ConfigureAwait(false)) { @@ -1763,22 +1787,14 @@ public class ImageController : BaseJellyfinApiController [AcceptsImageFile] public async Task UploadCustomSplashscreen() { + if (!TryGetImageExtensionFromContentType(Request.ContentType, out var extension)) + { + return BadRequest("Incorrect ContentType."); + } + var memoryStream = await GetMemoryStream(Request.Body).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 brandingOptions = _serverConfigurationManager.GetConfiguration("branding"); brandingOptions.SplashscreenLocation = filePath; @@ -2106,4 +2122,23 @@ public class ImageController : BaseJellyfinApiController 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; + } } diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 8157dc0c2..5a1871070 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -117,7 +117,9 @@ namespace MediaBrowser.Model.Net // Type image { "image/jpeg", ".jpg" }, + { "image/tiff", ".tiff" }, { "image/x-png", ".png" }, + { "image/x-icon", ".ico" }, // Type text { "text/plain", ".txt" }, @@ -178,5 +180,8 @@ namespace MediaBrowser.Model.Net var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault(); return string.IsNullOrEmpty(extension) ? null : "." + extension; } + + public static bool IsImage(ReadOnlySpan mimeType) + => mimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase); } } diff --git a/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs new file mode 100644 index 000000000..d6428fb2c --- /dev/null +++ b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs @@ -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); + } +} -- cgit v1.2.3 From a38cb3ade8f3dc50e1a5d968c6b6ac68306bc5bb Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Tue, 10 Jan 2023 17:15:21 +0100 Subject: Fix tests --- tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs index cbab455f0..371c3811a 100644 --- a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs +++ b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs @@ -127,9 +127,10 @@ namespace Jellyfin.Model.Tests.Net [InlineData("image/jpeg", ".jpg")] [InlineData("image/png", ".png")] [InlineData("image/svg+xml", ".svg")] - [InlineData("image/tiff", ".tif")] + [InlineData("image/tiff", ".tiff")] [InlineData("image/vnd.microsoft.icon", ".ico")] [InlineData("image/webp", ".webp")] + [InlineData("image/x-icon", ".ico")] [InlineData("image/x-png", ".png")] [InlineData("text/css", ".css")] [InlineData("text/csv", ".csv")] -- cgit v1.2.3 From 48263078b46aa4ef46c0fb6944665b2c317bf077 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Fri, 17 Feb 2023 15:00:06 +0100 Subject: Reduce string allocations by regex --- Emby.Naming/AudioBook/AudioBookFilePathParser.cs | 4 +-- Emby.Naming/AudioBook/AudioBookNameParser.cs | 2 +- Emby.Naming/Common/NamingOptions.cs | 32 ---------------------- Emby.Naming/TV/EpisodePathParser.cs | 14 +++++----- Emby.Naming/Video/CleanDateTimeParser.cs | 2 +- Emby.Naming/Video/ExtraRuleResolver.cs | 2 +- Emby.Naming/Video/VideoListResolver.cs | 9 +++--- .../Library/Resolvers/Movies/MovieResolver.cs | 9 ++---- .../Users/UserManager.cs | 2 +- .../Encoder/EncoderValidator.cs | 6 ++-- .../Common/NamingOptionsTest.cs | 2 -- 11 files changed, 22 insertions(+), 62 deletions(-) (limited to 'tests') diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs index 7b4429ab1..219599d56 100644 --- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs +++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs @@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook var value = match.Groups["chapter"]; if (value.Success) { - if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { result.ChapterNumber = intValue; } @@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook var value = match.Groups["part"]; if (value.Success) { - if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { result.PartNumber = intValue; } diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs index 97b34199e..f49c3f0e7 100644 --- a/Emby.Naming/AudioBook/AudioBookNameParser.cs +++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs @@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook var value = match.Groups["year"]; if (value.Success) { - if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) + if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { result.Year = intValue; } diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 54f62a157..c16a71e02 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -453,16 +453,6 @@ namespace Emby.Naming.Common }, }; - EpisodeWithoutSeasonExpressions = new[] - { - @"[/\._ \-]()([0-9]+)(-[0-9]+)?" - }; - - EpisodeMultiPartExpressions = new[] - { - @"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)" - }; - VideoExtraRules = new[] { new ExtraRule( @@ -797,16 +787,6 @@ namespace Emby.Naming.Common /// public EpisodeExpression[] EpisodeExpressions { get; set; } - /// - /// Gets or sets list of raw episode without season regular expressions strings. - /// - public string[] EpisodeWithoutSeasonExpressions { get; set; } - - /// - /// Gets or sets list of raw multi-part episodes regular expressions strings. - /// - public string[] EpisodeMultiPartExpressions { get; set; } - /// /// Gets or sets list of video file extensions. /// @@ -877,16 +857,6 @@ namespace Emby.Naming.Common /// public Regex[] CleanStringRegexes { get; private set; } = Array.Empty(); - /// - /// Gets list of episode without season regular expressions. - /// - public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty(); - - /// - /// Gets list of multi-part episode regular expressions. - /// - public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty(); - /// /// Compiles raw regex strings into regexes. /// @@ -894,8 +864,6 @@ namespace Emby.Naming.Common { CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray(); CleanStringRegexes = CleanStrings.Select(Compile).ToArray(); - EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray(); - EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray(); } private Regex Compile(string exp) diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index d706be280..8cd5a126e 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -113,7 +113,7 @@ namespace Emby.Naming.TV if (expression.DateTimeFormats.Length > 0) { if (DateTime.TryParseExact( - match.Groups[0].Value, + match.Groups[0].ValueSpan, expression.DateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, @@ -125,7 +125,7 @@ namespace Emby.Naming.TV result.Success = true; } } - else if (DateTime.TryParse(match.Groups[0].Value, out date)) + else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date)) { result.Year = date.Year; result.Month = date.Month; @@ -138,12 +138,12 @@ namespace Emby.Naming.TV } else if (expression.IsNamed) { - if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) { result.SeasonNumber = num; } - if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) + if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EpisodeNumber = num; } @@ -158,7 +158,7 @@ namespace Emby.Naming.TV if (nextIndex >= name.Length || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal)) { - if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) + if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EndingEpisodeNumber = num; } @@ -170,12 +170,12 @@ namespace Emby.Naming.TV } else { - if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) + if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num)) { result.SeasonNumber = num; } - if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) + if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num)) { result.EpisodeNumber = num; } diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs index 0ee633dcc..9a6c6e978 100644 --- a/Emby.Naming/Video/CleanDateTimeParser.cs +++ b/Emby.Naming/Video/CleanDateTimeParser.cs @@ -43,7 +43,7 @@ namespace Emby.Naming.Video && match.Groups.Count == 5 && match.Groups[1].Success && match.Groups[2].Success - && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) + && int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year); return true; diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs index 21d0da364..3219472ef 100644 --- a/Emby.Naming/Video/ExtraRuleResolver.cs +++ b/Emby.Naming/Video/ExtraRuleResolver.cs @@ -56,7 +56,7 @@ namespace Emby.Naming.Video } else if (rule.RuleType == ExtraRuleType.Regex) { - var filename = Path.GetFileName(path); + var filename = Path.GetFileName(path.AsSpan()); var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled); diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index 01e383d1c..8247c374d 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -176,16 +176,15 @@ namespace Emby.Naming.Video } // There are no span overloads for regex unfortunately - var tmpTestFilename = testFilename.ToString(); - if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName)) + if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName)) { - tmpTestFilename = cleanName.Trim(); + testFilename = cleanName.AsSpan().Trim(); } // The CleanStringParser should have removed common keywords etc. - return string.IsNullOrEmpty(tmpTestFilename) + return testFilename.IsEmpty || testFilename[0] == '-' - || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); + || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 1522cd3ae..ef4fa1fd2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -313,13 +313,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return result; } - private static bool IsIgnored(string filename) - { - // Ignore samples - Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); - - return m.Success; - } + private static bool IsIgnored(ReadOnlySpan filename) + => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static bool ContainsFile(IReadOnlyList result, FileSystemMetadata file) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 92384986a..c4756433e 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -740,7 +740,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)", nameof(name)); } - private static bool IsValidUsername(string name) + private static bool IsValidUsername(ReadOnlySpan name) { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 9e6134b52..540d50bf1 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -277,7 +277,7 @@ namespace MediaBrowser.MediaEncoding.Encoder if (match.Success) { - if (Version.TryParse(match.Groups[1].Value, out var result)) + if (Version.TryParse(match.Groups[1].ValueSpan, out var result)) { return result; } @@ -327,8 +327,8 @@ namespace MediaBrowser.MediaEncoding.Encoder RegexOptions.Multiline)) { var version = new Version( - int.Parse(match.Groups["major"].Value, CultureInfo.InvariantCulture), - int.Parse(match.Groups["minor"].Value, CultureInfo.InvariantCulture)); + int.Parse(match.Groups["major"].ValueSpan, CultureInfo.InvariantCulture), + int.Parse(match.Groups["minor"].ValueSpan, CultureInfo.InvariantCulture)); map.Add(match.Groups["name"].Value, version); } diff --git a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs index 58aaed023..c49663248 100644 --- a/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs +++ b/tests/Jellyfin.Naming.Tests/Common/NamingOptionsTest.cs @@ -12,8 +12,6 @@ namespace Jellyfin.Naming.Tests.Common Assert.NotEmpty(options.CleanDateTimeRegexes); Assert.NotEmpty(options.CleanStringRegexes); - Assert.NotEmpty(options.EpisodeWithoutSeasonRegexes); - Assert.NotEmpty(options.EpisodeMultiPartRegexes); } [Fact] -- cgit v1.2.3 From 40a1e1924aba735c94a6575e225b8e63524f9714 Mon Sep 17 00:00:00 2001 From: Lenart Kos Date: Fri, 17 Feb 2023 22:40:54 +0100 Subject: Add rule and tests to fix #9341 Add an additional EpisodeExpression that matches `Series title Season 3 Episode 9 - Episode title.avi` correctly. Fixes #9341 --- Emby.Naming/Common/NamingOptions.cs | 10 +++++++++- tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs | 5 +++++ tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 54f62a157..b9c98c942 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -338,7 +338,15 @@ namespace Emby.Naming.Common } }, - // This isn't a Kodi naming rule, but the expression below causes false positives, + // This isn't a Kodi naming rule, but the expression below causes false episode numbers for + // Title Season X Episode X naming schemes. + // "Series Season X Episode X - Title.avi", "Series S03 E09.avi", "s3 e9 - Title.avi" + new EpisodeExpression(@".*[\\\/]((?[^\\/]+?)\s)?[Ss](?:eason)?\s*(?[0-9]+)\s+[Ee](?:pisode)?\s*(?[0-9]+).*$") + { + IsNamed = true + }, + + // Not a Kodi rule as well, but the expression below also causes false positives, // so we make sure this one gets tested first. // "Foo Bar 889" new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?[\w\s]+?)\s(?[0-9]{1,4})(-(?[0-9]{2,4}))*[^\\\/x]*$") diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs index 68059f980..406381f14 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodeNumberTests.cs @@ -73,6 +73,11 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("[BBT-RMX] Ranma ½ - 154 [50AC421A].mkv", 154)] // hyphens in the pre-name info, triple digit episode number [InlineData("Season 2/Episode 21 - 94 Meetings.mp4", 21)] // Title starts with a number [InlineData("/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv/The.Legend.of.Condor.Heroes.2017.E07.V2.web-dl.1080p.h264.aac-hdctv.mkv", 7)] + [InlineData("Season 3/The Series Season 3 Episode 9 - The title.avi", 9)] + [InlineData("Season 3/The Series S3 E9 - The title.avi", 9)] + [InlineData("Season 3/S003 E009.avi", 9)] + [InlineData("Season 3/Season 3 Episode 9.avi", 9)] + // [InlineData("Case Closed (1996-2007)/Case Closed - 317.mkv", 317)] // triple digit episode number // TODO: [InlineData("Season 2/16 12 Some Title.avi", 16)] // TODO: [InlineData("Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", 3)] diff --git a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs index af219b118..7604ddc80 100644 --- a/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs +++ b/tests/Jellyfin.Naming.Tests/TV/EpisodePathParserTest.cs @@ -30,6 +30,7 @@ namespace Jellyfin.Naming.Tests.TV [InlineData("/Season 02/Elementary - 02x03-E15 - Ep Name.mp4", false, "Elementary", 2, 3)] [InlineData("/Season 1/Elementary - S01E23-E24-E26 - The Woman.mp4", false, "Elementary", 1, 23)] [InlineData("/The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH/The Wonder Years s04e07 Christmas Party NTSC PDTV.avi", false, "The Wonder Years", 4, 7)] + [InlineData("/The.Sopranos/Season 3/The Sopranos Season 3 Episode 09 - The Telltale Moozadell.avi", false, "The Sopranos", 3, 9)] // TODO: [InlineData("/Castle Rock 2x01 Que el rio siga su curso [WEB-DL HULU 1080p h264 Dual DD5.1 Subs].mkv", "Castle Rock", 2, 1)] // TODO: [InlineData("/After Life 1x06 Episodio 6 [WEB-DL NF 1080p h264 Dual DD 5.1 Sub].mkv", "After Life", 1, 6)] // TODO: [InlineData("/Season 4/Uchuu.Senkan.Yamato.2199.E03.avi", "Uchuu Senkan Yamoto 2199", 4, 3)] -- cgit v1.2.3 From a527034ebe31e1aa43c5fd4adb98e8cff871988a Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Fri, 17 Feb 2023 15:16:08 -0700 Subject: Validate requested user id (#8812) --- Jellyfin.Api/Controllers/ArtistsController.cs | 9 ++- Jellyfin.Api/Controllers/ChannelsController.cs | 9 ++- Jellyfin.Api/Controllers/DevicesController.cs | 2 + Jellyfin.Api/Controllers/FilterController.cs | 8 ++- Jellyfin.Api/Controllers/GenresController.cs | 6 +- Jellyfin.Api/Controllers/InstantMixController.cs | 22 ++++-- Jellyfin.Api/Controllers/ItemsController.cs | 3 +- Jellyfin.Api/Controllers/LibraryController.cs | 22 +++--- Jellyfin.Api/Controllers/LiveTvController.cs | 26 ++++--- Jellyfin.Api/Controllers/MediaInfoController.cs | 5 +- Jellyfin.Api/Controllers/MoviesController.cs | 4 +- Jellyfin.Api/Controllers/MusicGenresController.cs | 6 +- Jellyfin.Api/Controllers/PersonsController.cs | 7 +- Jellyfin.Api/Controllers/PlaylistsController.cs | 8 ++- Jellyfin.Api/Controllers/QuickConnectController.cs | 11 +-- Jellyfin.Api/Controllers/SearchController.cs | 5 +- Jellyfin.Api/Controllers/StudiosController.cs | 6 +- Jellyfin.Api/Controllers/TvShowsController.cs | 15 ++-- .../Controllers/UniversalAudioController.cs | 6 +- Jellyfin.Api/Controllers/VideosController.cs | 5 +- Jellyfin.Api/Controllers/YearsController.cs | 6 +- Jellyfin.Api/Helpers/RequestHelpers.cs | 27 ++++++++ .../Helpers/RequestHelpersTests.cs | 80 ++++++++++++++++++++++ .../Controllers/ItemsControllerTests.cs | 4 +- 24 files changed, 232 insertions(+), 70 deletions(-) (limited to 'tests') diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index 11933fd97..c9d2f67f9 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -118,6 +118,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -125,7 +126,7 @@ public class ArtistsController : BaseJellyfinApiController User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(default)) + if (!userId.Value.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -321,6 +322,7 @@ public class ArtistsController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -328,7 +330,7 @@ public class ArtistsController : BaseJellyfinApiController User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (userId.HasValue && !userId.Equals(default)) + if (!userId.Value.Equals(default)) { user = _userManager.GetUserById(userId.Value); } @@ -462,11 +464,12 @@ public class ArtistsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions().AddClientFields(User); var item = _libraryManager.GetArtist(name, dtoOptions); - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 42f072f66..b5c4d8346 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -60,11 +60,12 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery] bool? supportsMediaDeletion, [FromQuery] bool? isFavorite) { + userId = RequestHelpers.GetUserId(User, userId); return _channelManager.GetChannels(new ChannelQuery { Limit = limit, StartIndex = startIndex, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, SupportsLatestItems = supportsLatestItems, SupportsMediaDeletion = supportsMediaDeletion, IsFavorite = isFavorite @@ -124,7 +125,8 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -198,7 +200,8 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 497862236..aa0dff212 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Data.Dtos; using Jellyfin.Data.Entities.Security; using Jellyfin.Data.Queries; @@ -48,6 +49,7 @@ public class DevicesController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); } diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index dd64ff903..dac07429f 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; @@ -51,7 +53,8 @@ public class FilterController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -143,7 +146,8 @@ public class FilterController : BaseJellyfinApiController [FromQuery] bool? isSeries, [FromQuery] bool? recursive) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 711fb4aef..eb03b514c 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -90,11 +90,12 @@ public class GenresController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -155,6 +156,7 @@ public class GenresController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions() .AddClientFields(User); @@ -170,7 +172,7 @@ public class GenresController : BaseJellyfinApiController item ??= new Genre(); - if (userId is null || userId.Value.Equals(default)) + if (userId.Value.Equals(default)) { return _dtoService.GetBaseItemDto(item, dtoOptions); } diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 43f09b49a..4dc2a4253 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; @@ -74,7 +75,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -110,7 +112,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var album = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -146,7 +149,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var playlist = (Playlist)_libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -181,7 +185,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -217,7 +222,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -253,7 +259,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -326,7 +333,8 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { var item = _libraryManager.GetItemById(id); - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 99366e80c..728e62810 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -240,7 +240,8 @@ public class ItemsController : BaseJellyfinApiController { var isApiKey = User.GetIsApiKey(); // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method - var user = !isApiKey && userId.HasValue && !userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = !isApiKey && !userId.Value.Equals(default) ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException() : null; diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index e8b68c7c3..bf59febed 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Data.Entities; @@ -142,12 +143,13 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -208,12 +210,13 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool inheritFromParent = false) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -403,7 +406,8 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool? isFavorite) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -437,6 +441,7 @@ public class LibraryController : BaseJellyfinApiController public ActionResult> GetAncestors([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { var item = _libraryManager.GetItemById(itemId); + userId = RequestHelpers.GetUserId(User, userId); if (item is null) { @@ -445,7 +450,7 @@ public class LibraryController : BaseJellyfinApiController var baseItemDtos = new List(); - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -675,8 +680,9 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { + userId = RequestHelpers.GetUserId(User, userId); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -691,7 +697,7 @@ public class LibraryController : BaseJellyfinApiController return new QueryResult(); } - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 318ed5c67..96fc91f93 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -153,6 +153,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool addCurrentProgram = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -161,7 +162,7 @@ public class LiveTvController : BaseJellyfinApiController new LiveTvChannelQuery { ChannelType = type, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, StartIndex = startIndex, Limit = limit, IsFavorite = isFavorite, @@ -180,7 +181,7 @@ public class LiveTvController : BaseJellyfinApiController dtoOptions, CancellationToken.None); - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -211,7 +212,8 @@ public class LiveTvController : BaseJellyfinApiController [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = channelId.Equals(default) @@ -271,6 +273,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? isLibraryItem, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -279,7 +282,7 @@ public class LiveTvController : BaseJellyfinApiController new RecordingQuery { ChannelId = channelId, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, StartIndex = startIndex, Limit = limit, Status = status, @@ -382,7 +385,8 @@ public class LiveTvController : BaseJellyfinApiController [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult> GetRecordingFolders([FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var folders = _liveTvManager.GetRecordingFolders(user); @@ -404,7 +408,8 @@ public class LiveTvController : BaseJellyfinApiController [Authorize(Policy = Policies.LiveTvAccess)] public ActionResult GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); @@ -560,7 +565,8 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -699,7 +705,8 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -737,7 +744,8 @@ public class LiveTvController : BaseJellyfinApiController [FromRoute, Required] string programId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/MediaInfoController.cs b/Jellyfin.Api/Controllers/MediaInfoController.cs index ea10dd771..da24616ff 100644 --- a/Jellyfin.Api/Controllers/MediaInfoController.cs +++ b/Jellyfin.Api/Controllers/MediaInfoController.cs @@ -132,6 +132,7 @@ public class MediaInfoController : BaseJellyfinApiController // Copy params from posted body // TODO clean up when breaking API compatibility. userId ??= playbackInfoDto?.UserId; + userId = RequestHelpers.GetUserId(User, userId); maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate; startTimeTicks ??= playbackInfoDto?.StartTimeTicks; audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex; @@ -253,10 +254,12 @@ public class MediaInfoController : BaseJellyfinApiController [FromQuery] bool? enableDirectPlay, [FromQuery] bool? enableDirectStream) { + userId ??= openLiveStreamDto?.UserId; + userId = RequestHelpers.GetUserId(User, userId); var request = new LiveStreamRequest { OpenToken = openToken ?? openLiveStreamDto?.OpenToken, - UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty, + UserId = userId.Value, PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId, MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate, StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks, diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index a9336f6d2..e1145481f 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -67,7 +68,8 @@ public class MoviesController : BaseJellyfinApiController [FromQuery] int categoryLimit = 5, [FromQuery] int itemLimit = 8) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 3db1d89c1..435457af6 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -90,11 +90,12 @@ public class MusicGenresController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -144,6 +145,7 @@ public class MusicGenresController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetMusicGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions().AddClientFields(User); MusicGenre? item; @@ -162,7 +164,7 @@ public class MusicGenresController : BaseJellyfinApiController return NotFound(); } - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 5310f50b1..b4c6f490a 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Dto; @@ -77,11 +78,12 @@ public class PersonsController : BaseJellyfinApiController [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -117,6 +119,7 @@ public class PersonsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetPerson([FromRoute, Required] string name, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions() .AddClientFields(User); @@ -126,7 +129,7 @@ public class PersonsController : BaseJellyfinApiController return NotFound(); } - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index 79c0d3c7b..c6dbea5e2 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; using MediaBrowser.Controller.Dto; @@ -81,11 +82,13 @@ public class PlaylistsController : BaseJellyfinApiController ids = createPlaylistRequest?.Ids ?? Array.Empty(); } + userId ??= createPlaylistRequest?.UserId ?? default; + userId = RequestHelpers.GetUserId(User, userId); var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest { Name = name ?? createPlaylistRequest?.Name, ItemIdList = ids, - UserId = userId ?? createPlaylistRequest?.UserId ?? default, + UserId = userId.Value, MediaType = mediaType ?? createPlaylistRequest?.MediaType }).ConfigureAwait(false); @@ -107,7 +110,8 @@ public class PlaylistsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids, [FromQuery] Guid? userId) { - await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false); + userId = RequestHelpers.GetUserId(User, userId); + await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId.Value).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs index 503b9d372..d7e54b5b6 100644 --- a/Jellyfin.Api/Controllers/QuickConnectController.cs +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Net; @@ -116,17 +117,11 @@ public class QuickConnectController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> AuthorizeQuickConnect([FromQuery, Required] string code, [FromQuery] Guid? userId = null) { - var currentUserId = User.GetUserId(); - var actualUserId = userId ?? currentUserId; - - if (actualUserId.Equals(default) || (!userId.Equals(currentUserId) && !User.IsInRole(UserRoles.Administrator))) - { - return Forbid("Unknown user id"); - } + userId = RequestHelpers.GetUserId(User, userId); try { - return await _quickConnect.AuthorizeRequest(actualUserId, code).ConfigureAwait(false); + return await _quickConnect.AuthorizeRequest(userId.Value, code).ConfigureAwait(false); } catch (AuthenticationException) { diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index a25b43345..f638c31c3 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -3,6 +3,8 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -98,6 +100,7 @@ public class SearchController : BaseJellyfinApiController [FromQuery] bool includeStudios = true, [FromQuery] bool includeArtists = true) { + userId = RequestHelpers.GetUserId(User, userId); var result = _searchEngine.GetSearchHints(new SearchQuery { Limit = limit, @@ -108,7 +111,7 @@ public class SearchController : BaseJellyfinApiController IncludePeople = includePeople, IncludeStudios = includeStudios, StartIndex = startIndex, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, IncludeItemTypes = includeItemTypes, ExcludeItemTypes = excludeItemTypes, MediaTypes = mediaTypes, diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index 21965e956..f434f60f5 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -86,11 +86,12 @@ public class StudiosController : BaseJellyfinApiController [FromQuery] bool? enableImages = true, [FromQuery] bool enableTotalRecordCount = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -139,10 +140,11 @@ public class StudiosController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetStudio([FromRoute, Required] string name, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions().AddClientFields(User); var item = _libraryManager.GetStudio(name); - if (userId.HasValue && !userId.Equals(default)) + if (!userId.Equals(default)) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index b0760f97c..7d23281f2 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Jellyfin.Api.Extensions; +using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -87,6 +88,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool disableFirstEpisode = false, [FromQuery] bool enableRewatching = false) { + userId = RequestHelpers.GetUserId(User, userId); var options = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); @@ -98,7 +100,7 @@ public class TvShowsController : BaseJellyfinApiController ParentId = parentId, SeriesId = seriesId, StartIndex = startIndex, - UserId = userId ?? Guid.Empty, + UserId = userId.Value, EnableTotalRecordCount = enableTotalRecordCount, DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, @@ -106,7 +108,7 @@ public class TvShowsController : BaseJellyfinApiController }, options); - var user = userId is null || userId.Value.Equals(default) + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -144,7 +146,8 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -215,7 +218,8 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] string? sortBy) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); @@ -331,7 +335,8 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery] bool? enableUserData) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 345521597..12d033ae6 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -106,11 +106,7 @@ public class UniversalAudioController : BaseJellyfinApiController [FromQuery] bool enableRedirection = true) { var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels); - - if (!userId.HasValue || userId.Value.Equals(default)) - { - userId = User.GetUserId(); - } + userId = RequestHelpers.GetUserId(User, userId); _logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", deviceProfile); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 3a61367f7..c0ec646ed 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -104,12 +104,13 @@ public class VideosController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { - var user = userId is null || userId.Value.Equals(default) + userId = RequestHelpers.GetUserId(User, userId); + var user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); var item = itemId.Equals(default) - ? (userId is null || userId.Value.Equals(default) + ? (userId.Value.Equals(default) ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index def37cb97..74370db50 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -85,11 +85,12 @@ public class YearsController : BaseJellyfinApiController [FromQuery] bool recursive = true, [FromQuery] bool? enableImages = true) { + userId = RequestHelpers.GetUserId(User, userId); var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId is null || userId.Value.Equals(default) + User? user = userId.Value.Equals(default) ? null : _userManager.GetUserById(userId.Value); BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -171,6 +172,7 @@ public class YearsController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetYear([FromRoute, Required] int year, [FromQuery] Guid? userId) { + userId = RequestHelpers.GetUserId(User, userId); var item = _libraryManager.GetYear(year); if (item is null) { @@ -180,7 +182,7 @@ public class YearsController : BaseJellyfinApiController var dtoOptions = new DtoOptions() .AddClientFields(User); - if (userId.HasValue && !userId.Value.Equals(default)) + if (!userId.Value.Equals(default)) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 0b7a4fa1a..57098edba 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -11,6 +11,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Querying; @@ -55,6 +56,32 @@ public static class RequestHelpers return result; } + /// + /// Checks if the user can access a user. + /// + /// The for the current request. + /// The user id. + /// A whether the user can access the user. + internal static Guid GetUserId(ClaimsPrincipal claimsPrincipal, Guid? userId) + { + var authenticatedUserId = claimsPrincipal.GetUserId(); + + // UserId not provided, fall back to authenticated user id. + if (userId is null || userId.Value.Equals(default)) + { + return authenticatedUserId; + } + + // User must be administrator to access another user. + var isAdministrator = claimsPrincipal.IsInRole(UserRoles.Administrator); + if (!userId.Value.Equals(authenticatedUserId) && !isAdministrator) + { + throw new SecurityException("Forbidden"); + } + + return userId.Value; + } + /// /// Checks if the user can update an entry. /// diff --git a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs index c4640bd22..2d7741d81 100644 --- a/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs +++ b/tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs @@ -1,7 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Security.Claims; +using Jellyfin.Api.Constants; using Jellyfin.Api.Helpers; using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Net; using Xunit; namespace Jellyfin.Api.Tests.Helpers @@ -15,6 +19,82 @@ namespace Jellyfin.Api.Tests.Helpers Assert.Equal(expected, RequestHelpers.GetOrderBy(sortBy, requestedSortOrder)); } + [Fact] + public static void GetUserId_IsAdmin() + { + Guid? requestUserId = Guid.NewGuid(); + Guid? authUserId = Guid.NewGuid(); + + var claims = new[] + { + new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.IsApiKey, bool.FalseString), + new Claim(ClaimTypes.Role, UserRoles.Administrator) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + var userId = RequestHelpers.GetUserId(principal, requestUserId); + + Assert.Equal(requestUserId, userId); + } + + [Fact] + public static void GetUserId_IsApiKey_EmptyGuid() + { + Guid? requestUserId = Guid.Empty; + + var claims = new[] + { + new Claim(InternalClaimTypes.IsApiKey, bool.TrueString) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + var userId = RequestHelpers.GetUserId(principal, requestUserId); + + Assert.Equal(Guid.Empty, userId); + } + + [Fact] + public static void GetUserId_IsApiKey_Null() + { + Guid? requestUserId = null; + + var claims = new[] + { + new Claim(InternalClaimTypes.IsApiKey, bool.TrueString) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + var userId = RequestHelpers.GetUserId(principal, requestUserId); + + Assert.Equal(Guid.Empty, userId); + } + + [Fact] + public static void GetUserId_IsUser() + { + Guid? requestUserId = Guid.NewGuid(); + Guid? authUserId = Guid.NewGuid(); + + var claims = new[] + { + new Claim(InternalClaimTypes.UserId, authUserId.Value.ToString("N", CultureInfo.InvariantCulture)), + new Claim(InternalClaimTypes.IsApiKey, bool.FalseString), + new Claim(ClaimTypes.Role, UserRoles.User) + }; + + var identity = new ClaimsIdentity(claims, string.Empty); + var principal = new ClaimsPrincipal(identity); + + Assert.Throws(() => RequestHelpers.GetUserId(principal, requestUserId)); + } + public static TheoryData, IReadOnlyList, (string, SortOrder)[]> GetOrderBy_Success_TestData() { var data = new TheoryData, IReadOnlyList, (string, SortOrder)[]>(); diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs index 62b32b92e..078002994 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/ItemsControllerTests.cs @@ -22,13 +22,13 @@ public sealed class ItemsControllerTests : IClassFixture