* Fix it so non-moderators can't search deleted comments using the `updater`, `body`, `score`, `do_not_bump_post`, or `is_sticky` fields. Searching for these fields will exclude deleted comments. * Fix it so non-moderators can search for their own deleted comments using the `creator` field, but not for deleted comments belonging to other users. * Fix it so that if a regular user searches `commenter:<username>`, they can only see posts with undeleted comments by that user. If a moderator or the commenter themselves searches `commenter:<username>`, they can see all posts the user has commented on, including posts with deleted comments. * Fix it so the comment count on user profiles only counts visible comments. Regular users can only see the number of undeleted comments a user has, while moderators and the commenter themselves can see the total number of comments. Known issue: * It's still possible to order deleted comments by score, which can let you infer the score of deleted comments.
248 lines
7.7 KiB
Ruby
248 lines
7.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ApplicationRecord < ActiveRecord::Base
|
|
self.abstract_class = true
|
|
|
|
include Deletable
|
|
include Mentionable
|
|
include Normalizable
|
|
include ArrayAttribute
|
|
include HasDtextLinks
|
|
extend HasBitFlags
|
|
extend Searchable
|
|
|
|
concerning :PaginationMethods do
|
|
class_methods do
|
|
def paginate(*args, **options)
|
|
extending(PaginationExtension).paginate(*args, **options)
|
|
end
|
|
|
|
# Perform a search using the model's `search` method, then paginate the results.
|
|
#
|
|
# @param params [Hash] The URL request params from the user
|
|
# @param page [Integer] The page number
|
|
# @param limit [Integer] The number of posts per page
|
|
# @param count_pages [Boolean] If true, calculate the exact number of pages of
|
|
# results. If false (the default), don't calculate the exact number of pages
|
|
# of results; assume there are too many pages to count.
|
|
# @param count [Integer] the precalculated number of search results, or nil to calculate it
|
|
# @param defaults [Hash] The default params for the search
|
|
# @param current_user [User] The user performing the search
|
|
def paginated_search(params, page: params[:page], limit: params[:limit], count_pages: params[:search].present?, count: nil, defaults: {}, current_user: CurrentUser.user)
|
|
search_params = params.fetch(:search, {}).permit!
|
|
search_params = defaults.merge(search_params).with_indifferent_access
|
|
|
|
max_limit = (params[:format] == "sitemap") ? 10_000 : 1_000
|
|
search(search_params, current_user).paginate(page, limit: limit, max_limit: max_limit, count: count, search_count: count_pages)
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :PrivilegeMethods do
|
|
class_methods do
|
|
def visible(_user)
|
|
all
|
|
end
|
|
|
|
def visible_for_search(attribute, current_user)
|
|
policy(current_user).visible_for_search(all, attribute)
|
|
end
|
|
|
|
def policy(current_user)
|
|
Pundit.policy(current_user, self)
|
|
end
|
|
end
|
|
|
|
def policy(current_user)
|
|
Pundit.policy(current_user, self)
|
|
end
|
|
end
|
|
|
|
concerning :ApiMethods do
|
|
class_methods do
|
|
def available_includes
|
|
[]
|
|
end
|
|
|
|
def multiple_includes
|
|
reflections.select { |_, v| v.macro == :has_many }.keys.map(&:to_sym)
|
|
end
|
|
|
|
def associated_models(name)
|
|
if reflections[name].options[:polymorphic]
|
|
reflections[name].active_record.try(:model_types) || []
|
|
else
|
|
[reflections[name].class_name]
|
|
end
|
|
end
|
|
end
|
|
|
|
def available_includes
|
|
self.class.available_includes
|
|
end
|
|
|
|
# XXX deprecated, shouldn't expose this as an instance method.
|
|
def api_attributes(user: CurrentUser.user)
|
|
policy = Pundit.policy(user, self) || ApplicationPolicy.new(user, self)
|
|
policy.api_attributes
|
|
end
|
|
|
|
# XXX deprecated, shouldn't expose this as an instance method.
|
|
def html_data_attributes(user: CurrentUser.user)
|
|
policy = Pundit.policy(user, self) || ApplicationPolicy.new(user, self)
|
|
policy.html_data_attributes
|
|
end
|
|
|
|
def serializable_hash(options = {})
|
|
options ||= {}
|
|
if options[:only].is_a?(String)
|
|
options.delete(:methods)
|
|
options.delete(:include)
|
|
options.merge!(ParameterBuilder.serial_parameters(options[:only], self))
|
|
else
|
|
options[:methods] ||= []
|
|
attributes, methods = api_attributes.partition { |attr| has_attribute?(attr) }
|
|
methods += options[:methods]
|
|
options[:only] ||= attributes + methods
|
|
|
|
attributes &= options[:only]
|
|
methods &= options[:only]
|
|
|
|
options[:only] = attributes
|
|
options[:methods] = methods
|
|
|
|
options.delete(:methods) if options[:methods].empty?
|
|
end
|
|
|
|
hash = super(options)
|
|
hash.transform_keys { |key| key.delete("?") }
|
|
end
|
|
end
|
|
|
|
concerning :SearchMethods do
|
|
class_methods do
|
|
def model_restriction(table)
|
|
table.project(1)
|
|
end
|
|
|
|
def attribute_restriction(*)
|
|
all
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :ActiveRecordExtensions do
|
|
class_methods do
|
|
def without_timeout
|
|
connection.execute("SET STATEMENT_TIMEOUT = 0") unless Rails.env.test?
|
|
yield
|
|
ensure
|
|
connection.execute("SET STATEMENT_TIMEOUT = #{CurrentUser.user.try(:statement_timeout) || 3_000}") unless Rails.env.test?
|
|
end
|
|
|
|
def with_timeout(n, default_value = nil, new_relic_params = {})
|
|
connection.execute("SET STATEMENT_TIMEOUT = #{n}") unless Rails.env.test?
|
|
yield
|
|
rescue ::ActiveRecord::StatementInvalid => e
|
|
DanbooruLogger.log(e, expected: true, **new_relic_params)
|
|
default_value
|
|
ensure
|
|
connection.execute("SET STATEMENT_TIMEOUT = #{CurrentUser.user.try(:statement_timeout) || 3_000}") unless Rails.env.test?
|
|
end
|
|
|
|
def update!(*args)
|
|
all.each { |record| record.update!(*args) }
|
|
end
|
|
|
|
def each_duplicate(*columns)
|
|
return enum_for(:each_duplicate, *columns) unless block_given?
|
|
|
|
group(columns).having("count(*) > 1").count.each do |values, count|
|
|
hash = columns.zip(Array.wrap(values)).to_h
|
|
yield count: count, **hash
|
|
end
|
|
end
|
|
|
|
def destroy_duplicates!(*columns, log: true)
|
|
each_duplicate(*columns) do |count:, **columns_with_values|
|
|
records = where(columns_with_values).order(:id)
|
|
dupes = records.drop(1)
|
|
|
|
if log
|
|
data = { keep: records.first.id, destroy: dupes.map(&:id), count: count, **columns_with_values }
|
|
DanbooruLogger.info("Destroying duplicate #{self.name} #{dupes.map(&:id).join(", ")}", data)
|
|
end
|
|
|
|
dupes.each(&:destroy!)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Save the record, but convert RecordNotUnique exceptions thrown by the database into
|
|
# Rails validation errors. This way duplicate records only return one type of error.
|
|
# This assumes the table only has one uniqueness constraint in the database.
|
|
def save_if_unique(column)
|
|
save
|
|
rescue ActiveRecord::RecordNotUnique => e
|
|
self.errors.add(column, :taken)
|
|
false
|
|
end
|
|
end
|
|
|
|
concerning :UserMethods do
|
|
class_methods do
|
|
def belongs_to_updater(**options)
|
|
class_eval do
|
|
belongs_to :updater, class_name: "User", **options
|
|
before_validation do |rec|
|
|
rec.updater_id = CurrentUser.id
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
concerning :DtextMethods do
|
|
def dtext_shortlink(**_options)
|
|
"#{self.class.name.underscore.tr("_", " ")} ##{id}"
|
|
end
|
|
end
|
|
|
|
concerning :ConcurrencyMethods do
|
|
class_methods do
|
|
def parallel_each(batch_size: 1000, in_processes: 4, in_threads: nil, &block)
|
|
# XXX We may deadlock if a transaction is open; do a non-parallel each.
|
|
return find_each(&block) if connection.transaction_open?
|
|
|
|
# XXX Use threads in testing because processes can't see each other's
|
|
# database transactions.
|
|
if Rails.env.test?
|
|
in_processes = nil
|
|
in_threads = 2
|
|
end
|
|
|
|
current_user = CurrentUser.user
|
|
|
|
find_in_batches(batch_size: batch_size, error_on_ignore: true) do |batch|
|
|
Parallel.each(batch, in_processes: in_processes, in_threads: in_threads) do |record|
|
|
# XXX In threaded mode, the current user isn't inherited from the
|
|
# parent thread because the current user is a thread-local
|
|
# variable. Hence, we have to set it explicitly in the child thread.
|
|
CurrentUser.scoped(current_user) do
|
|
yield record
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def revised?
|
|
updated_at > created_at
|
|
end
|
|
|
|
def warnings
|
|
@warnings ||= ActiveModel::Errors.new(self)
|
|
end
|
|
end
|