Smart Filter Encoding Fix (#2387)

This commit is contained in:
Joe Milazzo 2023-11-02 08:35:43 -05:00 committed by GitHub
parent b6d4938e22
commit 9894a2623c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 677 additions and 471 deletions

View File

@ -10,8 +10,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.9" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.9" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.10" />
<PackageReference Include="BenchmarkDotNet.Annotations" Version="0.13.10" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
</ItemGroup>

View File

@ -6,12 +6,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.13" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="19.2.69" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="19.2.69" />
<PackageReference Include="xunit" Version="2.5.2" />
<PackageReference Include="xunit" Version="2.6.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View File

@ -5,7 +5,6 @@ using API.DTOs.Filtering;
using API.DTOs.Filtering.v2;
using API.Entities.Enums;
using API.Helpers;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
using Xunit;
namespace API.Tests.Helpers;
@ -15,25 +14,25 @@ public class SmartFilterHelperTests
[Fact]
public void Test_Decode()
{
var encoded = """
stmts=comparison%3D5%26field%3D18%26value%3D95%2Ccomparison%3D0%26field%3D4%26value%3D0%2Ccomparison%3D7%26field%3D1%26value%3Da&sortOptions=sortField=2&isAscending=false&limitTo=10&combination=1
""";
const string encoded = """
name=Test&stmts=comparison%253D0%25C2%25A6field%253D18%25C2%25A6value%253D95<EFBFBD>comparison%253D0%25C2%25A6field%253D4%25C2%25A6value%253D0<EFBFBD>comparison%253D7%25C2%25A6field%253D1%25C2%25A6value%253Da&sortOptions=sortField%3D2¦isAscending%3DFalse&limitTo=10&combination=1
""";
var filter = SmartFilterHelper.Decode(encoded);
Assert.Equal(10, filter.LimitTo);
Assert.Equal(SortField.CreatedDate, filter.SortOptions.SortField);
Assert.False(filter.SortOptions.IsAscending);
Assert.Null(filter.Name);
Assert.Equal("Test" , filter.Name);
var list = filter.Statements.ToList();
AssertStatementSame(list[2], FilterField.SeriesName, FilterComparison.Matches, "a");
AssertStatementSame(list[1], FilterField.AgeRating, FilterComparison.Equal, (int) AgeRating.Unknown + string.Empty);
AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Contains, "95");
AssertStatementSame(list[0], FilterField.Genres, FilterComparison.Equal, "95");
}
[Fact]
public void Test_Encode()
public void Test_EncodeDecode()
{
var filter = new FilterV2Dto()
{
@ -56,10 +55,61 @@ public class SmartFilterHelperTests
};
var encodedFilter = SmartFilterHelper.Encode(filter);
Assert.Equal("name=Test&stmts=comparison%253D0%252Cfield%253D4%252Cvalue%253D0&sortOptions=sortField%3D2%2CisAscending%3DFalse&limitTo=10&combination=1", encodedFilter);
var decoded = SmartFilterHelper.Decode(encodedFilter);
Assert.Single(decoded.Statements);
AssertStatementSame(decoded.Statements.First(), filter.Statements.First());
Assert.Equal("Test", decoded.Name);
Assert.Equal(10, decoded.LimitTo);
Assert.Equal(SortField.CreatedDate, decoded.SortOptions.SortField);
Assert.False(decoded.SortOptions.IsAscending);
}
private void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value)
[Fact]
public void Test_EncodeDecode_MultipleValues_Contains()
{
var filter = new FilterV2Dto()
{
Name = "Test",
SortOptions = new SortOptions() {
IsAscending = false,
SortField = SortField.CreatedDate
},
LimitTo = 10,
Combination = FilterCombination.And,
Statements = new List<FilterStatementDto>()
{
new FilterStatementDto()
{
Comparison = FilterComparison.Equal,
Field = FilterField.AgeRating,
Value = $"{(int) AgeRating.Unknown + string.Empty},{(int) AgeRating.G + string.Empty}"
}
}
};
var encodedFilter = SmartFilterHelper.Encode(filter);
var decoded = SmartFilterHelper.Decode(encodedFilter);
Assert.Single(decoded.Statements);
AssertStatementSame(decoded.Statements.First(), filter.Statements.First());
Assert.Equal(2, decoded.Statements.First().Value.Split(",").Length);
Assert.Equal("Test", decoded.Name);
Assert.Equal(10, decoded.LimitTo);
Assert.Equal(SortField.CreatedDate, decoded.SortOptions.SortField);
Assert.False(decoded.SortOptions.IsAscending);
}
private static void AssertStatementSame(FilterStatementDto statement, FilterStatementDto statement2)
{
Assert.Equal(statement.Field, statement2.Field);
Assert.Equal(statement.Comparison, statement2.Comparison);
Assert.Equal(statement.Value, statement2.Value);
}
private static void AssertStatementSame(FilterStatementDto statement, FilterField field, FilterComparison combination, string value)
{
Assert.Equal(statement.Field, field);
Assert.Equal(statement.Comparison, combination);

View File

@ -23,7 +23,7 @@ public abstract class SiteThemeServiceTest : AbstractDbTest
private readonly IEventHub _messageHub = Substitute.For<IEventHub>();
protected SiteThemeServiceTest(ITestOutputHelper testOutputHelper) : base()
protected SiteThemeServiceTest(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
@ -56,7 +56,7 @@ public abstract class SiteThemeServiceTest : AbstractDbTest
});
await _context.SaveChangesAsync();
var ex = await Assert.ThrowsAsync<KavitaException>(async () => await siteThemeService.UpdateDefault(10));
var ex = await Assert.ThrowsAsync<KavitaException>(() => siteThemeService.UpdateDefault(10));
Assert.Equal("Theme file missing or invalid", ex.Message);
}

