diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index af9d36964..237a27bfc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -102,7 +102,7 @@ class ApplicationController < ActionController::Base render_error_page(406, exception, message: "#{request.format} is not a supported format for this page") when PaginationExtension::PaginationError render_error_page(410, exception, template: "static/pagination_error", message: "You cannot go beyond page #{CurrentUser.user.page_limit}.") - when Post::SearchError + when PostQueryBuilder::TagLimitError render_error_page(422, exception, template: "static/tag_limit_error", message: "You cannot search for more than #{CurrentUser.tag_query_limit} tags at a time.") when RateLimiter::RateLimitError render_error_page(429, exception) diff --git a/app/controllers/counts_controller.rb b/app/controllers/counts_controller.rb index 4baf35f2d..a685b698d 100644 --- a/app/controllers/counts_controller.rb +++ b/app/controllers/counts_controller.rb @@ -4,7 +4,7 @@ class CountsController < ApplicationController def posts estimate_count = params.fetch(:estimate_count, "true").truthy? skip_cache = params.fetch(:skip_cache, "false").truthy? - @count = PostQueryBuilder.new(params[:tags], CurrentUser.user).normalized_query.fast_count(timeout: CurrentUser.statement_timeout, estimate_count: estimate_count, skip_cache: skip_cache) + @count = PostQueryBuilder.new(params[:tags], CurrentUser.user, tag_limit: CurrentUser.user.tag_query_limit).normalized_query.fast_count(timeout: CurrentUser.statement_timeout, estimate_count: estimate_count, skip_cache: skip_cache) if request.format.xml? respond_with({ posts: @count }, root: "counts") diff --git a/app/logical/discord_slash_command/count_command.rb b/app/logical/discord_slash_command/count_command.rb index 6211cdcb1..dce71ccf3 100644 --- a/app/logical/discord_slash_command/count_command.rb +++ b/app/logical/discord_slash_command/count_command.rb @@ -19,7 +19,7 @@ class DiscordSlashCommand def call tags = params[:tags] - query = PostQueryBuilder.new(tags, User.anonymous).normalized_query + query = PostQueryBuilder.new(tags, User.anonymous, tag_limit: nil).normalized_query count = query.fast_count(estimate_count: true, skip_cache: true) respond_with("`#{tags}`: #{count} posts") diff --git a/app/logical/discord_slash_command/posts_command.rb b/app/logical/discord_slash_command/posts_command.rb index 0e59f2a89..68a8bd602 100644 --- a/app/logical/discord_slash_command/posts_command.rb +++ b/app/logical/discord_slash_command/posts_command.rb @@ -28,7 +28,7 @@ class DiscordSlashCommand def call tags = params[:tags] limit = params.fetch(:limit, 3).clamp(1, 10) - posts = Post.user_tag_match(tags, User.anonymous).limit(limit) + posts = Post.user_tag_match(tags, User.anonymous, tag_limit: nil).limit(limit) respond_with(posts: posts) end diff --git a/app/logical/discord_slash_command/random_command.rb b/app/logical/discord_slash_command/random_command.rb index 888e8ad15..ea8537a39 100644 --- a/app/logical/discord_slash_command/random_command.rb +++ b/app/logical/discord_slash_command/random_command.rb @@ -26,7 +26,7 @@ class DiscordSlashCommand def call tags = params[:tags] limit = params.fetch(:limit, 1).clamp(1, 10) - posts = Post.user_tag_match(tags, User.anonymous).random(limit) + posts = Post.user_tag_match(tags, User.anonymous, tag_limit: nil).random(limit) respond_with(posts: posts) end diff --git a/app/logical/post_query_builder.rb b/app/logical/post_query_builder.rb index 33c37ff07..b6f750d7d 100644 --- a/app/logical/post_query_builder.rb +++ b/app/logical/post_query_builder.rb @@ -3,6 +3,8 @@ require "strscan" class PostQueryBuilder extend Memoist + class TagLimitError < StandardError; end + # How many tags a `blah*` search should match. MAX_WILDCARD_TAGS = 100 @@ -58,13 +60,14 @@ class PostQueryBuilder UNLIMITED_METATAGS = %w[status rating limit] - attr_reader :query_string, :current_user, :safe_mode, :hide_deleted_posts + attr_reader :query_string, :current_user, :tag_limit, :safe_mode, :hide_deleted_posts alias_method :safe_mode?, :safe_mode alias_method :hide_deleted_posts?, :hide_deleted_posts - def initialize(query_string, current_user = User.anonymous, safe_mode: false, hide_deleted_posts: false) + def initialize(query_string, current_user = User.anonymous, tag_limit: nil, safe_mode: false, hide_deleted_posts: false) @query_string = query_string @current_user = current_user + @tag_limit = tag_limit @safe_mode = safe_mode @hide_deleted_posts = hide_deleted_posts end @@ -468,15 +471,8 @@ class PostQueryBuilder relation end - def self.is_unlimited_tag?(term) - term.type == :metatag && term.name.in?(UNLIMITED_METATAGS) - end - def build - tag_count = terms.count { |term| !PostQueryBuilder.is_unlimited_tag?(term) } - if tag_count > current_user.tag_query_limit - raise ::Post::SearchError - end + validate! relation = Post.all relation = add_joins(relation) @@ -642,6 +638,18 @@ class PostQueryBuilder relation.find_ordered(ids) end + def validate! + tag_count = terms.count { |term| !is_unlimited_tag?(term) } + + if tag_limit.present? && tag_count > tag_limit + raise TagLimitError + end + end + + def is_unlimited_tag?(term) + term.type == :metatag && term.name.in?(UNLIMITED_METATAGS) + end + concerning :ParseMethods do def scan_query terms = [] diff --git a/app/logical/post_sets/post.rb b/app/logical/post_sets/post.rb index adf4eafb0..2515c400f 100644 --- a/app/logical/post_sets/post.rb +++ b/app/logical/post_sets/post.rb @@ -6,7 +6,7 @@ module PostSets attr_reader :page, :random, :post_count, :format, :tag_string, :query, :normalized_query def initialize(tags, page = 1, per_page = nil, user: CurrentUser.user, random: false, format: "html") - @query = PostQueryBuilder.new(tags, user, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: user.hide_deleted_posts?) + @query = PostQueryBuilder.new(tags, user, tag_limit: user.tag_query_limit, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: user.hide_deleted_posts?) @normalized_query = query.normalized_query @tag_string = tags @page = page diff --git a/app/models/post.rb b/app/models/post.rb index 21ed66ec3..cf47c9b0e 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1265,11 +1265,11 @@ class Post < ApplicationRecord end def system_tag_match(query) - user_tag_match(query, User.system, safe_mode: false, hide_deleted_posts: false) + user_tag_match(query, User.system, tag_limit: nil, safe_mode: false, hide_deleted_posts: false) end - def user_tag_match(query, user = CurrentUser.user, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: user.hide_deleted_posts?) - post_query = PostQueryBuilder.new(query, user, safe_mode: safe_mode, hide_deleted_posts: hide_deleted_posts) + def user_tag_match(query, user = CurrentUser.user, tag_limit: user.tag_query_limit, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: user.hide_deleted_posts?) + post_query = PostQueryBuilder.new(query, user, tag_limit: tag_limit, safe_mode: safe_mode, hide_deleted_posts: hide_deleted_posts) post_query.normalized_query.build end diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb index af523de69..be21a855c 100644 --- a/test/unit/post_query_builder_test.rb +++ b/test/unit/post_query_builder_test.rb @@ -1045,7 +1045,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase should "fail for more than 6 tags" do post1 = create(:post, rating: "s") - assert_raise(::Post::SearchError) do + assert_raise(PostQueryBuilder::TagLimitError) do Post.user_tag_match("a b c rating:s width:10 height:10 user:bob") end end diff --git a/test/unit/post_sets/post_test.rb b/test/unit/post_sets/post_test.rb index 77f88800a..b1bb73a60 100644 --- a/test/unit/post_sets/post_test.rb +++ b/test/unit/post_sets/post_test.rb @@ -82,7 +82,7 @@ module PostSets should "fail" do @set = PostSets::Post.new("a b c", user: create(:user)) - assert_raises(::Post::SearchError) do + assert_raises(PostQueryBuilder::TagLimitError) do @set.posts end end