posts/index: fix rating:s being included in page title in safe mode.

Fixes bug described in d3e4ac7c17 (commitcomment-39049351)

When dealing with searches, there are several variables we have to keep
in mind:

* Whether tag aliases should be applied.
* Whether search terms should be sorted.
* Whether the rating:s and -status:deleted metatags should be added by
  safe mode and the hide deleted posts setting.

Which of these things we need to do depends on the context:

* We want to apply aliases when actually doing the search, calculating
  the count, looking up the wiki excerpt, recording missed/popular
  searches in Reportbooru, and calculating related tags for the sidebar,
  but not when displaying the raw search as typed by the user (for
  example, in the page title or in the tag search box).
* We want to sort the search when calculating cache keys for fast_count
  or related tags, and when recording missed/popular searches, but not
  in the page title or when displaying the raw search.
* We want to add rating:s and -status:deleted when performing the
  search, calculating the count, or recording missed/popular searches,
  but not when calculating related tags for the sidebar, or when
  displaying the page title or raw search.

Here we introduce normalized_query and try to use it in contexts where
query normalization is necessary. When to use the normalized query
versus the raw unnormalized query is still subtle and prone to error.
This commit is contained in:
evazion
2020-05-12 21:08:52 -05:00
parent ea400296d4
commit ad02e0f62c
11 changed files with 91 additions and 69 deletions

View File

@@ -4,6 +4,6 @@ class CountsController < ApplicationController
def posts def posts
estimate_count = params.fetch(:estimate_count, "true").truthy? estimate_count = params.fetch(:estimate_count, "true").truthy?
skip_cache = params.fetch(:skip_cache, "false").truthy? skip_cache = params.fetch(:skip_cache, "false").truthy?
@count = PostQueryBuilder.new(params[:tags], CurrentUser.user).fast_count(timeout: CurrentUser.statement_timeout, estimate_count: estimate_count, skip_cache: skip_cache) @count = PostQueryBuilder.new(params[:tags], CurrentUser.user).normalized_query.fast_count(timeout: CurrentUser.statement_timeout, estimate_count: estimate_count, skip_cache: skip_cache)
end end
end end

View File