View File

@ -55,27 +55,27 @@
<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Docnet.Core" Version="2.6.0" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.1" />
<PackageReference Include="ExCSS" Version="4.2.2" />
<PackageReference Include="EasyCaching.InMemory" Version="1.9.2" />
<PackageReference Include="ExCSS" Version="4.2.4" />
<PackageReference Include="Flurl" Version="3.0.7" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Hangfire" Version="1.8.5" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.5" />
<PackageReference Include="Hangfire.InMemory" Version="0.5.1" />
<PackageReference Include="Hangfire" Version="1.8.6" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.6" />
<PackageReference Include="Hangfire.InMemory" Version="0.6.0" />
<PackageReference Include="Hangfire.MaximumConcurrentExecutions" Version="1.1.0" />
<PackageReference Include="Hangfire.MemoryStorage.Core" Version="1.4.0" />
<PackageReference Include="Hangfire.Storage.SQLite" Version="0.3.4" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.53" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.54" />
<PackageReference Include="MarkdownDeep.NET.Core" Version="1.5.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageReference Include="MimeTypeMapOfficial" Version="1.0.17" />

View File

@ -2,6 +2,8 @@
namespace API.Comparators;
#nullable enable
/// <summary>
/// Sorts chapters based on their Number. Uses natural ordering of doubles.
/// </summary>

View File

