# Sanitize gem bridge and helpers
#
require 'sanitize'
require 'sanitize/railtie' if defined? Rails
module Sanitize::Rails
# Configures the sanitizer with the given `config` hash.
#
# In your environment.rb, in the after_initialize block:
#
# Sanitize::Rails.configure(
# :elements => [ ... ],
# :attributes => [ ... ],
# ...
# )
#
def self.configure(config)
Engine.configure(config)
end
module Engine
extend self
def configure(config)
@@config = config.freeze
end
# Returns a memoized instance of the Engine with the
# configuration passed to the +configure+ method or with
# the Gem default configuration.
#
def cleaner
@sanitizer ||= ::Sanitize.new(@@config || {})
end
# Returns a copy of the given `string` after sanitizing it
#
def clean(string)
string.dup.tap {|s| clean!(s)}
end
# Sanitizes the given `string` in place
#
def clean!(string)
cleaner.clean!(string)
end
def callback_for(options) #:nodoc:
point = (options[:on] || 'save').to_s
unless %w( save create ).include?(point)
raise ArgumentError, "Invalid callback point #{point}, valid ones are :save and :create"
end
"before_#{point}".intern
end
def method_for(fields) #:nodoc:
"sanitize_#{fields.join('_')}".intern
end
end
module ActionView
def self.included(base)
base.class_eval do
# To make sure we're called *after* the method is defined
undef_method :sanitize
# Overrides ActionView's sanitize() helper to use +Engine#clean+
#
# FIXME: Options are currently ignored.
#
def sanitize(string, options = {})
Engine.clean(string)
end
end
end
end
# Adds the +sanitizes+ method to ActiveRecord children classes
#
module ActiveRecord
# Generates before_save/before_create filters that implement
# sanitization on the given fields, in the given callback
# point.
#
# Usage:
#
# sanitizes :some_field, :some_other_field #, :on => :save
#
# Valid callback points are :save and :create, callbacks are installed "before_"
# by default. Generated callbacks are named with the "sanitize_" prefix follwed
# by the field names separated by an underscore.
#
def sanitizes(*fields)
options = fields.extract_options!
callback = Engine.callback_for(options)
sanitizer = Engine.method_for(fields)
define_method(sanitizer) do # # Unrolled version
fields.each do |field| #
value = send(field)
unless value.blank? # def sanitize_fieldA_fieldB
sanitized = Engine.clean(value) # self.fieldA = Engine.clean(self.fieldA) unless fieldA.blank?
send("#{field}=", sanitized) # self.fieldB = Engine.clean(self.fieldB) unless fieldB.blank?
end # end
end #
end # end
protected sanitizer # protected :sanitize_fieldA_fieldB
send callback, sanitizer # before_save :sanitize_fieldA_fieldB
end
end
# Adds two `sanitize_as_html{,!}` helpers to String itself,
# that call +Engine#clean+ or +Engine#clean!+ in turn
#
module String
# Calls +Engine#clean+ on this String instance
#
def sanitize_as_html
Engine.clean(self)
end
# Calls +Engine#clean!+ on this String instance
#
def sanitize_as_html!
Engine.clean!(self)
end
end
# Test instrumentation
#
module TestHelpers
class << self
# Instruments the given base class with the +assert_sanitizes+
# helper, and memoizes the given options, accessible from the
# helper itself via the +valid+ and +invalid+ methods.
#
# Those methods contains two HTML strings, one assumed to be
# "invalid" and the other, well, "valid".
#
# In your ActiveSupport::Testcase:
#
# Sanitize::Rails::TestHelpers.setup(self,
# :invalid => 'some string',
# :valid => 'some string'
# )
#
def setup(base, options = {})
base.instance_eval { include TestHelpers }
@@options = options
end
def valid; @@options[:valid] rescue nil end
def invalid; @@options[:invalid] rescue nil end
end
# Verifies that the given `klass` sanitizes the given `fields`, by
# checking both the presence of the sanitize callback and that it
# works as expected, by setting the +invalid+ string first, invoking
# the callback and then checking that the string has been changed
# into the +valid+ one.
#
# If you pass an Hash as the last argument, it can contain `:valid`,
# `:invalid` and `:object` keys. The first two ones override the
# configured defaults, while the third executes assertions on the
# specified object. If no :object is given, a new object is instantiated
# by the given `klass` with no arguments.
#
# If neither `:valid`/`:invalid` strings are configured nor are passed
# via the options, the two default strings in the method source are
# used.
#
def assert_sanitizes(klass, *fields)
options = fields.extract_options!
sanitizer = Engine.method_for(fields)
# Verify the callback works
invalid = options[:invalid] || TestHelpers.invalid || 'ntani
'
valid = options[:valid] || TestHelpers.valid || 'ntani
'
object = options[:object] || klass.new
fields.each {|field| object.send("#{field}=", invalid) }
object.send sanitizer
fields.each {|field| assert_equal(valid, object.send(field)) }
end
end
end