diff --git a/README.rdoc b/README.rdoc index e03bbd1..3b21509 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,8 +1,6 @@ = Acts as Sanitiled -This plugin, based on Chris Wanstrath's venerable acts_as_textiled, extends the -automatic textiling functionality to sanitization as well using as its basis Ryan -Grove's powerful yet simple Sanitize gem. +This plugin, based on Chris Wanstrath's venerable acts_as_textiled, extends the automatic textiling functionality to sanitization as well using as its basis Ryan Grove's powerful yet simple Sanitize gem. The reasoning behind this approach is simple. Filtering input before it is saved to the database (as xss_terminate and many other popular plugins do) often fails to preserve user intent. On the other hand, filtering output at the template level is error prone, and you are begging to get pwned. Short of some sort of taint mode (which Rails 3 will have!), I believe the method employed by acts_as_textiled is the next best thing: you get safe output by default, but input is never corrupted. diff --git a/lib/acts_as_sanitiled.rb b/lib/acts_as_sanitiled.rb index 6c51407..49ef1c5 100644 --- a/lib/acts_as_sanitiled.rb +++ b/lib/acts_as_sanitiled.rb @@ -1,132 +1,122 @@ -require 'sanitize' unless defined? Sanitize +require 'rubygems' +require 'sanitize' +require 'RedCloth' -begin - require 'RedCloth' unless defined? RedCloth -rescue LoadError - nil -end - -module Err - module Acts #:nodoc: all - module Textiled - def self.included(klass) - klass.extend ClassMethods - end +module ActsAsSanitiled #:nodoc: all + def self.included(klass) + klass.extend ClassMethods + end - module ClassMethods - def acts_as_textiled(*attributes) - @textiled_attributes ||= [] + module ClassMethods + def acts_as_textiled(*attributes) + @textiled_attributes ||= [] - @textiled_unicode = String.new.respond_to? :chars + @textiled_unicode = String.new.respond_to? :chars - options = attributes.last.is_a?(Hash) ? attributes.pop : {} - skip_textile = options.delete(:skip_textile) - skip_sanitize = options.delete(:skip_sanitize) + options = attributes.last.is_a?(Hash) ? attributes.pop : {} + skip_textile = options.delete(:skip_textile) + skip_sanitize = options.delete(:skip_sanitize) - raise 'Both textile and sanitize were skipped' if skip_textile && skip_sanitize + raise 'Both textile and sanitize were skipped' if skip_textile && skip_sanitize - sanitize_options = options.empty? ? Sanitize::Config::RELAXED : options - red_cloth_options = attributes.last && attributes.last.is_a?(Array) ? attributes.pop : [] + sanitize_options = options.empty? ? Sanitize::Config::RELAXED : options + red_cloth_options = attributes.last && attributes.last.is_a?(Array) ? attributes.pop : [] - raise 'No attributes were specified to filter' if attributes.empty? + raise 'No attributes were specified to filter' if attributes.empty? - type_options = %w( plain source ) + type_options = %w( plain source ) - attributes.each do |attribute| - define_method(attribute) do |*type| - type = type.first + attributes.each do |attribute| + define_method(attribute) do |*type| + type = type.first - if type.nil? && self[attribute] - if textiled[attribute.to_s].nil? - string = self[attribute] - string = RedCloth.new(string, red_cloth_options).to_html unless skip_textile - string = Sanitize.clean(string, sanitize_options) unless skip_sanitize - textiled[attribute.to_s] = string - end - textiled[attribute.to_s] - elsif type.nil? && self[attribute].nil? - nil - elsif type_options.include?(type.to_s) - send("#{attribute}_#{type}") - else - raise "I don't understand the `#{type}' option. Try #{type_options.join(' or ')}." - end + if type.nil? && self[attribute] + if textiled[attribute.to_s].nil? + string = self[attribute] + string = RedCloth.new(string, red_cloth_options).to_html unless skip_textile + string = Sanitize.clean(string, sanitize_options) unless skip_sanitize + textiled[attribute.to_s] = string end - - define_method("#{attribute}_plain", proc { strip_redcloth_html(__send__(attribute)) if __send__(attribute) } ) - define_method("#{attribute}_source", proc { __send__("#{attribute}_before_type_cast") } ) - - @textiled_attributes << attribute + textiled[attribute.to_s] + elsif type.nil? && self[attribute].nil? + nil + elsif type_options.include?(type.to_s) + send("#{attribute}_#{type}") + else + raise "I don't understand the `#{type}' option. Try #{type_options.join(' or ')}." end - - include Err::Acts::Textiled::InstanceMethods end - def textiled_attributes - Array(@textiled_attributes) - end + define_method("#{attribute}_plain", proc { strip_redcloth_html(__send__(attribute)) if __send__(attribute) } ) + define_method("#{attribute}_source", proc { __send__("#{attribute}_before_type_cast") } ) + + @textiled_attributes << attribute end - module InstanceMethods - def textiled - textiled? ? (@textiled ||= {}) : @attributes.dup - end + include ActsAsSanitiled::InstanceMethods + end - def textiled? - @is_textiled != false - end + def textiled_attributes + Array(@textiled_attributes) + end + end - def textiled=(bool) - @is_textiled = !!bool - end + module InstanceMethods + def textiled + textiled? ? (@textiled ||= {}) : @attributes.dup + end - def textilize - self.class.textiled_attributes.each { |attr| __send__(attr) } - end + def textiled? + @is_textiled != false + end - def reload - textiled.clear - super - end + def textiled=(bool) + @is_textiled = !!bool + end - def write_attribute(attr_name, value) - textiled[attr_name.to_s] = nil - super - end + def textilize + self.class.textiled_attributes.each { |attr| __send__(attr) } + end - private - def strip_redcloth_html(html) - returning html.dup.gsub(html_regexp, '') do |h| - redcloth_glyphs.each do |(entity, char)| - sub = [ :gsub!, entity, char ] - @textiled_unicode ? h.chars.send(*sub) : h.send(*sub) - end - end - end + def reload + textiled.clear + super + end - def redcloth_glyphs - [[ '’', "'" ], - [ '‘', "'" ], - [ '<', '<' ], - [ '>', '>' ], - [ '”', '"' ], - [ '“', '"' ], - [ '…', '...' ], - [ '\1—', '--' ], - [ ' → ', '->' ], - [ ' – ', '-' ], - [ '×', 'x' ], - [ '™', '(TM)' ], - [ '®', '(R)' ], - [ '©', '(C)' ]] - end + def write_attribute(attr_name, value) + textiled[attr_name.to_s] = nil + super + end - def html_regexp - %r{<(?:[^>"']+|"(?:\\.|[^\\"]+)*"|'(?:\\.|[^\\']+)*')*>}xm + private + def strip_redcloth_html(html) + returning html.dup.gsub(html_regexp, '') do |h| + redcloth_glyphs.each do |(entity, char)| + sub = [ :gsub!, entity, char ] + @textiled_unicode ? h.chars.send(*sub) : h.send(*sub) end end end + + def redcloth_glyphs + [[ '’', "'" ], + [ '‘', "'" ], + [ '<', '<' ], + [ '>', '>' ], + [ '”', '"' ], + [ '“', '"' ], + [ '…', '...' ], + [ '\1—', '--' ], + [ ' → ', '->' ], + [ ' – ', '-' ], + [ '×', 'x' ], + [ '™', '(TM)' ], + [ '®', '(R)' ], + [ '©', '(C)' ]] + end + + def html_regexp + %r{<(?:[^>"']+|"(?:\\.|[^\\"]+)*"|'(?:\\.|[^\\']+)*')*>}xm + end end end - -ActiveRecord::Base.send(:include, Err::Acts::Textiled) diff --git a/rails/init.rb b/rails/init.rb new file mode 100644 index 0000000..0345a0c --- /dev/null +++ b/rails/init.rb @@ -0,0 +1 @@ +ActiveRecord::Base.send(:include, ActsAsSanitiled) \ No newline at end of file diff --git a/test/helper.rb b/test/helper.rb index 57d34d0..63fe1f5 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -1,14 +1,12 @@ $:.unshift File.dirname(__FILE__) + '/../lib' begin - require 'rubygems' + require 'acts_as_sanitiled' require 'yaml' - require 'mocha' - require 'active_support' require 'test/spec' - require 'RedCloth' + require 'active_support' rescue LoadError - puts "acts_as_textiled requires the mocha and test-spec gems to run its tests" + puts "acts_as_sanitiled requires yaml, and test-spec to run its tests" exit end @@ -53,7 +51,7 @@ def self.find(id) end end unless defined? ActiveRecord -require 'acts_as_sanitiled.rb' +ActiveRecord::Base.send(:include, ActsAsSanitiled) class Author < ActiveRecord::Base acts_as_textiled :blog, [:lite_mode]