Files
danbooru/app/models/bulk_update_request.rb
evazion d9dc84325f Fix #5365: Don't allow whitespace-only text submission.
Fix bug where it was possible to submit blank text in various text fields.

Caused by `String#blank?` not considering certain Unicode characters as blank. `blank?` is defined
as `match?(/\A[[:space:]]*\z/)`, where `[[:space:]]` matches ASCII spaces (space, tab, newline, etc)
and Unicode characters in the Space category ([1]). However, there are other space-like characters
not in the Space category. This includes U+200B (Zero-Width Space), and many more.

It turns out the "Default ignorable code points" [2][3] are what we're after. These are the set of 400
or so formatting and control characters that are invisible when displayed.

Note that there are other control characters that aren't invisible when rendered, instead they're
shown with a placeholder glyph. These include the ASCII C0 and C1 control codes [4], certain Unicode
control characters [5], and unassigned, reserved, and private use codepoints.

There is one outlier: the Braille pattern blank (U+2800) [6]. This character is visually blank, but is
not considered to be a space or an ignorable code point.

[1]: https://codepoints.net/search?gc[]=Z
[2]: https://codepoints.net/search?DI=1
[3]: https://www.unicode.org/review/pr-5.html
[4]: https://codepoints.net/search?gc[]=Cc
[5]: https://codepoints.net/search?gc[]=Cf
[6]: https://codepoints.net/U+2800
[7]: https://en.wikipedia.org/wiki/Whitespace_character
[8]: https://character.construction/blanks
[9]: https://invisible-characters.com
2022-12-05 01:58:34 -06:00

131 lines
3.8 KiB
Ruby

# frozen_string_literal: true
class BulkUpdateRequest < ApplicationRecord
STATUSES = %w[pending approved rejected processing failed]
attr_accessor :title, :reason
belongs_to :user
belongs_to :forum_topic, optional: true
belongs_to :forum_post, optional: true
belongs_to :approver, optional: true, class_name: "User"
validates :reason, visible_string: true, on: :create
validates :script, visible_string: true
validates :title, visible_string: true, if: ->(rec) { rec.forum_topic_id.blank? }
validates :forum_topic, presence: true, if: ->(rec) { rec.forum_topic_id.present? }
validates :status, inclusion: { in: STATUSES }
validate :validate_script, if: :script_changed?
before_save :update_tags, if: :script_changed?
after_create :create_forum_topic
scope :pending_first, -> { order(Arel.sql("(case status when 'processing' then 0 when 'pending' then 1 when 'approved' then 2 when 'rejected' then 3 when 'failed' then 4 else 5 end)")) }
scope :pending, -> {where(status: "pending")}
scope :approved, -> { where(status: "approved") }
scope :rejected, -> { where(status: "rejected") }
scope :processing, -> { where(status: "processing") }
scope :failed, -> { where(status: "failed") }
scope :has_topic, -> { where.not(forum_topic: nil) }
module SearchMethods
def search(params, current_user)
q = search_attributes(params, [:id, :created_at, :updated_at, :script, :tags, :user, :forum_topic, :forum_post, :approver], current_user: current_user)
if params[:status].present?
q = q.where(status: params[:status].split(","))
end
params[:order] ||= "status_desc"
case params[:order]
when "id_desc"
q = q.order(id: :desc)
when "id_asc"
q = q.order(id: :asc)
when "updated_at_desc"
q = q.order(updated_at: :desc)
when "updated_at_asc"
q = q.order(updated_at: :asc)
else
q = q.apply_default_order(params)
end
q
end
end
module ApprovalMethods
def forum_updater
@forum_updater ||= ForumUpdater.new(forum_topic)
end
def approve!(approver)
transaction do
CurrentUser.scoped(approver) do
processor.validate!(:approval)
update!(status: "processing", approver: approver)
processor.process_later!
forum_updater.update("The #{bulk_update_request_link} (forum ##{forum_post.id}) has been approved by @#{approver.name}.")
end
end
end
def create_forum_topic
CurrentUser.scoped(user) do
body = "[bur:#{id}]\n\n#{reason}"
self.forum_topic = ForumTopic.create(title: title, category_id: 1, creator: user) unless forum_topic.present?
self.forum_post = forum_topic.forum_posts.create(body: body, creator: user) unless forum_post.present?
save
end
end
def reject!(rejector = User.system)
transaction do
update!(status: "rejected")
forum_updater.update("The #{bulk_update_request_link} (forum ##{forum_post.id}) has been rejected by @#{rejector.name}.")
end
end
def bulk_update_request_link
%{"bulk update request ##{id}":#{Routes.bulk_update_requests_path(search: { id: id })}}
end
end
def validate_script
if processor.invalid?(:request)
errors.add(:base, processor.errors.full_messages.join("; "))
end
end
extend SearchMethods
include ApprovalMethods
def update_tags
self.tags = processor.affected_tags
end
def processor
@processor ||= BulkUpdateRequestProcessor.new(self)
end
def is_tag_move_allowed?
processor.is_tag_move_allowed?
end
def is_pending?
status == "pending"
end
def is_approved?
status.in?(%w[approved processing])
end
def is_rejected?
status == "rejected"
end
def self.available_includes
[:user, :forum_topic, :forum_post, :approver]
end
end