Files
danbooru/app/models/post_flag.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

106 lines
3.2 KiB
Ruby

# frozen_string_literal: true
class PostFlag < ApplicationRecord
module Reasons
UNAPPROVED = "Unapproved in three days"
REJECTED = "Unapproved in three days after returning to moderation queue%"
end
belongs_to :creator, class_name: "User"
belongs_to :post
before_validation { post.lock! }
validates :reason, visible_string: true, length: { in: 1..140 }
validate :validate_creator_is_not_limited, on: :create
validate :validate_post, on: :create
validates :creator_id, uniqueness: { scope: :post_id, on: :create, unless: :is_deletion, message: "have already flagged this post" }
after_create :update_post
after_create :prune_disapprovals
attr_accessor :is_deletion
enum status: {
pending: 0,
succeeded: 1,
rejected: 2,
}
scope :by_users, -> { where.not(creator: User.system) }
scope :by_system, -> { where(creator: User.system) }
scope :in_cooldown, -> { by_users.where("created_at >= ?", Danbooru.config.moderation_period.ago) }
scope :expired, -> { pending.where("post_flags.created_at < ?", Danbooru.config.moderation_period.ago) }
scope :active, -> { pending.or(rejected.in_cooldown) }
module SearchMethods
def category_matches(category)
case category
when "normal"
where("reason NOT IN (?) AND reason NOT LIKE ?", [Reasons::UNAPPROVED], Reasons::REJECTED)
when "unapproved"
where(reason: Reasons::UNAPPROVED)
when "rejected"
where("reason LIKE ?", Reasons::REJECTED)
when "deleted"
where("reason = ? OR reason LIKE ?", Reasons::UNAPPROVED, Reasons::REJECTED)
else
none
end
end
def search(params, current_user)
q = search_attributes(params, [:id, :created_at, :updated_at, :reason, :status, :post, :creator], current_user: current_user)
if params[:category]
q = q.category_matches(params[:category])
end
q.apply_default_order(params)
end
end
extend SearchMethods
def category
case reason
when Reasons::UNAPPROVED
:unapproved
when /#{Reasons::REJECTED.gsub("%", ".*")}/
:rejected
else
:normal
end
end
def prune_disapprovals
return if is_deletion
PostDisapproval.where(post: post).delete_all
end
def update_post
post.update_column(:is_flagged, true) if pending?
end
def validate_creator_is_not_limited
errors.add(:creator, "have reached your flag limit") if creator.is_flag_limited? && !is_deletion
end
def validate_post
errors.add(:post, "is pending and cannot be flagged") if post.is_pending? && !is_deletion
errors.add(:post, "is deleted and cannot be flagged") if post.is_deleted? && creator != User.system # DanbooruBot is allowed to prune expired appeals
errors.add(:post, "is already flagged") if post.is_flagged? && !is_deletion
errors.add(:post, "cannot be flagged") if !post.visible?(creator)
flag = post.flags.in_cooldown.last
if !is_deletion && !creator.is_approver? && flag.present?
errors.add(:post, "cannot be flagged more than once every #{Danbooru.config.moderation_period.inspect} (last flagged: #{flag.created_at.to_formatted_s(:long)})")
end
end
def uploader_id
post.uploader_id
end
def self.available_includes
[:post]
end
end