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

134 lines
3.4 KiB
Ruby

# frozen_string_literal: true
class Note < ApplicationRecord
class RevertError < StandardError; end
attr_accessor :html_id
belongs_to :post
has_many :versions, -> {order("note_versions.id ASC")}, :class_name => "NoteVersion", :dependent => :destroy
validates :x, presence: true
validates :y, presence: true
validates :width, presence: true
validates :height, presence: true
validates :body, visible_string: true
validate :note_within_image
after_save :update_post
after_save :create_version
scope :active, -> { where(is_active: true) }
module SearchMethods
def search(params, current_user)
q = search_attributes(params, [:id, :created_at, :updated_at, :is_active, :x, :y, :width, :height, :body, :version, :post], current_user: current_user)
q.apply_default_order(params)
end
end
extend SearchMethods
def note_within_image
return false unless post.present?
if x < 0 || y < 0 || (x > post.image_width) || (y > post.image_height) || width < 0 || height < 0 || (x + width > post.image_width) || (y + height > post.image_height)
errors.add(:note, "must be inside the image")
end
end
def rescale!(x_scale, y_scale)
self.x *= x_scale
self.y *= y_scale
self.width *= x_scale
self.height *= y_scale
save!
end
def update_post
if saved_changes?
if post.notes.active.exists?
post.update_columns(last_noted_at: updated_at)
else
post.update_columns(last_noted_at: nil)
end
end
end
def create_version(updater: CurrentUser.user)
return unless saved_change_to_versioned_attributes?
if merge_version?(updater.id)
merge_version
else
Note.where(:id => id).update_all("version = coalesce(version, 0) + 1")
reload
create_new_version(updater.id)
end
end
def saved_change_to_versioned_attributes?
new_record? || saved_change_to_x? || saved_change_to_y? || saved_change_to_width? || saved_change_to_height? || saved_change_to_is_active? || saved_change_to_body?
end
def create_new_version(updater_id)
versions.create(
:updater_id => updater_id,
:post_id => post_id,
:x => x,
:y => y,
:width => width,
:height => height,
:is_active => is_active,
:body => body,
:version => version
)
end
def merge_version
prev = versions.last
prev.update(x: x, y: y, width: width, height: height, is_active: is_active, body: body)
end
def merge_version?(updater_id)
prev = versions.last
prev && prev.updater_id == updater_id && prev.updated_at > 1.hour.ago
end
def revert_to(version)
if id != version.note_id
raise RevertError, "You cannot revert to a previous version of another note."
end
self.x = version.x
self.y = version.y
self.post_id = version.post_id
self.body = version.body
self.width = version.width
self.height = version.height
self.is_active = version.is_active
end
def revert_to!(version)
revert_to(version)
save!
end
def copy_to(new_post)
new_note = dup
new_note.post_id = new_post.id
new_note.version = 0
width_ratio = new_post.image_width.to_f / post.image_width
height_ratio = new_post.image_height.to_f / post.image_height
new_note.x = x * width_ratio
new_note.y = y * height_ratio
new_note.width = width * width_ratio
new_note.height = height * height_ratio
new_note.save
end
def self.available_includes
[:post]
end
end