@@ -53,22 +53,15 @@ class PostQueryBuilder
COUNT_METATAG_SYNONYMS.flat_map { |str| [str, "#{str}_asc"] } + COUNT_METATAG_SYNONYMS.flat_map { |str| [str, "#{str}_asc"] } +
CATEGORY_COUNT_METATAGS.flat_map { |str| [str, "#{str}_asc"] } CATEGORY_COUNT_METATAGS.flat_map { |str| [str, "#{str}_asc"] }
attr_reader :query_string, :terms, :current_user, :safe_mode, :hide_deleted_posts attr_reader :query_string, :current_user, :safe_mode, :hide_deleted_posts
alias_method :safe_mode?, :safe_mode alias_method :safe_mode?, :safe_mode
alias_method :hide_deleted_posts?, :hide_deleted_posts alias_method :hide_deleted_posts?, :hide_deleted_posts
def initialize(query_string, current_user = User.anonymous, normalize_aliases: true, normalize_order: true, safe_mode: false, hide_deleted_posts: false) def initialize(query_string, current_user = User.anonymous, safe_mode: false, hide_deleted_posts: false)
@query_string = query_string @query_string = query_string
@current_user = current_user @current_user = current_user
@safe_mode = safe_mode @safe_mode = safe_mode
@hide_deleted_posts = hide_deleted_posts @hide_deleted_posts = hide_deleted_posts
@terms = scan_query
@terms << OpenStruct.new(type: :metatag, name: "rating", value: "s") if safe_mode?
@terms << OpenStruct.new(type: :metatag, name: "status", value: "deleted", negated: true) if hide_deleted?
normalize_aliases! if normalize_aliases
normalize_order! if normalize_order
end end
def tags_match(tags, relation) def tags_match(tags, relation)
@@ -457,11 +450,6 @@ class PostQueryBuilder
relation relation
end end
def hide_deleted?
has_status_metatag = select_metatags(:status).any? { |metatag| metatag.value.downcase.in?(%w[deleted active any all]) }
hide_deleted_posts? && !has_status_metatag
end
def build def build
tag_count = terms.count { |term| !Danbooru.config.is_unlimited_tag?(term) } tag_count = terms.count { |term| !Danbooru.config.is_unlimited_tag?(term) }
if tag_count > Danbooru.config.tag_query_limit if tag_count > Danbooru.config.tag_query_limit
@@ -695,20 +683,6 @@ class PostQueryBuilder
end end
end end
def normalize_aliases!
tag_names = tags.map(&:name)
tag_aliases = tag_names.zip(TagAlias.to_aliased(tag_names)).to_h
terms.map! do |term|
term.name = tag_aliases[term.name] if term.type == :tag
term
end
end
def normalize_order!
terms.sort_by!(&:to_s).uniq!
end
def parse_tag_edit def parse_tag_edit
split_query split_query
end end
@@ -884,11 +858,51 @@ class PostQueryBuilder
end end
end end
concerning :NormalizationMethods do
def normalized_query(implicit: true, sort: true)
post_query = dup
post_query.terms.concat(implicit_metatags) if implicit
post_query.normalize_aliases!
post_query.normalize_order! if sort
post_query
end
def normalize_aliases!
tag_names = tags.map(&:name)
tag_aliases = tag_names.zip(TagAlias.to_aliased(tag_names)).to_h
terms.map! do |term|
term.name = tag_aliases[term.name] if term.type == :tag
term
end
end
def normalize_order!
terms.sort_by!(&:to_s).uniq!
end
def implicit_metatags
metatags = []
metatags << OpenStruct.new(type: :metatag, name: "rating", value: "s") if safe_mode?
metatags << OpenStruct.new(type: :metatag, name: "status", value: "deleted", negated: true) if hide_deleted?
metatags
end
def hide_deleted?
has_status_metatag = select_metatags(:status).any? { |metatag| metatag.value.downcase.in?(%w[deleted active any all]) }
hide_deleted_posts? && !has_status_metatag
end
end
concerning :UtilityMethods do concerning :UtilityMethods do
def to_s def to_s
split_query.join(" ") split_query.join(" ")
end end
def terms
@terms ||= scan_query
end
def tags def tags
terms.select { |term| term.type == :tag } terms.select { |term| term.type == :tag }
end end
@@ -943,5 +957,5 @@ class PostQueryBuilder
end end
end end
memoize :split_query memoize :split_query, :normalized_query
end end

View File

@@ -4,6 +4,7 @@ module PostSets
MAX_SIDEBAR_TAGS = 25 MAX_SIDEBAR_TAGS = 25
attr_reader :page, :random, :post_count, :format, :tag_string, :query attr_reader :page, :random, :post_count, :format, :tag_string, :query
delegate :normalized_query, to: :query
def initialize(tags, page = 1, per_page = nil, random: false, format: "html") def initialize(tags, page = 1, per_page = nil, random: false, format: "html")
@query = PostQueryBuilder.new(tags, CurrentUser.user, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: CurrentUser.hide_deleted_posts?) @query = PostQueryBuilder.new(tags, CurrentUser.user, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: CurrentUser.hide_deleted_posts?)
@@ -29,8 +30,8 @@ module PostSets
end end
def tag def tag
return nil unless query.has_single_tag? return nil unless normalized_query.has_single_tag?
@tag ||= Tag.find_by(name: query.tags.first.name) @tag ||= Tag.find_by(name: normalized_query.tags.first.name)
end end
def artist def artist
@@ -40,7 +41,7 @@ module PostSets
end end
def pool def pool
pool_names = query.select_metatags(:pool, :ordpool).map(&:value) pool_names = normalized_query.select_metatags(:pool, :ordpool).map(&:value)
name = pool_names.first name = pool_names.first
return nil unless pool_names.size == 1 return nil unless pool_names.size == 1
@@ -48,7 +49,7 @@ module PostSets
end end
def favgroup def favgroup
favgroup_names = query.select_metatags(:favgroup, :ordfavgroup).map(&:value) favgroup_names = normalized_query.select_metatags(:favgroup, :ordfavgroup).map(&:value)
name = favgroup_names.first name = favgroup_names.first
return nil unless favgroup_names.size == 1 return nil unless favgroup_names.size == 1
@@ -88,7 +89,7 @@ module PostSets
# no need to get counts for formats that don't use a paginator # no need to get counts for formats that don't use a paginator
nil nil
else else
query.fast_count normalized_query.fast_count
end end
end end
@@ -105,7 +106,7 @@ module PostSets
if is_random? if is_random?
temp = get_random_posts temp = get_random_posts
else else
temp = query.build.paginate(page, count: post_count, search_count: !post_count.nil?, limit: per_page) temp = normalized_query.build.paginate(page, count: post_count, search_count: !post_count.nil?, limit: per_page)
end end
end end
end end
@@ -176,7 +177,7 @@ module PostSets
end end
def similar_tags def similar_tags
RelatedTagCalculator.cached_similar_tags_for_search(query, MAX_SIDEBAR_TAGS) RelatedTagCalculator.cached_similar_tags_for_search(normalized_query(implicit: false), MAX_SIDEBAR_TAGS)
end end
def frequent_tags def frequent_tags

