Files
nairobi/app/models/concerns/globalizable.rb
Javi Martín 7bf4e4d611 Sanitize descriptions in the views
Sanitizing descriptions before saving a record has a few drawbacks:

1. It makes the application rely on data being safe in the database. If
somehow dangerous data enters the database, the application will be
vulnerable to XSS attacks
2. It makes the code complicated
3. It isn't backwards compatible; if we decide to disallow a certain
HTML tag in the future, we'd need to sanitize existing data.

On the other hand, sanitizing the data in the view means we don't need
to triple-check dangerous HTML has already been stripped when we see the
method `auto_link_already_sanitized_html`, since now every time we use
it we sanitize the text in the same line we call this method.

We could also sanitize the data twice, both when saving to the database
and when displaying values in the view. However, doing so wouldn't make
the application safer, since we sanitize text introduced through
textarea fields but we don't sanitize text introduced through input
fields.

Finally, we could also overwrite the `description` method so it
sanitizes the text. But we're already introducing Globalize which
overwrites that method, and overwriting it again is a bit too confusing
in my humble opinion. It can also lead to hard-to-debug behaviour.
2019-10-21 21:32:02 +02:00

102 lines
3.4 KiB
Ruby

module Globalizable
MIN_TRANSLATIONS = 1
extend ActiveSupport::Concern
included do
globalize_accessors
accepts_nested_attributes_for :translations, allow_destroy: true
validate :check_translations_number, on: :update, if: :translations_required?
after_validation :copy_error_to_current_translation, on: :update
def locales_not_marked_for_destruction
translations.reject(&:marked_for_destruction?).map(&:locale)
end
def locales_marked_for_destruction
I18n.available_locales - locales_not_marked_for_destruction
end
def locales_persisted_and_marked_for_destruction
translations.select { |t| t.persisted? && t.marked_for_destruction? }.map(&:locale)
end
def translations_required?
translated_attribute_names.any? { |attr| required_attribute?(attr) }
end
if self.paranoid? && translation_class.attribute_names.include?("hidden_at")
translation_class.send :acts_as_paranoid, column: :hidden_at
end
scope :with_translation, -> { joins("LEFT OUTER JOIN #{translations_table_name} ON #{table_name}.id = #{translations_table_name}.#{reflections["translations"].foreign_key} AND #{translations_table_name}.locale='#{I18n.locale}'") }
private
def required_attribute?(attribute)
presence_validators = [ActiveModel::Validations::PresenceValidator,
ActiveRecord::Validations::PresenceValidator]
attribute_validators(attribute).any? { |validator| presence_validators.include? validator }
end
def attribute_validators(attribute)
self.class.validators_on(attribute).map(&:class)
end
def check_translations_number
errors.add(:base, :translations_too_short) unless traslations_count_valid?
end
def traslations_count_valid?
translations.reject(&:marked_for_destruction?).count >= MIN_TRANSLATIONS
end
def copy_error_to_current_translation
return unless errors.added?(:base, :translations_too_short)
if locales_persisted_and_marked_for_destruction.include?(I18n.locale)
locale = I18n.locale
else
locale = locales_persisted_and_marked_for_destruction.first
end
translation = translation_for(locale)
translation.errors.add(:base, :translations_too_short)
end
def searchable_globalized_values
values = {}
translations.each do |translation|
Globalize.with_locale(translation.locale) do
values.merge! searchable_translations_definitions
end
end
values
end
end
class_methods do
def validates_translation(method, options = {})
validates(method, options.merge(if: lambda { |resource| resource.translations.blank? }))
if options.include?(:length)
lenght_validate = { length: options[:length] }
translation_class.instance_eval do
validates method, lenght_validate.merge(if: lambda { |translation| translation.locale == I18n.default_locale })
end
if options.count > 1
translation_class.instance_eval do
validates method, options.reject { |key| key == :length }
end
end
else
translation_class.instance_eval { validates method, options }
end
end
def translation_class_delegate(method)
translation_class.instance_eval { delegate method, to: :globalized_model }
end
end
end