@ -2,6 +2,8 @@
namespace API.Comparators;
#nullable enable
public class NumericComparer : IComparer
{

View File

@ -6,6 +6,7 @@ using static System.Char;
namespace API.Comparators;
public static class StringLogicalComparer
{
public static int Compare(string s1, string s2)

View File

@ -29,6 +29,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
/// <summary>
/// All Account matters
/// </summary>

View File

@ -6,6 +6,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
public class AdminController : BaseApiController
{
private readonly UserManager<AppUser> _userManager;

View File

@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
[ApiController]
[Route("api/[controller]")]
[Authorize]

View File

@ -14,6 +14,8 @@ using VersOne.Epub;
namespace API.Controllers;
#nullable enable
public class BookController : BaseApiController
{
private readonly IBookService _bookService;

View File

@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
/// <summary>
/// Responsible for the CBL import flow
/// </summary>

View File

@ -13,6 +13,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
/// <summary>
/// APIs for Collections
/// </summary>

View File

@ -12,6 +12,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
/// <summary>
/// Responsible interacting and creating Devices
/// </summary>

View File

@ -16,6 +16,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
/// <summary>
/// All APIs related to downloading entities from the system. Requires Download Role or Admin Role.
/// </summary>

View File

@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
[AllowAnonymous]
public class FallbackController : Controller
{

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using API.Constants;
using API.Data;
using API.Data.Repositories;
using API.DTOs.Dashboard;
@ -10,23 +9,22 @@ using API.DTOs.Filtering.v2;
using API.Entities;
using API.Extensions;
using API.Helpers;
using EasyCaching.Core;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
/// <summary>
/// This is responsible for Filter caching
/// </summary>
public class FilterController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
private readonly IEasyCachingProviderFactory _cacheFactory;
public FilterController(IUnitOfWork unitOfWork, IEasyCachingProviderFactory cacheFactory)
public FilterController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_cacheFactory = cacheFactory;
}
/// <summary>
@ -93,4 +91,26 @@ public class FilterController : BaseApiController
await _unitOfWork.CommitAsync();
return Ok();
}
/// <summary>
/// Encode the Filter
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("encode")]
public ActionResult<string> EncodeFilter(FilterV2Dto dto)
{
return Ok(SmartFilterHelper.Encode(dto));
}
/// <summary>
/// Decodes the Filter
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
[HttpPost("decode")]
public ActionResult<FilterV2Dto> DecodeFilter(DecodeFilterDto dto)
{
return Ok(SmartFilterHelper.Decode(dto.EncodedFilter));
}
}

View File

@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
[AllowAnonymous]
public class HealthController : BaseApiController
{

View File

@ -13,6 +13,8 @@ using MimeTypes;
namespace API.Controllers;
#nullable enable
/// <summary>
/// Responsible for servicing up images stored in Kavita for entities
/// </summary>

View File

@ -27,6 +27,8 @@ using TaskScheduler = API.Services.TaskScheduler;
namespace API.Controllers;
#nullable enable
[Authorize]
public class LibraryController : BaseApiController
{

View File

@ -15,6 +15,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
public class LicenseController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;

View File

@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
public class LocaleController : BaseApiController
{
private readonly ILocalizationService _localizationService;

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
public class MetadataController : BaseApiController
{

View File

@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
/// <summary>
/// For the Panels app explicitly
/// </summary>

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using API.Data;
using API.DTOs;
@ -11,6 +12,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
public class PluginController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;
@ -43,7 +46,7 @@ public class PluginController : BaseApiController
var userId = await _unitOfWork.UserRepository.GetUserIdByApiKeyAsync(apiKey);
if (userId <= 0)
{
_logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {Information}", pluginName, new
_logger.LogInformation("A Plugin ({PluginName}) tried to authenticate with an apiKey that doesn't match. Information {@Information}", Uri.EscapeDataString(pluginName), new
{
IpAddress = ipAddress,
UserAgent = userAgent,
@ -52,7 +55,7 @@ public class PluginController : BaseApiController
throw new KavitaUnauthenticatedUserException();
}
var user = await _unitOfWork.UserRepository.GetUserByIdAsync(userId);
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", pluginName, user!.UserName, userId);
_logger.LogInformation("Plugin {PluginName} has authenticated with {UserName} ({UserId})'s API Key", Uri.EscapeDataString(pluginName), user!.UserName, userId);
return new UserDto
{
Username = user.UserName!,

View File

@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
/// <summary>
/// Responsible for providing external ratings for Series
/// </summary>

View File

@ -26,6 +26,8 @@ using MimeTypes;
namespace API.Controllers;
#nullable enable
/// <summary>
/// For all things regarding reading, mainly focusing on non-Book related entities
/// </summary>

View File

@ -16,6 +16,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
[Authorize]
public class ReadingListController : BaseApiController
{

View File

@ -17,6 +17,8 @@ using Newtonsoft.Json;
namespace API.Controllers;
#nullable enable
public class RecommendedController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;

View File

@ -18,6 +18,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
public class ReviewController : BaseApiController
{
private readonly ILogger<ReviewController> _logger;

View File

@ -21,6 +21,7 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
public class ScrobblingController : BaseApiController
{

View File

@ -10,6 +10,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
/// <summary>
/// Responsible for the Search interface from the UI
/// </summary>

View File

@ -28,6 +28,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
public class SeriesController : BaseApiController
{
private readonly ILogger<SeriesController> _logger;

View File

@ -20,13 +20,14 @@ using Hangfire.Storage;
using Kavita.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using MimeTypes;
using TaskScheduler = API.Services.TaskScheduler;
namespace API.Controllers;
#nullable enable
[Authorize(Policy = "RequireAdminRole")]
public class ServerController : BaseApiController
{
@ -286,8 +287,6 @@ public class ServerController : BaseApiController
if (emailServiceUrl.Equals(EmailService.DefaultApiUrl)) return Ok(null);
return Ok(await _emailService.GetVersion(emailServiceUrl));
}
}

View File

@ -24,6 +24,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
public class SettingsController : BaseApiController
{
private readonly ILogger<SettingsController> _logger;

View File

@ -14,6 +14,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
public class StatsController : BaseApiController
{
private readonly IStatisticService _statService;

View File

@ -10,6 +10,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
/// <summary>
/// Responsible for anything that deals with Streams (SmartFilters, ExternalSource, DashboardStream, SideNavStream)
/// </summary>

View File

@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
/// <summary>
/// All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any
/// other purposes.

View File

@ -11,6 +11,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
public class ThemeController : BaseApiController
{
private readonly IUnitOfWork _unitOfWork;

View File

@ -13,6 +13,8 @@ using Microsoft.Extensions.Logging;
namespace API.Controllers;
#nullable enable
/// <summary>
///
/// </summary>

View File

@ -13,6 +13,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
[Authorize]
public class UsersController : BaseApiController
{

View File

@ -16,6 +16,8 @@ using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
#nullable enable
/// <summary>
/// Responsible for all things Want To Read
/// </summary>
@ -42,7 +44,7 @@ public class WantToReadController : BaseApiController
/// <returns></returns>
[HttpPost]
[Obsolete("use v2 instead")]
public async Task<ActionResult<PagedList<SeriesDto>>> GetWantToRead([FromQuery] UserParams userParams, FilterDto filterDto)
public async Task<ActionResult<PagedList<SeriesDto>>> GetWantToRead([FromQuery] UserParams? userParams, FilterDto filterDto)
{
userParams ??= new UserParams();
var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserAsync(User.GetUserId(), userParams, filterDto);
@ -60,7 +62,7 @@ public class WantToReadController : BaseApiController
/// <param name="filterDto"></param>
/// <returns></returns>
[HttpPost("v2")]
public async Task<ActionResult<PagedList<SeriesDto>>> GetWantToReadV2([FromQuery] UserParams userParams, FilterV2Dto filterDto)
public async Task<ActionResult<PagedList<SeriesDto>>> GetWantToReadV2([FromQuery] UserParams? userParams, FilterV2Dto filterDto)
{
userParams ??= new UserParams();
var pagedList = await _unitOfWork.SeriesRepository.GetWantToReadForUserV2Async(User.GetUserId(), userParams, filterDto);

View File

@ -0,0 +1,9 @@
namespace API.DTOs.Filtering.v2;
/// <summary>
/// For requesting an encoded filter to be decoded
/// </summary>
public class DecodeFilterDto
{
public string EncodedFilter { get; set; }
}

View File

@ -0,0 +1,61 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using API.DTOs.Filtering.v2;
using API.Helpers;
using Microsoft.Extensions.Logging;
namespace API.Data.ManualMigrations;
/// <summary>
/// v0.7.10.2 introduced a bad encoding, this will migrate those bad smart filters
/// </summary>
public static class MigrateSmartFilterEncoding
{
public static async Task Migrate(IUnitOfWork unitOfWork, DataContext dataContext, ILogger<Program> logger)
{
logger.LogCritical("Running MigrateSmartFilterEncoding migration - Please be patient, this may take some time. This is not an error");
var statementsRegex = new Regex("stmts=(?<Statements>.*?)&");
const string valueRegex = @"value=(?<value>\d+)";
const string fieldRegex = @"field=(?<value>\d+)";
const string comparisonRegex = @"comparison=(?<value>\d+)";
var smartFilters = dataContext.AppUserSmartFilter.ToList();
foreach (var filter in smartFilters)
{
if (filter.Filter.Contains(SmartFilterHelper.StatementSeparator)) continue;
var statements = statementsRegex.Matches(filter.Filter)
.Select(match => match.Groups["Statements"])
.FirstOrDefault(group => group.Success && group != Match.Empty)?.Value;
if (string.IsNullOrEmpty(statements)) continue;
// We have statements. Let's remove the statements and generate a filter dto
var noStmt = statementsRegex.Replace(filter.Filter, string.Empty).Replace("stmts=", string.Empty);
var filterDto = SmartFilterHelper.Decode(noStmt);
// Now we just parse each individual stmt into the core components and add to statements
var individualParts = Uri.UnescapeDataString(statements).Split(',').Select(Uri.UnescapeDataString);
foreach (var part in individualParts)
{
filterDto.Statements.Add(new FilterStatementDto()
{
Value = Regex.Match(part, valueRegex).Groups["value"].Value,
Field = Enum.Parse<FilterField>(Regex.Match(part, fieldRegex).Groups["value"].Value),
Comparison = Enum.Parse<FilterComparison>(Regex.Match(part, comparisonRegex).Groups["value"].Value),
});
}
filter.Filter = SmartFilterHelper.Encode(filterDto);
}
if (unitOfWork.HasChanges())
{
await unitOfWork.CommitAsync();
}
logger.LogCritical("Running MigrateSmartFilterEncoding migration - Completed. This is not an error");
}
}

View File

@ -627,7 +627,6 @@ public class UserRepository : IUserRepository
return await ApplyLimit(filterSeriesQuery
.Sort(filter.SortOptions)
.AsSplitQuery(), filter.LimitTo)
.Select(o => o.Bookmark)
.ProjectTo<BookmarkDto>(_mapper.ConfigurationProvider)
.ToListAsync();
}

View File

@ -4,6 +4,7 @@ using API.Entities;
using API.Helpers;
namespace API.Extensions;
#nullable enable
public static class AppUserExtensions
{

View File

@ -17,6 +17,7 @@ using Microsoft.Extensions.DependencyInjection;
namespace API.Extensions;
public static class ApplicationServiceExtensions
{
public static void AddApplicationServices(this IServiceCollection services, IConfiguration config, IWebHostEnvironment env)

View File

@ -5,6 +5,7 @@ using API.Helpers;
using API.Services.Tasks.Scanner.Parser;
namespace API.Extensions;
#nullable enable
public static class ChapterListExtensions
{

View File

@ -3,6 +3,7 @@ using Kavita.Common;
using JwtRegisteredClaimNames = Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames;
namespace API.Extensions;
#nullable enable
public static class ClaimsPrincipalExtensions
{

View File

@ -1,6 +1,7 @@
using System;
namespace API.Extensions;
#nullable enable
public static class DateTimeExtensions
{

View File

@ -2,6 +2,7 @@
using API.Entities.Enums;
namespace API.Extensions;
#nullable enable
public static class EncodeFormatExtensions
{

View File

@ -6,6 +6,7 @@ using API.Data.Misc;
using API.Entities.Enums;
namespace API.Extensions;
#nullable enable
public static class EnumerableExtensions
{

View File

@ -2,6 +2,7 @@
using System.IO;
namespace API.Extensions;
#nullable enable
public static class FileInfoExtensions
{

View File

@ -4,6 +4,7 @@ using API.DTOs.Filtering;
using API.Entities.Enums;
namespace API.Extensions;
#nullable enable
public static class FilterDtoExtensions
{

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace API.Extensions;
#nullable enable
public static class HttpExtensions
{

View File

@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
namespace API.Extensions;
#nullable enable
public static class IdentityServiceExtensions
{

View File

@ -4,6 +4,7 @@ using API.Entities;
using API.Services.Tasks.Scanner.Parser;
namespace API.Extensions;
#nullable enable
public static class ParserInfoListExtensions
{

View File

@ -1,6 +1,7 @@
using System.IO;
namespace API.Extensions;
#nullable enable
public static class PathExtensions
{

View File

@ -1,14 +1,14 @@
using System.Linq;
using API.DTOs.Filtering;
using API.Entities;
using API.Extensions.QueryExtensions;
namespace API.Extensions.QueryExtensions.Filtering;
#nullable enable
public class BookmarkSeriesPair
{
public AppUserBookmark Bookmark { get; set; }
public Series Series { get; set; }
public AppUserBookmark Bookmark { get; init; } = null!;
public Series Series { get; init; } = null!;
}
public static class BookmarkSort

View File

@ -9,7 +9,6 @@ using Kavita.Common;
using Microsoft.EntityFrameworkCore;
namespace API.Extensions.QueryExtensions.Filtering;
#nullable enable
public static class SeriesFilter

View File

@ -1,7 +1,9 @@
using System.Linq;
using API.DTOs.Filtering;
using API.Entities;
using API.Extensions.QueryExtensions;
namespace API.Extensions.QueryExtensions.Filtering;
#nullable enable
public static class SeriesSort
{

View File

@ -4,6 +4,7 @@ using API.Entities;
using Microsoft.EntityFrameworkCore;
namespace API.Extensions.QueryExtensions;
#nullable enable
/// <summary>
/// All extensions against IQueryable that enables the dynamic including based on bitwise flag pattern

View File

@ -12,6 +12,7 @@ using API.Entities.Scrobble;
using Microsoft.EntityFrameworkCore;
namespace API.Extensions.QueryExtensions;
#nullable enable
public static class QueryableExtensions
{

View File

@ -4,6 +4,7 @@ using API.Entities;
using API.Entities.Enums;
namespace API.Extensions.QueryExtensions;
#nullable enable
/// <summary>
/// Responsible for restricting Entities based on an AgeRestriction

View File

@ -1,5 +1,4 @@
#nullable enable
using System.Collections.Generic;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using API.Comparators;
@ -7,6 +6,7 @@ using API.Entities;
using API.Services.Tasks.Scanner.Parser;
namespace API.Extensions;
#nullable enable
public static class SeriesExtensions
{

View File

@ -2,6 +2,7 @@
using System.Text.RegularExpressions;
namespace API.Extensions;
#nullable enable
public static class StringExtensions
{

View File

@ -5,6 +5,7 @@ using API.Entities;
using API.Entities.Enums;
namespace API.Extensions;
#nullable enable
public static class VolumeListExtensions
{

View File

@ -3,6 +3,7 @@ using System.IO.Compression;
using System.Linq;
namespace API.Extensions;
#nullable enable
public static class ZipArchiveExtensions
{

View File

@ -1,24 +0,0 @@
using API.DTOs.Filtering.v2;
using API.Entities;
namespace API.Helpers.Builders;
public class SmartFilterBuilder : IEntityBuilder<AppUserSmartFilter>
{
private AppUserSmartFilter _smartFilter;
public AppUserSmartFilter Build() => _smartFilter;
public SmartFilterBuilder(FilterV2Dto filter)
{
_smartFilter = new AppUserSmartFilter()
{
Name = filter.Name,
Filter = SmartFilterHelper.Encode(filter)
};
}
// public SmartFilterBuilder WithName(string name)
// {
//
// }
}

View File

@ -4,6 +4,7 @@ using API.Entities.Interfaces;
using API.Services;
namespace API.Helpers;
#nullable enable
public interface ICacheHelper
{

View File

@ -2,6 +2,7 @@
using Hangfire;
namespace API.Helpers.Converters;
#nullable enable
public static class CronConverter
{

View File

@ -7,6 +7,7 @@ using API.Entities.Enums;
using API.Extensions;
namespace API.Helpers.Converters;
#nullable enable
public static class FilterFieldValueConverter
{

View File

@ -6,6 +6,7 @@ using API.Entities.Enums;
using AutoMapper;
namespace API.Helpers.Converters;
#nullable enable
public class ServerSettingConverter : ITypeConverter<IEnumerable<ServerSetting>, ServerSettingDto>
{

View File

@ -3,6 +3,7 @@ using API.DTOs.Scrobbling;
using API.Entities.Enums;
namespace API.Helpers;
#nullable enable
public static class LibraryTypeHelper
{

View File

@ -1,4 +1,5 @@
namespace API.Helpers;
#nullable enable
public static class NumberHelper
{

View File

@ -2,6 +2,7 @@
using API.Entities;
namespace API.Helpers;
#nullable enable
public static class OrderableHelper
{

View File

@ -5,10 +5,11 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
namespace API.Helpers;
#nullable enable
public class PagedList<T> : List<T>
{
public PagedList(IEnumerable<T> items, int count, int pageNumber, int pageSize)
private PagedList(IEnumerable<T> items, int count, int pageNumber, int pageSize)
{
CurrentPage = pageNumber;
TotalPages = (int) Math.Ceiling(count / (double) pageSize);

View File

@ -1,4 +1,5 @@
namespace API.Helpers;
#nullable enable
public class PaginationHeader
{

View File

@ -6,6 +6,7 @@ using API.Services.Tasks.Scanner;
using API.Services.Tasks.Scanner.Parser;
namespace API.Helpers;
#nullable enable
public static class ParserInfoHelpers
{

View File

@ -8,6 +8,7 @@ using API.Extensions;
using API.Helpers.Builders;
namespace API.Helpers;
#nullable enable
public static class PersonHelper
{
@ -29,6 +30,7 @@ public static class PersonHelper
foreach (var name in names)
{
var normalizedName = name.ToNormalized();
// BUG: Doesn't this create a duplicate entry because allPeopleTypeRoles is a different instance?
var person = allPeopleTypeRole.Find(p =>
p.NormalizedName != null && p.NormalizedName.Equals(normalizedName));
if (person == null)

View File

@ -1 +0,0 @@


View File

@ -1,29 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
namespace API.Helpers;
public static class SqlHelper
{
public static List<T> RawSqlQuery<T>(DbContext context, string query, Func<DbDataReader, T> map)
{
using var command = context.Database.GetDbConnection().CreateCommand();
command.CommandText = query;
command.CommandType = CommandType.Text;
context.Database.OpenConnection();
using var result = command.ExecuteReader();
var entities = new List<T>();
while (result.Read())
{
entities.Add(map(result));
}
return entities;
}
}

View File

@ -6,6 +6,7 @@ using API.Extensions;
using API.Services.Tasks.Scanner;
namespace API.Helpers;
#nullable enable
public static class SeriesHelper
{

View File

@ -5,14 +5,24 @@ using System.Web;
using API.DTOs.Filtering;
using API.DTOs.Filtering.v2;
#nullable enable
namespace API.Helpers;
public static class SmartFilterHelper
{
private const string SortOptionsKey = "sortOptions=";
private const string NameKey = "name=";
private const string SortFieldKey = "sortField=";
private const string IsAscendingKey = "isAscending=";
private const string StatementsKey = "stmts=";
private const string LimitToKey = "limitTo=";
private const string CombinationKey = "combination=";
private const string StatementComparisonKey = "comparison=";
private const string StatementFieldKey = "field=";
private const string StatementValueKey = "value=";
public const string StatementSeparator = "\ufffd";
public const string InnerStatementSeparator = "¦";
public static FilterV2Dto Decode(string? encodedFilter)
{
@ -21,7 +31,7 @@ public static class SmartFilterHelper
return new FilterV2Dto(); // Create a default filter if the input is empty
}
string[] parts = encodedFilter.Split('&');
var parts = encodedFilter.Split('&');
var filter = new FilterV2Dto();
foreach (var part in parts)
@ -42,7 +52,7 @@ public static class SmartFilterHelper
{
filter.Statements = DecodeFilterStatementDtos(part.Substring(StatementsKey.Length));
}
else if (part.StartsWith("name="))
else if (part.StartsWith(NameKey))
{
filter.Name = HttpUtility.UrlDecode(part.Substring(5));
}
@ -51,7 +61,7 @@ public static class SmartFilterHelper
return filter;
}
public static string Encode(FilterV2Dto filter)
public static string Encode(FilterV2Dto? filter)
{
if (filter == null)
return string.Empty;
@ -59,50 +69,50 @@ public static class SmartFilterHelper
var encodedStatements = EncodeFilterStatementDtos(filter.Statements);
var encodedSortOptions = filter.SortOptions != null
? $"{SortOptionsKey}{EncodeSortOptions(filter.SortOptions)}"
: "";
: string.Empty;
var encodedLimitTo = $"{LimitToKey}{filter.LimitTo}";
return $"{EncodeName(filter.Name)}{encodedStatements}&{encodedSortOptions}&{encodedLimitTo}&{CombinationKey}{(int) filter.Combination}";
}
private static string EncodeName(string name)
private static string EncodeName(string? name)
{
return string.IsNullOrWhiteSpace(name) ? string.Empty : $"name={HttpUtility.UrlEncode(name)}&";
return string.IsNullOrWhiteSpace(name) ? string.Empty : $"{NameKey}{Uri.EscapeDataString(name)}&";
}
private static string EncodeSortOptions(SortOptions sortOptions)
{
return Uri.EscapeDataString($"sortField={(int) sortOptions.SortField},isAscending={sortOptions.IsAscending}");
return Uri.EscapeDataString($"{SortFieldKey}{(int) sortOptions.SortField}{InnerStatementSeparator}{IsAscendingKey}{sortOptions.IsAscending}");
}
private static string EncodeFilterStatementDtos(ICollection<FilterStatementDto> statements)
private static string EncodeFilterStatementDtos(ICollection<FilterStatementDto>? statements)
{
if (statements == null || statements.Count == 0)
return string.Empty;
var encodedStatements = StatementsKey + Uri.EscapeDataString(string.Join(",", statements.Select(EncodeFilterStatementDto)));
var encodedStatements = StatementsKey + Uri.EscapeDataString(string.Join(StatementSeparator, statements.Select(EncodeFilterStatementDto)));
return encodedStatements;
}
private static string EncodeFilterStatementDto(FilterStatementDto statement)
{
var encodedComparison = $"comparison={(int) statement.Comparison}";
var encodedField = $"field={(int) statement.Field}";
var encodedValue = $"value={Uri.EscapeDataString(statement.Value)}";
return Uri.EscapeDataString($"{encodedComparison},{encodedField},{encodedValue}");
var encodedComparison = $"{StatementComparisonKey}{(int) statement.Comparison}";
var encodedField = $"{StatementFieldKey}{(int) statement.Field}";
var encodedValue = $"{StatementValueKey}{Uri.EscapeDataString(statement.Value)}";
return Uri.EscapeDataString($"{encodedComparison}{InnerStatementSeparator}{encodedField}{InnerStatementSeparator}{encodedValue}");
}
private static List<FilterStatementDto> DecodeFilterStatementDtos(string encodedStatements)
{
encodedStatements = HttpUtility.UrlDecode(encodedStatements);
string[] statementStrings = encodedStatements.Split(',');
var statementStrings = Uri.UnescapeDataString(encodedStatements).Split(StatementSeparator);
var statements = new List<FilterStatementDto>();
foreach (var statementString in statementStrings)
{
var parts = statementString.Split('&');
var parts = Uri.UnescapeDataString(statementString).Split(InnerStatementSeparator);
if (parts.Length < 3)
continue;
@ -110,7 +120,7 @@ public static class SmartFilterHelper
{
Comparison = Enum.Parse<FilterComparison>(parts[0].Split("=")[1]),
Field = Enum.Parse<FilterField>(parts[1].Split("=")[1]),
Value = HttpUtility.UrlDecode(parts[2].Split("=")[1])
Value = Uri.UnescapeDataString(parts[2].Split("=")[1])
});
}
@ -119,22 +129,22 @@ public static class SmartFilterHelper
private static SortOptions DecodeSortOptions(string encodedSortOptions)
{
string[] parts = encodedSortOptions.Split(',');
var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith("sortField="));
var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith("isAscending="));
var parts = Uri.UnescapeDataString(encodedSortOptions).Split(InnerStatementSeparator);
var sortFieldPart = parts.FirstOrDefault(part => part.StartsWith(SortFieldKey));
var isAscendingPart = parts.FirstOrDefault(part => part.StartsWith(IsAscendingKey));
var isAscending = isAscendingPart?.Substring(11).Equals("true", StringComparison.OrdinalIgnoreCase) ?? false;
if (sortFieldPart != null)
if (sortFieldPart == null)
{
var sortField = Enum.Parse<SortField>(sortFieldPart.Split("=")[1]);
return new SortOptions
{
SortField = sortField,
IsAscending = isAscending
};
return new SortOptions();
}
return null;
var sortField = Enum.Parse<SortField>(sortFieldPart.Split("=")[1]);
return new SortOptions
{
SortField = sortField,
IsAscending = isAscending
};
}
}

View File

@ -9,8 +9,8 @@ using API.Extensions;
using API.Helpers.Builders;
namespace API.Helpers;
#nullable enable
public static class TagHelper
{
/// <summary>

View File

@ -1,4 +1,5 @@
namespace API.Helpers;
#nullable enable
public class UserParams
{
@ -15,7 +16,7 @@ public class UserParams
init => _pageSize = (value == 0) ? MaxPageSize : value;
}
public static readonly UserParams Default = new UserParams()
public static readonly UserParams Default = new()
{
PageSize = 20,
PageNumber = 1

View File

@ -16,6 +16,8 @@ using Microsoft.Extensions.Logging;
namespace API.Services;
#nullable enable
public interface IAccountService
{
Task<IEnumerable<ApiException>> ChangeUserPassword(AppUser user, string newPassword);

View File

@ -18,6 +18,8 @@ using SharpCompress.Common;
namespace API.Services;
#nullable enable
public interface IArchiveService
{
void ExtractArchive(string archivePath, string extractPath);

View File

@ -29,6 +29,7 @@ using VersOne.Epub.Options;
using VersOne.Epub.Schema;
namespace API.Services;
#nullable enable
public interface IBookService

View File

@ -12,6 +12,8 @@ using Microsoft.Extensions.Logging;
namespace API.Services;
#nullable enable
public interface IBookmarkService
{
Task DeleteBookmarkFiles(IEnumerable<AppUserBookmark> bookmarks);

View File

@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace API.Services.HostedServices;
#nullable enable
public class StartupTasksHostedService : IHostedService
{

View File

@ -14,6 +14,7 @@ using Kavita.Common.Helpers;
using Microsoft.Extensions.Logging;
namespace API.Services.Plus;
#nullable enable
/// <summary>
/// Used for matching and fetching metadata on a series

View File

@ -13,6 +13,7 @@ using Kavita.Common.EnvironmentInfo;
using Microsoft.Extensions.Logging;
namespace API.Services.Plus;
#nullable enable
internal class RegisterLicenseResponseDto
{

View File

@ -17,6 +17,7 @@ using Kavita.Common.Helpers;
using Microsoft.Extensions.Logging;
namespace API.Services.Plus;
#nullable enable
public interface IRatingService
{

View File

@ -18,6 +18,7 @@ using Kavita.Common.Helpers;
using Microsoft.Extensions.Logging;
namespace API.Services.Plus;
#nullable enable
public record PlusSeriesDto
{

View File

@ -22,6 +22,7 @@ using Kavita.Common.Helpers;
using Microsoft.Extensions.Logging;
namespace API.Services.Plus;
#nullable enable
/// <summary>
/// Misleading name but is the source of data (like a review coming from AniList)

View File

@ -12,6 +12,7 @@ using Hangfire;
using Microsoft.Extensions.Logging;
namespace API.Services.Tasks;
#nullable enable
public interface IBackupService
{

View File

@ -13,6 +13,7 @@ using Hangfire;
using Microsoft.Extensions.Logging;
namespace API.Services.Tasks;
#nullable enable
public interface ICleanupService
{

View File

@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging;
using VersOne.Epub;
namespace API.Services.Tasks.Metadata;
#nullable enable
public interface IWordCountAnalyzerService
{

Some files were not shown because too many files have changed in this diff Show More