View File

@@ -6,7 +6,7 @@ class RelatedTagQuery
def initialize(query:, user: User.anonymous, category: nil, type: nil, limit: nil) def initialize(query:, user: User.anonymous, category: nil, type: nil, limit: nil)
@user = user @user = user
@post_query = PostQueryBuilder.new(query, user) @post_query = PostQueryBuilder.new(query, user).normalized_query
@query = @post_query.to_s @query = @post_query.to_s
@category = category @category = category
@type = type @type = type

View File

@@ -604,7 +604,7 @@ class Post < ApplicationRecord
# If someone else committed changes to this post before we did, # If someone else committed changes to this post before we did,
# then try to merge the tag changes together. # then try to merge the tag changes together.
current_tags = tag_string_was.split current_tags = tag_string_was.split
new_tags = PostQueryBuilder.new(tag_string, normalize_aliases: false).parse_tag_edit new_tags = PostQueryBuilder.new(tag_string).parse_tag_edit
old_tags = old_tag_string.split old_tags = old_tag_string.split
kept_tags = current_tags & new_tags kept_tags = current_tags & new_tags
@@ -642,7 +642,7 @@ class Post < ApplicationRecord
end end
def normalize_tags def normalize_tags
normalized_tags = PostQueryBuilder.new(tag_string, normalize_aliases: false).parse_tag_edit normalized_tags = PostQueryBuilder.new(tag_string).parse_tag_edit
normalized_tags = apply_casesensitive_metatags(normalized_tags) normalized_tags = apply_casesensitive_metatags(normalized_tags)
normalized_tags = normalized_tags.map(&:downcase) normalized_tags = normalized_tags.map(&:downcase)
normalized_tags = filter_metatags(normalized_tags) normalized_tags = filter_metatags(normalized_tags)
@@ -1475,7 +1475,8 @@ class Post < ApplicationRecord
end end
def user_tag_match(query, user = CurrentUser.user, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: user.hide_deleted_posts?) def user_tag_match(query, user = CurrentUser.user, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: user.hide_deleted_posts?)
PostQueryBuilder.new(query, user, safe_mode: safe_mode, hide_deleted_posts: hide_deleted_posts).build post_query = PostQueryBuilder.new(query, user, safe_mode: safe_mode, hide_deleted_posts: hide_deleted_posts)
post_query.normalized_query.build
end end
def search(params) def search(params)

View File

@@ -140,11 +140,11 @@ class SavedSearch < ApplicationRecord
end end
def normalized_query def normalized_query
PostQueryBuilder.new(query).to_s PostQueryBuilder.new(query).normalized_query.to_s
end end
def normalize_query def normalize_query
self.query = PostQueryBuilder.new(query, normalize_order: false).to_s self.query = PostQueryBuilder.new(query).normalized_query(sort: false).to_s
end end
end end

View File

