[15 min fix] Enforce an upper limit for articles.body_markdown (#13418)

* Enforce a length limit for articles.body_markdown

* Fix spec assertion

* Add bytesize validation to body_markdown
This commit is contained in:
rhymes 2021-04-16 20:20:11 +02:00 committed by GitHub
parent 9fe7be190b
commit 05c220619f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 55 additions and 3 deletions

View file

@ -54,7 +54,9 @@ class Article < ApplicationRecord
inverse_of: :commentable,
class_name: "Comment"
validates :body_markdown, length: { minimum: 0, allow_nil: false }, uniqueness: { scope: %i[user_id title] }
validates :body_markdown, bytesize: { maximum: 800.kilobytes, too_long: "is too long." }
validates :body_markdown, length: { minimum: 0, allow_nil: false }
validates :body_markdown, uniqueness: { scope: %i[user_id title] }
validates :boost_states, presence: true
validates :cached_tag_list, length: { maximum: 126 }
validates :canonical_url, uniqueness: { allow_nil: true }

View file

@ -0,0 +1,35 @@
# Adapted from https://github.com/rails/rails/issues/19570#issuecomment-348366536
# and https://github.com/rails/rails/blob/v6.1.3.1/activemodel/lib/active_model/validations/length.rb
# Currently only supports `:maximum`
class BytesizeValidator < ActiveModel::EachValidator
MESSAGES = { maximum: :too_long }.freeze
CHECKS = { maximum: :<= }.freeze
RESERVED_OPTIONS = %i[maximum too_long].freeze
ERROR_MISSING_OPTIONS_MESSAGE = "Specify the :maximum option.".freeze
ERROR_NON_NEGATIVE_MESSAGE = ":maximum must be a non-negative Integer".freeze
def check_validity!
raise ArgumentError, ERROR_MESSAGE unless options.key?(:maximum)
maximum = options[:maximum]
raise ArgumentError, ERROR_NON_NEGATIVE_MESSAGE unless maximum.is_a?(Integer) && maximum >= 0
end
def validate_each(record, attribute, value)
key = :maximum
value_bytesize = value.respond_to?(:bytesize) ? value.bytesize : value.to_s.bytesize
errors_options = options.except(*RESERVED_OPTIONS)
check_value = options[key]
return if value_bytesize.public_send(CHECKS[key], check_value)
errors_options[:count] = check_value
default_message = options[MESSAGES[key]]
errors_options[:message] ||= default_message if default_message
record.errors.add(attribute, MESSAGES[key], **errors_options)
end
end

View file

@ -32,6 +32,7 @@ RSpec.describe Article, type: :model do
it { is_expected.to have_many(:tags) }
it { is_expected.to have_many(:user_subscriptions).dependent(:nullify) }
it { is_expected.to validate_length_of(:body_markdown).is_at_least(0) }
it { is_expected.to validate_length_of(:cached_tag_list).is_at_most(126) }
it { is_expected.to validate_length_of(:title).is_at_most(128) }
@ -81,11 +82,25 @@ RSpec.describe Article, type: :model do
end
describe "#body_markdown" do
it "is unique scoped for user_id and title" do
it "is unique scoped for user_id and title", :aggregate_failures do
art2 = build(:article, body_markdown: article.body_markdown, user: article.user, title: article.title)
expect(art2).not_to be_valid
expect(art2.errors.full_messages.to_sentence).to match("markdown has already been taken")
expect(art2.errors_as_sentence).to match("markdown has already been taken")
end
# using https://unicode-table.com/en/11A15/ multibyte char
it "is valid if its bytesize is less than 800 kilobytes" do
article.body_markdown = "𑨕" * 204_800 # 4 bytes x 204800 = 800 kilobytes
expect(article).to be_valid
end
it "is not valid if its bytesize exceeds 800 kilobytes" do
article.body_markdown = "𑨕" * 204_801
expect(article).not_to be_valid
expect(article.errors_as_sentence).to match("too long")
end
end