Fix an exploit that let you determine the flagger of a post using `flagger:<username>` saved searches. Saved searches were performed as DanbooruBot, but since DanbooruBot is a moderator, it let unprivileged users do `flagger:<username>` searches. Saved searches were done as a moderator to avoid tag limits, but this is no longer necessary since the last PostQueryBuilder refactor. fred get out
186 lines
4.8 KiB
Ruby
186 lines
4.8 KiB
Ruby
class SavedSearch < ApplicationRecord
|
|
REDIS_EXPIRY = 1.hour
|
|
QUERY_LIMIT = 1000
|
|
|
|
attr_reader :disable_labels
|
|
|
|
belongs_to :user
|
|
|
|
normalize :query, :normalize_query
|
|
normalize :labels, :normalize_labels
|
|
|
|
validates :query, presence: true
|
|
validate :validate_count, on: :create
|
|
|
|
scope :labeled, ->(label) { where_array_includes_any_lower(:labels, [normalize_label(label)]) }
|
|
scope :has_tag, ->(name) { where_regex(:query, "(^| )[~-]?#{Regexp.escape(name)}( |$)", flags: "i") }
|
|
|
|
def self.visible(user)
|
|
where(user: user)
|
|
end
|
|
|
|
concerning :Redis do
|
|
extend Memoist
|
|
|
|
class_methods do
|
|
extend Memoist
|
|
|
|
def redis
|
|
return nil if Danbooru.config.redis_url.blank?
|
|
::Redis.new(url: Danbooru.config.redis_url)
|
|
end
|
|
memoize :redis
|
|
|
|
def post_ids_for(user_id, label: nil)
|
|
return [] if redis.nil?
|
|
|
|
queries = queries_for(user_id, label: label)
|
|
post_ids = Set.new
|
|
queries.each do |query|
|
|
redis_key = "search:#{query}"
|
|
if redis.exists?(redis_key)
|
|
sub_ids = redis.smembers(redis_key).map(&:to_i)
|
|
post_ids.merge(sub_ids)
|
|
else
|
|
PopulateSavedSearchJob.perform_later(query)
|
|
end
|
|
end
|
|
post_ids.to_a.sort.last(QUERY_LIMIT)
|
|
end
|
|
end
|
|
|
|
def refreshed_at
|
|
return Time.zone.now if SavedSearch.redis.nil?
|
|
|
|
ttl = SavedSearch.redis.ttl("search:#{normalized_query}")
|
|
return nil if ttl < 0
|
|
(REDIS_EXPIRY.to_i - ttl).seconds.ago
|
|
end
|
|
memoize :refreshed_at
|
|
|
|
def cached_size
|
|
return 0 if SavedSearch.redis.nil?
|
|
|
|
SavedSearch.redis.scard("search:#{normalized_query}")
|
|
end
|
|
memoize :cached_size
|
|
end
|
|
|
|
concerning :Labels do
|
|
class_methods do
|
|
def normalize_labels(labels)
|
|
# XXX should sort and uniq, but will break some use cases.
|
|
labels.map { |label| normalize_label(label) }.reject(&:blank?)
|
|
end
|
|
|
|
def normalize_label(label)
|
|
label.to_s.unicode_normalize(:nfc).normalize_whitespace.downcase.gsub(/[[:space:]]+/, "_").squeeze("_").gsub(/\A_|_\z/, "")
|
|
end
|
|
|
|
def all_labels
|
|
select(Arel.sql("distinct unnest(labels) as label")).order(:label)
|
|
end
|
|
|
|
def labels_like(label)
|
|
all_labels.select { |ss| ss.label.ilike?(label) }.map(&:label)
|
|
end
|
|
|
|
def labels_for(user_id)
|
|
SavedSearch
|
|
.where(user_id: user_id)
|
|
.order("label")
|
|
.pluck(Arel.sql("distinct unnest(labels) as label"))
|
|
end
|
|
end
|
|
|
|
def label_string
|
|
labels.join(" ")
|
|
end
|
|
|
|
def label_string=(val)
|
|
self.labels = val.to_s.split(/[[:space:]]+/)
|
|
end
|
|
end
|
|
|
|
concerning :Search do
|
|
class_methods do
|
|
def search(params)
|
|
q = search_attributes(params, :id, :created_at, :updated_at, :query)
|
|
|
|
if params[:label]
|
|
q = q.labeled(params[:label])
|
|
end
|
|
|
|
case params[:order]
|
|
when "query"
|
|
q = q.order(:query).order(id: :desc)
|
|
when "label"
|
|
q = q.order(:labels).order(id: :desc)
|
|
else
|
|
q = q.apply_default_order(params)
|
|
end
|
|
|
|
q
|
|
end
|
|
|
|
def populate(query, timeout: 10_000)
|
|
return if redis.nil?
|
|
|
|
redis_key = "search:#{query}"
|
|
return if redis.exists?(redis_key)
|
|
|
|
post_ids = Post.with_timeout(timeout, [], query: query) do
|
|
Post.anon_tag_match(query).limit(QUERY_LIMIT).pluck(:id)
|
|
end
|
|
|
|
if post_ids.present?
|
|
redis.sadd(redis_key, post_ids)
|
|
redis.expire(redis_key, REDIS_EXPIRY.to_i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :Queries do
|
|
class_methods do
|
|
def normalize_query(query)
|
|
PostQueryBuilder.new(query.to_s).normalized_query(sort: false).to_s
|
|
end
|
|
|
|
def queries_for(user_id, label: nil)
|
|
searches = SavedSearch.where(user_id: user_id)
|
|
searches = searches.labeled(label) if label.present?
|
|
queries = searches.map(&:normalized_query)
|
|
queries.sort.uniq
|
|
end
|
|
|
|
def rewrite_queries!(old_name, new_name)
|
|
has_tag(old_name).find_each do |ss|
|
|
ss.lock!
|
|
ss.rewrite_query(old_name, new_name)
|
|
ss.save!
|
|
end
|
|
end
|
|
end
|
|
|
|
def normalized_query
|
|
@normalized_query ||= PostQueryBuilder.new(query).normalized_query.to_s
|
|
end
|
|
|
|
def rewrite_query(old_name, new_name)
|
|
query.gsub!(/(?:\A| )([-~])?#{Regexp.escape(old_name)}(?: |\z)/i) { " #{$1}#{new_name} " }
|
|
query.strip!
|
|
end
|
|
end
|
|
|
|
def validate_count
|
|
if user.saved_searches.count >= user.max_saved_searches
|
|
errors.add(:user, "can only have up to #{user.max_saved_searches} " + "saved search".pluralize(user.max_saved_searches))
|
|
end
|
|
end
|
|
|
|
def disable_labels=(value)
|
|
user.update(disable_categorized_saved_searches: true) if value.to_s.truthy?
|
|
end
|
|
end
|