@@ -237,7 +237,7 @@ class Upload < ApplicationRecord
include SourceMethods include SourceMethods
def assign_rating_from_tags def assign_rating_from_tags
if rating = PostQueryBuilder.new(tag_string, normalize_aliases: false).find_metatag(:rating) if rating = PostQueryBuilder.new(tag_string).find_metatag(:rating)
self.rating = rating.downcase.first self.rating = rating.downcase.first
end end
end end

View File

@@ -163,7 +163,7 @@ class PostPresenter
end end
def has_sequential_navigation?(params) def has_sequential_navigation?(params)
return false if PostQueryBuilder.new(params[:q], normalize_aliases: false).has_metatag?(:order, :ordfav, :ordpool) return false if PostQueryBuilder.new(params[:q]).has_metatag?(:order, :ordfav, :ordpool)
return false if params[:pool_id].present? || params[:favgroup_id].present? return false if params[:pool_id].present? || params[:favgroup_id].present?
return CurrentUser.user.enable_sequential_post_navigation return CurrentUser.user.enable_sequential_post_navigation
end end

View File

@@ -76,7 +76,7 @@ class UserPresenter
end end
def commented_posts_count(template) def commented_posts_count(template)
count = PostQueryBuilder.new("commenter:#{user.name}", User.system).fast_count count = PostQueryBuilder.new("commenter:#{user.name}").fast_count
count = "?" if count.nil? count = "?" if count.nil?
template.link_to(count, template.posts_path(:tags => "commenter:#{user.name} order:comment_bumped")) template.link_to(count, template.posts_path(:tags => "commenter:#{user.name} order:comment_bumped"))
end end
@@ -90,7 +90,7 @@ class UserPresenter
end end
def noted_posts_count(template) def noted_posts_count(template)
count = PostQueryBuilder.new("noteupdater:#{user.name}", User.system).fast_count count = PostQueryBuilder.new("noteupdater:#{user.name}").fast_count
count = "?" if count.nil? count = "?" if count.nil?
template.link_to(count, template.posts_path(:tags => "noteupdater:#{user.name} order:note")) template.link_to(count, template.posts_path(:tags => "noteupdater:#{user.name} order:note"))
end end

View File

@@ -305,6 +305,13 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
assert_select "#post_#{@post.id}", 1 assert_select "#post_#{@post.id}", 1
end end
end end
context "in safe mode" do
should "not include the rating:s tag in the page title" do
get posts_path(tags: "1girl", safe_mode: true)
assert_select "title", text: "1girl Art | Safebooru"
end
end
end end
context "show_seq action" do context "show_seq action" do

View File

