app/models/concerns/sanitization.rb
# Sanitization DSL. Should be used for all text input fields
module Sanitization
extend ActiveSupport::Concern
require 'cgi'
protected
# DSL method to sanitize specific fields automatically before the validation step
#
# @api semipublic
# @param fields [Symbol] As many ActiveRecord fields as you like
# @param options [Hash] (Optional)
# :method - default: 'clean' | others: 'tiny_mce'
# :admin - default: false | allows more elements when true
# + other before_validate params
# @return [undefined]
def auto_sanitize *fields
options = {} unless (options = fields.last).is_a? Hash
options.reverse_merge! method: 'clean', admin: false, remove_all_spaces: false
# For each field: define a new method, then register a callback to that method
fields.each do |field|
create_sanitization_method_for field, options
end
end
private
def create_sanitization_method_for field, options
if field.is_a? Symbol
method_name = "sanitize_#{ options[:method] }_#{ field }"
define_method method_name, send("#{ options[:method] }_sanitization",
field, options[:admin],
options[:remove_all_spaces])
before_validation method_name.to_sym, options
create_html_safe_method_for_tiny_mce field, options
end
end
def create_html_safe_method_for_tiny_mce field, options
# TinyMCE contents are assumed to have HTML content and are now sanitized. So we can always give them the html_safe flag.
if options[:method] == 'tiny_mce'
define_method "#{ field }_with_html_safe",
Proc.new {
orig = send("#{ field }_without_html_safe")
orig.is_a?(String) ? orig.html_safe : orig
}
define_method field, -> { read_attribute field } # no idea why this fix is needed
alias_method :"#{field}_without_html_safe", field
alias_method field, :"#{field}_with_html_safe"
end
end
# Method content for sanitize_clean_X callbacks
# @api private
# @return [Proc]
def clean_sanitization field, _admin_mode, remove_spaces_mode # admin_mode not used
Proc.new { self.send("#{field}=", Sanitization.sanitize_clean(self.send(field), remove_spaces_mode)) }
end
# Method content for sanitize_tiny_mce_X callbacks
# @api private
# @return [Proc]
def tiny_mce_sanitization field, admin_mode, remove_spaces_mode
Proc.new { self.send("#{field}=", Sanitization.sanitize_tiny_mce(self.send(field), admin_mode, remove_spaces_mode)) }
end
# Sanitization specifically for tiny mce fields which allow certain HTML elements
#
# @api private
# @param field [String] The content to sanitize
# @param admin_mode [Boolean] When true more tags are allowed
# @return [String] The sanitized content
def self.sanitize_tiny_mce field, admin_mode = false, remove_spaces = false
modify Sanitize.clean(
field,
elements: admin_mode ?
%w(a b i strong em p h1 h2 h3 h4 h5 h6 br hr ul ol li img div span iframe) :
%w(a b i strong em p h1 h2 h3 h4 h5 h6 br hr ul ol li),
attributes: {
'a' => %w(href type target),
'img' => %w(src alt),
'iframe' => # iframes aren't allowed for non-admins
%w(src frameborder webkitallowfullscreen mozallowfullscreen allowfullscreen),
:all => admin_mode ?
['width', 'height', 'data', 'name', 'id', 'class', 'style', 'data-truncate'] :
%w(width height name)
},
css: {
properties: admin_mode ?
['text-align', 'margin', 'margin-top', 'margin-bottom', 'margin-left', 'margin-right', 'tab-stops', 'font-family', 'font-size', 'line-height', 'padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right', 'cursor', 'height', 'width', 'font-variant', 'font-style', 'color', 'font-weight', 'text-decoration'] :
[]
},
protocols: {
'a' => { 'href' => ['http', 'https', 'mailto', :relative] },
'img' => { 'src' => ['http', 'https', :relative] }
}
), remove_spaces
end
# Clean sanitization with further string modification
#
# @api private
# @param field [String] The content to sanitize
# @param remove_spaces [Boolean] for stricter modification
# @return [String] The sanitized content
def self.sanitize_clean field, remove_spaces
# Needed because of the inject_questionnaire method in the mass_upload model (else statement)
field = field.first if field.class == Array
field = Sanitize.clean(field) if field
reverse_encoding modify field, remove_spaces
end
# Modify sanitized strings even further
# @api private
# @param string [String, nil] The string to modify
# @return [String] The modified string
def self.modify string, remove_spaces
if string.is_a? String
string
.strip() # Remove leading and trailing white space
.gsub(
/\s+/, (remove_spaces ? '' : ' ') # Either multiple whitespaces become one, or all whitespaces are removed
)
else
string
end
end
# Clean sanitized fields get HTML entities encoded, which we need to revert
# @api private
# @param string [String, nil] The string with HTML-entities
# @return [String] The unencoded string
def self.reverse_encoding string
string.is_a?(String) ? CGI.unescapeHTML(string) : string
end
end