From 2c174829247e9b40bf7ef3972ffa9d89053706dd Mon Sep 17 00:00:00 2001 From: Kieran Eglin Date: Thu, 23 May 2024 14:52:40 -0700 Subject: [PATCH] Fix bug with special chars in search form --- lib/pinchflat/media/media_query.ex | 29 +++++++++++++++++-- .../source_html/media_item_table_live.ex | 2 +- ...nge_media_items_search_index_tokenizer.exs | 8 ++--- test/pinchflat/media_test.exs | 11 +++++++ 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/lib/pinchflat/media/media_query.ex b/lib/pinchflat/media/media_query.ex index 1412ab8..95e3c67 100644 --- a/lib/pinchflat/media/media_query.ex +++ b/lib/pinchflat/media/media_query.ex @@ -128,9 +128,11 @@ defmodule Pinchflat.Media.MediaQuery do def matches_search_term(nil), do: dynamic([mi], true) def matches_search_term(term) do - case String.trim(term) do + escaped_term = clean_search_term(term) + + case String.trim(escaped_term) do "" -> dynamic([mi], true) - term -> dynamic([mi], fragment("media_items_search_index MATCH ?", ^term)) + _ -> dynamic([mi], fragment("media_items_search_index MATCH ?", ^escaped_term)) end end @@ -161,9 +163,11 @@ defmodule Pinchflat.Media.MediaQuery do def matching_search_term(query, nil), do: query def matching_search_term(query, term) do + escaped_term = clean_search_term(term) + from(mi in query, join: mi_search_index in assoc(mi, :media_items_search_index), - where: fragment("media_items_search_index MATCH ?", ^term), + where: fragment("media_items_search_index MATCH ?", ^escaped_term), select_merge: %{ matching_search_term: fragment(""" @@ -175,4 +179,23 @@ defmodule Pinchflat.Media.MediaQuery do order_by: [desc: fragment("rank")] ) end + + # SQLite's FTS5 is very picky about what it will accept as a search term. + # To that end, we need to clean up the search term before passing it to the + # MATCH clause. + # This method: + # - Trims leading and trailing whitespace + # - Collapses multiple spaces into a single space + # - Removes quote characters + # - Wraps any word in quotes (must happen after the double quote replacement) + # + # This allows for works with apostrophes and quotes to be searched for correctly + defp clean_search_term(term) do + term + |> String.trim() + |> String.replace(~r/\s+/, " ") + |> String.split(~r/\s+/) + |> Enum.map(fn str -> String.replace(str, ~s("), "") end) + |> Enum.map_join(" ", fn str -> ~s("#{str}") end) + end end diff --git a/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex b/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex index 3890ffa..d6e3d2a 100644 --- a/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex +++ b/lib/pinchflat_web/controllers/sources/source_html/media_item_table_live.ex @@ -72,7 +72,7 @@ defmodule Pinchflat.Sources.MediaItemTableLive do media_state = session["media_state"] source = Sources.get_source!(session["source_id"]) base_query = generate_base_query(source, media_state) - pagination_attrs = fetch_pagination_attributes(base_query, page, "friendly") + pagination_attrs = fetch_pagination_attributes(base_query, page, nil) new_assigns = Map.merge( diff --git a/priv/repo/migrations/20240523183003_change_media_items_search_index_tokenizer.exs b/priv/repo/migrations/20240523183003_change_media_items_search_index_tokenizer.exs index b944683..b254a5d 100644 --- a/priv/repo/migrations/20240523183003_change_media_items_search_index_tokenizer.exs +++ b/priv/repo/migrations/20240523183003_change_media_items_search_index_tokenizer.exs @@ -3,10 +3,10 @@ defmodule Pinchflat.Repo.Migrations.ChangeMediaItemsSearchIndexTokenizer do def up do # These all need to run as part of separate `execute` blocks. Do NOT ask me why. - execute "DROP TRIGGER media_items_search_index_insert;" - execute "DROP TRIGGER media_items_search_index_update;" - execute "DROP TRIGGER media_items_search_index_delete;" - execute "DROP TABLE media_items_search_index;" + execute "DROP TRIGGER IF EXISTS media_items_search_index_insert;" + execute "DROP TRIGGER IF EXISTS media_items_search_index_update;" + execute "DROP TRIGGER IF EXISTS media_items_search_index_delete;" + execute "DROP TABLE IF EXISTS media_items_search_index;" execute """ CREATE VIRTUAL TABLE media_items_search_index USING fts5( diff --git a/test/pinchflat/media_test.exs b/test/pinchflat/media_test.exs index f98afb1..515b7f9 100644 --- a/test/pinchflat/media_test.exs +++ b/test/pinchflat/media_test.exs @@ -727,6 +727,17 @@ defmodule Pinchflat.MediaTest do test "returns an empty list when the search term is nil" do assert [] = Media.search(nil) end + + test "doesn't blow up if there's an apostrophe or quotes in the search term" do + assert [] = Media.search("don't expl'ode") + assert [] = Media.search(~s(dont expl"o"de)) + assert [] = Media.search(~s(dont explo"de)) + end + + test "doesn't blow up if there is a trailing operand" do + assert [] = Media.search("foo OR") + assert [] = Media.search("foo AND") + end end describe "get_media_item!/1" do