@@ -6,7 +6,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
end end
def assert_fast_count(count, query, query_options = {}, fast_count_options = {}) def assert_fast_count(count, query, query_options = {}, fast_count_options = {})
assert_equal(count, PostQueryBuilder.new(query, **query_options).fast_count(**fast_count_options)) assert_equal(count, PostQueryBuilder.new(query, **query_options).normalized_query.fast_count(**fast_count_options))
end end
setup do setup do
@@ -1018,7 +1018,6 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
context "Parsing:" do context "Parsing:" do
should "split a query" do should "split a query" do
assert_equal(%w(aaa bbb), PostQueryBuilder.new("aaa bbb").split_query) assert_equal(%w(aaa bbb), PostQueryBuilder.new("aaa bbb").split_query)
assert_equal(%w(~aaa -bbb*), PostQueryBuilder.new("~AAa -BBB* -bbb*").split_query)
end end
should "not strip out valid characters when scanning" do should "not strip out valid characters when scanning" do
@@ -1050,22 +1049,22 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
end end
end end
context "The normalize_query method" do context "The normalized_query method" do
should "work" do should "work" do
create(:tag_alias, antecedent_name: "gray", consequent_name: "grey") create(:tag_alias, antecedent_name: "gray", consequent_name: "grey")
assert_equal("foo", PostQueryBuilder.new("foo").to_s) assert_equal("foo", PostQueryBuilder.new("foo").normalized_query.to_s)
assert_equal("foo", PostQueryBuilder.new(" foo ").to_s) assert_equal("foo", PostQueryBuilder.new(" foo ").normalized_query.to_s)
assert_equal("foo", PostQueryBuilder.new("FOO").to_s) assert_equal("foo", PostQueryBuilder.new("FOO").normalized_query.to_s)
assert_equal("foo", PostQueryBuilder.new("foo foo").to_s) assert_equal("foo", PostQueryBuilder.new("foo foo").normalized_query.to_s)
assert_equal("grey", PostQueryBuilder.new("gray").to_s) assert_equal("grey", PostQueryBuilder.new("gray").normalized_query.to_s)
assert_equal("aaa bbb", PostQueryBuilder.new("bbb aaa").to_s) assert_equal("aaa bbb", PostQueryBuilder.new("bbb aaa").normalized_query.to_s)
assert_equal("-aaa bbb", PostQueryBuilder.new("bbb -aaa").to_s) assert_equal("-aaa bbb", PostQueryBuilder.new("bbb -aaa").normalized_query.to_s)
assert_equal("~aaa ~bbb", PostQueryBuilder.new("~bbb ~aaa").to_s) assert_equal("~aaa ~bbb", PostQueryBuilder.new("~bbb ~aaa").normalized_query.to_s)
assert_equal("commentary:true bbb", PostQueryBuilder.new("bbb commentary:true").to_s) assert_equal("commentary:true bbb", PostQueryBuilder.new("bbb commentary:true").normalized_query.to_s)
assert_equal('commentary:"true" bbb', PostQueryBuilder.new("bbb commentary:'true'").to_s) assert_equal('commentary:"true" bbb', PostQueryBuilder.new("bbb commentary:'true'").normalized_query.to_s)
assert_equal('-commentary:true bbb', PostQueryBuilder.new("bbb -commentary:true").to_s) assert_equal('-commentary:true bbb', PostQueryBuilder.new("bbb -commentary:true").normalized_query.to_s)
assert_equal('-commentary:"true" bbb', PostQueryBuilder.new("bbb -commentary:'true'").to_s) assert_equal('-commentary:"true" bbb', PostQueryBuilder.new("bbb -commentary:'true'").normalized_query.to_s)
end end
end end
@@ -1114,7 +1113,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
should "set the expiration time" do should "set the expiration time" do
Cache.expects(:put).with(PostQueryBuilder.new("score:42 aaa").count_cache_key, 1, 180) Cache.expects(:put).with(PostQueryBuilder.new("score:42 aaa").count_cache_key, 1, 180)
PostQueryBuilder.new("aaa score:42").fast_count assert_fast_count(1, "aaa score:42")
end end
should "work with the hide_deleted_posts option turned on" do should "work with the hide_deleted_posts option turned on" do
@@ -1126,8 +1125,8 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
context "a blank search" do context "a blank search" do
should "should execute a search" do should "should execute a search" do
assert_equal(1, PostQueryBuilder.new("").fast_count(estimate_count: false)) assert_fast_count(1, "", {}, { estimate_count: false })
assert_nothing_raised { PostQueryBuilder.new("").fast_count(estimate_count: true) } assert_nothing_raised { PostQueryBuilder.new("").normalized_query.fast_count(estimate_count: true) }
end end
should "return 0 for a nonexisting tag" do should "return 0 for a nonexisting tag" do
@@ -1136,13 +1135,13 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
context "in safe mode" do context "in safe mode" do
should "work for a blank search" do should "work for a blank search" do
assert_equal(2, PostQueryBuilder.new("").fast_count(estimate_count: false)) assert_fast_count(0, "", { safe_mode: true }, { estimate_count: false })
assert_nothing_raised { PostQueryBuilder.new("").fast_count(estimate_count: true) } assert_nothing_raised { PostQueryBuilder.new("", safe_mode: true).normalized_query.fast_count(estimate_count: true) }
end end
should "work for a nil search" do should "work for a nil search" do
assert_equal(2, PostQueryBuilder.new(nil).fast_count(estimate_count: false)) assert_fast_count(0, nil, { safe_mode: true }, { estimate_count: false })
assert_nothing_raised { PostQueryBuilder.new(nil).fast_count(estimate_count: true) } assert_nothing_raised { PostQueryBuilder.new("", safe_mode: true).normalized_query.fast_count(estimate_count: true) }
end end
should "not fail for a two tag search by a member" do should "not fail for a two tag search by a member" do