From b580125e86ff25b5bbac06f6888afbdaa798ddc6 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Tue, 15 Jun 2021 10:33:02 -0500 Subject: [PATCH] fix searching when queries include non-ascii characters (#271) --- CHANGELOG.md | 1 + ErsatzTV/Extensions/StringExtensions.cs | 58 ++++++++++++++++++++++ ErsatzTV/Pages/Artist.razor | 17 ++++--- ErsatzTV/Pages/ArtistList.razor | 19 +++---- ErsatzTV/Pages/EpisodeList.razor | 19 +++---- ErsatzTV/Pages/Movie.razor | 31 ++++++------ ErsatzTV/Pages/MovieList.razor | 19 +++---- ErsatzTV/Pages/MusicVideoList.razor | 19 +++---- ErsatzTV/Pages/Search.razor | 44 ++++++++-------- ErsatzTV/Pages/TelevisionEpisodeList.razor | 8 +-- ErsatzTV/Pages/TelevisionSeasonList.razor | 23 +++++---- ErsatzTV/Pages/TelevisionShowList.razor | 19 +++---- ErsatzTV/Shared/LetterBar.razor | 4 +- ErsatzTV/Shared/MainLayout.razor | 18 ++----- 14 files changed, 159 insertions(+), 140 deletions(-) create mode 100644 ErsatzTV/Extensions/StringExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index c5237786c..571015387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix blazor error font color - Fix some audio stream languages missing from UI and search index - Fix audio stream selection for languages with multiple codes +- Fix searching when queries contain non-ascii characters ## [0.0.46-prealpha] - 2021-06-14 ### Added diff --git a/ErsatzTV/Extensions/StringExtensions.cs b/ErsatzTV/Extensions/StringExtensions.cs new file mode 100644 index 000000000..359c08e36 --- /dev/null +++ b/ErsatzTV/Extensions/StringExtensions.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; + +namespace ErsatzTV.Extensions +{ + public static class StringExtensions + { + public static string GetSearchQuery(this string uri) + { + try + { + string query = new Uri(uri).Query; + Dictionary parsed = QueryHelpers.ParseQuery(query); + if (parsed.TryGetValue("query", out StringValues value)) + { + return value; + } + + if (parsed.TryGetValue("b64query", out StringValues base64Value)) + { + return base64Value.DecodeBase64(); + } + } + catch (Exception) + { + // do nothing + } + + return string.Empty; + } + + public static string GetRelativeSearchQuery(this string query) + { + (string key, string value) = EncodeQuery(query); + return $"/search?{key}={value}"; + } + + private static string DecodeBase64(this StringValues input) => + Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(input)); + + public static EncodedQueryResult EncodeQuery(this string query) + { + string encoded = Uri.EscapeDataString(query); + + // TODO: remove this on dotnet 6 + // see https://github.com/dotnet/aspnetcore/pull/26769 + var fakeAbsolute = $"https://whatever.com/test?query={encoded}"; + return Uri.IsWellFormedUriString(fakeAbsolute, UriKind.Absolute) + ? new EncodedQueryResult("query", encoded) + : new EncodedQueryResult("b64query", WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(query))); + } + + public record EncodedQueryResult(string Key, string Value); + } +} diff --git a/ErsatzTV/Pages/Artist.razor b/ErsatzTV/Pages/Artist.razor index aa37e6e03..349fa1819 100644 --- a/ErsatzTV/Pages/Artist.razor +++ b/ErsatzTV/Pages/Artist.razor @@ -8,6 +8,7 @@ @using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules.Commands @using System.Globalization +@using ErsatzTV.Extensions @using Unit = LanguageExt.Unit @inject IMediator _mediator @inject IDialogService _dialog @@ -73,11 +74,11 @@ {
Languages:  - @_sortedLanguages.Head().EnglishName + @_sortedLanguages.Head().EnglishName @foreach (CultureInfo language in _sortedLanguages.Skip(1)) { - @language.EnglishName + @language.EnglishName }
} @@ -85,11 +86,11 @@ {
Genres:  - @_sortedGenres.Head() + @_sortedGenres.Head() @foreach (string genre in _sortedGenres.Skip(1)) { - @genre + @genre }
} @@ -97,11 +98,11 @@ {
Styles:  - @_sortedStyles.Head() + @_sortedStyles.Head() @foreach (string style in _sortedStyles.Skip(1)) { - @style + @style }
} @@ -109,11 +110,11 @@ {
Moods:  - @_sortedMoods.Head() + @_sortedMoods.Head() @foreach (string mood in _sortedMoods.Skip(1)) { - @mood + @mood }
} diff --git a/ErsatzTV/Pages/ArtistList.razor b/ErsatzTV/Pages/ArtistList.razor index ed8909923..1fa9ecdee 100644 --- a/ErsatzTV/Pages/ArtistList.razor +++ b/ErsatzTV/Pages/ArtistList.razor @@ -2,11 +2,11 @@ @page "/media/music/artists/page/{PageNumber:int}" @using LanguageExt.UnsafeValueAccess @using Microsoft.AspNetCore.WebUtilities -@using Microsoft.Extensions.Primitives @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.Search.Queries +@using ErsatzTV.Extensions @using Unit = LanguageExt.Unit @inherits MultiSelectBase @inject NavigationManager _navigationManager @@ -90,16 +90,7 @@ PageNumber = 1; } - string query = new Uri(_navigationManager.Uri).Query; - if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) - { - _query = value; - } - else - { - _query = null; - } - + _query = _navigationManager.Uri.GetSearchQuery(); return RefreshData(); } @@ -114,7 +105,8 @@ var uri = $"/media/music/artists/page/{PageNumber - 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } @@ -124,7 +116,8 @@ var uri = $"/media/music/artists/page/{PageNumber + 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } diff --git a/ErsatzTV/Pages/EpisodeList.razor b/ErsatzTV/Pages/EpisodeList.razor index 1b1afc7e9..ae128c008 100644 --- a/ErsatzTV/Pages/EpisodeList.razor +++ b/ErsatzTV/Pages/EpisodeList.razor @@ -2,11 +2,11 @@ @page "/media/tv/episodes/page/{PageNumber:int}" @using LanguageExt.UnsafeValueAccess @using Microsoft.AspNetCore.WebUtilities -@using Microsoft.Extensions.Primitives @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.Search.Queries +@using ErsatzTV.Extensions @using Unit = LanguageExt.Unit @inherits MultiSelectBase @inject NavigationManager _navigationManager @@ -90,16 +90,7 @@ PageNumber = 1; } - string query = new Uri(_navigationManager.Uri).Query; - if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) - { - _query = value; - } - else - { - _query = null; - } - + _query = _navigationManager.Uri.GetSearchQuery(); return RefreshData(); } @@ -114,7 +105,8 @@ var uri = $"/media/tv/episodes/page/{PageNumber - 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } @@ -124,7 +116,8 @@ var uri = $"/media/tv/episodes/page/{PageNumber + 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } diff --git a/ErsatzTV/Pages/Movie.razor b/ErsatzTV/Pages/Movie.razor index 35c8f6b05..d7c38b3f7 100644 --- a/ErsatzTV/Pages/Movie.razor +++ b/ErsatzTV/Pages/Movie.razor @@ -5,6 +5,7 @@ @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands +@using ErsatzTV.Extensions @inject IMediator _mediator @inject IDialogService _dialog @inject NavigationManager _navigationManager @@ -67,11 +68,11 @@ {
Content Ratings:  - @_sortedContentRatings.Head() + @_sortedContentRatings.Head() @foreach (string contentRating in _sortedContentRatings.Skip(1)) { - @contentRating + @contentRating }
} @@ -79,11 +80,11 @@ {
Languages:  - @_sortedLanguages.Head().EnglishName + @_sortedLanguages.Head().EnglishName @foreach (CultureInfo language in _sortedLanguages.Skip(1)) { - @language.EnglishName + @language.EnglishName }
} @@ -91,11 +92,11 @@ {
Studios:  - @_sortedStudios.Head() + @_sortedStudios.Head() @foreach (string studio in _sortedStudios.Skip(1)) { - @studio + @studio }
} @@ -103,11 +104,11 @@ {
Directors:  - @_sortedDirectors.Head() + @_sortedDirectors.Head() @foreach (string director in _sortedDirectors.Skip(1)) { - @director + @director }
} @@ -115,11 +116,11 @@ {
Writers:  - @_sortedWriters.Head() + @_sortedWriters.Head() @foreach (string writer in _sortedWriters.Skip(1)) { - @writer + @writer }
} @@ -127,11 +128,11 @@ {
Genres:  - @_sortedGenres.Head() + @_sortedGenres.Head() @foreach (string genre in _sortedGenres.Skip(1)) { - @genre + @genre }
} @@ -139,11 +140,11 @@ {
Tags:  - @_sortedTags.Head() + @_sortedTags.Head() @foreach (string tag in _sortedTags.Skip(1)) { - @tag + @tag }
} @@ -159,7 +160,7 @@ @foreach (ActorCardViewModel actor in _movie.Actors) { } diff --git a/ErsatzTV/Pages/MovieList.razor b/ErsatzTV/Pages/MovieList.razor index 9b4d8a105..a4d69652b 100644 --- a/ErsatzTV/Pages/MovieList.razor +++ b/ErsatzTV/Pages/MovieList.razor @@ -2,11 +2,11 @@ @page "/media/movies/page/{PageNumber:int}" @using LanguageExt.UnsafeValueAccess @using Microsoft.AspNetCore.WebUtilities -@using Microsoft.Extensions.Primitives @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.Search.Queries +@using ErsatzTV.Extensions @using Unit = LanguageExt.Unit @inherits MultiSelectBase @inject NavigationManager _navigationManager @@ -89,16 +89,7 @@ PageNumber = 1; } - string query = new Uri(_navigationManager.Uri).Query; - if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) - { - _query = value; - } - else - { - _query = null; - } - + _query = _navigationManager.Uri.GetSearchQuery(); return RefreshData(); } @@ -113,7 +104,8 @@ var uri = $"/media/movies/page/{PageNumber - 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } @@ -123,7 +115,8 @@ var uri = $"/media/movies/page/{PageNumber + 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } diff --git a/ErsatzTV/Pages/MusicVideoList.razor b/ErsatzTV/Pages/MusicVideoList.razor index 8ccfb9030..0810ff970 100644 --- a/ErsatzTV/Pages/MusicVideoList.razor +++ b/ErsatzTV/Pages/MusicVideoList.razor @@ -2,11 +2,11 @@ @page "/media/music/videos/page/{PageNumber:int}" @using LanguageExt.UnsafeValueAccess @using Microsoft.AspNetCore.WebUtilities -@using Microsoft.Extensions.Primitives @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.Search.Queries +@using ErsatzTV.Extensions @using Unit = LanguageExt.Unit @inherits MultiSelectBase @inject NavigationManager _navigationManager @@ -90,16 +90,7 @@ PageNumber = 1; } - string query = new Uri(_navigationManager.Uri).Query; - if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) - { - _query = value; - } - else - { - _query = null; - } - + _query = _navigationManager.Uri.GetSearchQuery(); return RefreshData(); } @@ -114,7 +105,8 @@ var uri = $"/media/music/videos/page/{PageNumber - 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } @@ -124,7 +116,8 @@ var uri = $"/media/music/videos/page/{PageNumber + 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } diff --git a/ErsatzTV/Pages/Search.razor b/ErsatzTV/Pages/Search.razor index 861ca00a1..f983a68a1 100644 --- a/ErsatzTV/Pages/Search.razor +++ b/ErsatzTV/Pages/Search.razor @@ -4,8 +4,8 @@ @using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.Search @using ErsatzTV.Application.Search.Queries +@using ErsatzTV.Extensions @using Microsoft.AspNetCore.WebUtilities -@using Microsoft.Extensions.Primitives @using Unit = LanguageExt.Unit @inherits MultiSelectBase @inject NavigationManager _navigationManager @@ -35,27 +35,27 @@ else { @_query - if (_movies.Count > 0) + if (_movies?.Count > 0) { @_movies.Count Movies } - if (_shows.Count > 0) + if (_shows?.Count > 0) { @_shows.Count Shows } - if (_episodes.Count > 0) + if (_episodes?.Count > 0) { @_episodes.Count Episodes } - if (_artists.Count > 0) + if (_artists?.Count > 0) { @_artists.Count Artists } - if (_musicVideos.Count > 0) + if (_musicVideos?.Count > 0) { @_musicVideos.Count Music Videos } @@ -71,7 +71,7 @@ - @if (_movies.Count > 0) + @if (_movies?.Count > 0) {
} - @if (_shows.Count > 0) + @if (_shows?.Count > 0) {
} - @if (_episodes.Count > 0) + @if (_episodes?.Count > 0) {
} - @if (_artists.Count > 0) + @if (_artists?.Count > 0) {
} - @if (_musicVideos.Count > 0) + @if (_musicVideos?.Count > 0) {
Directors:  - @sorted.Head() + @sorted.Head() @foreach (string director in sorted.Skip(1)) { - @director + @director }
} @@ -130,11 +130,11 @@
Writers:  - @sorted.Head() + @sorted.Head() @foreach (string writer in sorted.Skip(1)) { - @writer + @writer }
} diff --git a/ErsatzTV/Pages/TelevisionSeasonList.razor b/ErsatzTV/Pages/TelevisionSeasonList.razor index 0ba146c91..dab619e11 100644 --- a/ErsatzTV/Pages/TelevisionSeasonList.razor +++ b/ErsatzTV/Pages/TelevisionSeasonList.razor @@ -8,6 +8,7 @@ @using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.ProgramSchedules @using ErsatzTV.Application.ProgramSchedules.Commands +@using ErsatzTV.Extensions @using Unit = LanguageExt.Unit @inject IMediator _mediator @inject ILogger _logger @@ -81,11 +82,11 @@ {
Content Ratings:  - @_sortedContentRatings.Head() + @_sortedContentRatings.Head() @foreach (string contentRating in _sortedContentRatings.Skip(1)) { - @contentRating + @contentRating }
} @@ -93,11 +94,11 @@ {
Languages:  - @_sortedLanguages.Head().EnglishName + @_sortedLanguages.Head().EnglishName @foreach (CultureInfo language in _sortedLanguages.Skip(1)) { - @language.EnglishName + @language.EnglishName }
} @@ -105,11 +106,11 @@ {
Studios:  - @_sortedStudios.Head() + @_sortedStudios.Head() @foreach (string studio in _sortedStudios.Skip(1)) { - @studio + @studio }
} @@ -117,11 +118,11 @@ {
Genres:  - @_sortedGenres.Head() + @_sortedGenres.Head() @foreach (string genre in _sortedGenres.Skip(1)) { - @genre + @genre }
} @@ -129,11 +130,11 @@ {
Tags:  - @_sortedTags.Head() + @_sortedTags.Head() @foreach (string tag in _sortedTags.Skip(1)) { - @tag + @tag }
} @@ -160,7 +161,7 @@ @foreach (ActorCardViewModel actor in _show.Actors) { } diff --git a/ErsatzTV/Pages/TelevisionShowList.razor b/ErsatzTV/Pages/TelevisionShowList.razor index 8fe5ddc02..0c91051cc 100644 --- a/ErsatzTV/Pages/TelevisionShowList.razor +++ b/ErsatzTV/Pages/TelevisionShowList.razor @@ -2,11 +2,11 @@ @page "/media/tv/shows/page/{PageNumber:int}" @using LanguageExt.UnsafeValueAccess @using Microsoft.AspNetCore.WebUtilities -@using Microsoft.Extensions.Primitives @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCollections @using ErsatzTV.Application.MediaCollections.Commands @using ErsatzTV.Application.Search.Queries +@using ErsatzTV.Extensions @using Unit = LanguageExt.Unit @inherits MultiSelectBase @inject NavigationManager _navigationManager @@ -89,16 +89,7 @@ PageNumber = 1; } - string query = new Uri(_navigationManager.Uri).Query; - if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) - { - _query = value; - } - else - { - _query = null; - } - + _query = _navigationManager.Uri.GetSearchQuery(); return RefreshData(); } @@ -113,7 +104,8 @@ var uri = $"/media/tv/shows/page/{PageNumber - 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } @@ -123,7 +115,8 @@ var uri = $"/media/tv/shows/page/{PageNumber + 1}"; if (!string.IsNullOrWhiteSpace(_query)) { - uri = QueryHelpers.AddQueryString(uri, "query", _query); + (string key, string value) = _query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } _navigationManager.NavigateTo(uri); } diff --git a/ErsatzTV/Shared/LetterBar.razor b/ErsatzTV/Shared/LetterBar.razor index d5b5a4a9d..235374804 100644 --- a/ErsatzTV/Shared/LetterBar.razor +++ b/ErsatzTV/Shared/LetterBar.razor @@ -1,4 +1,5 @@ @using ErsatzTV.Core.Search +@using ErsatzTV.Extensions @using Microsoft.AspNetCore.WebUtilities
@@ -48,7 +49,8 @@ var uri = $"{BaseUri}/page/{PageMap.PageMap[letter]}"; if (!string.IsNullOrWhiteSpace(Query)) { - uri = QueryHelpers.AddQueryString(uri, "query", Query); + (string key, string value) = Query.EncodeQuery(); + uri = $"{uri}?{key}={value}"; } return uri + $"#letter-{letter}"; } diff --git a/ErsatzTV/Shared/MainLayout.razor b/ErsatzTV/Shared/MainLayout.razor index 10bf56d53..1587c2548 100644 --- a/ErsatzTV/Shared/MainLayout.razor +++ b/ErsatzTV/Shared/MainLayout.razor @@ -1,7 +1,5 @@ @using System.Reflection -@using Microsoft.AspNetCore.WebUtilities -@using Microsoft.Extensions.Primitives -@using System.Web +@using ErsatzTV.Extensions @inherits LayoutComponentBase @inject NavigationManager _navigationManager @@ -107,22 +105,12 @@ protected override async Task OnParametersSetAsync() { await base.OnParametersSetAsync(); - - string query = new Uri(_navigationManager.Uri).Query; - if (QueryHelpers.ParseQuery(query).TryGetValue("query", out StringValues value)) - { - _query = value; - } - else - { - _query = string.Empty; - } + _query = _navigationManager.Uri.GetSearchQuery(); } private void PerformSearch() { - string query = HttpUtility.UrlEncode(_query); - _navigationManager.NavigateTo($"/search?query={query}", true); + _navigationManager.NavigateTo(_query.GetRelativeSearchQuery(), true); StateHasChanged(); }