module Loofah
  #
  #  A replacement for
  #  XssTerminate[http://github.com/look/xss_terminate/tree/master],
  #  XssFoliate will strip all tags from your ActiveRecord models'
  #  string and text attributes.
  #
  #  Please read the Loofah documentation for an explanation of the
  #  different scrubbing methods, and
  #  Loofah::XssFoliate::ClassMethods for more information on the
  #  methods.
  #
  #  If you'd like to scrub all fields in all your models (and perhaps *opt-out* in specific models):
  #
  #    # config/initializers/loofah.rb
  #    require 'loofah'
  #    Loofah::XssFoliate.xss_foliate_all_models
  #
  #    # db/schema.rb
  #    create_table "posts" do |t|
  #      t.string  "title"
  #      t.text    "body"
  #      t.string  "author"
  #    end
  #
  #    # app/model/post.rb
  #    class Post < ActiveRecord::Base
  #      #  by default, title, body and author will all be scrubbed down to their inner text
  #    end
  #
  #  OR
  #
  #    # app/model/post.rb
  #    class Post < ActiveRecord::Base
  #      xss_foliate :except => :author  # opt-out of sanitizing author
  #    end
  #
  #  OR
  #
  #      xss_foliate :strip => [:title, body]  # strip unsafe tags from both title and body
  #
  #  OR
  #
  #      xss_foliate :except => :title         # scrub body and author but not title
  #
  #  OR
  #
  #      # remove all tags from title, remove unsafe tags from body
  #      xss_foliate :sanitize => :title, :scrub => :body
  #
  #  OR
  #
  #      # old xss_terminate code will work if you s/_terminate/_foliate/
  #      # was: xss_terminate :except => [:title], :sanitize => [:body]
  #      xss_foliate :except => [:title], :sanitize => [:body]
  #
  #  Alternatively, if you would like to *opt-in* to the models and attributes that are sanitized:
  #
  #    # config/initializers/loofah.rb
  #    require 'loofah'
  #    ## note omission of call to Loofah::XssFoliate.xss_foliate_all_models
  #
  #    # db/schema.rb
  #    create_table "posts" do |t|
  #      t.string  "title"
  #      t.text    "body"
  #      t.string  "author"
  #    end
  #
  #    # app/model/post.rb
  #    class Post < ActiveRecord::Base
  #      xss_foliate  # scrub title, body and author down to their inner text
  #    end
  #
  module XssFoliate
    #
    #  A replacement for
    #  XssTerminate[http://github.com/look/xss_terminate/tree/master],
    #  XssFoliate will strip all tags from your ActiveRecord models'
    #  string and text attributes.
    #
    #  See Loofah::XssFoliate for more example usage.
    #
    module ClassMethods
      # :stopdoc:
      VALID_OPTIONS = [:except, :html5lib_sanitize, :sanitize] + Loofah::Scrubbers.scrubber_symbols
      ALIASED_OPTIONS = {:html5lib_sanitize => :escape, :sanitize => :strip}
      REAL_OPTIONS = VALID_OPTIONS - ALIASED_OPTIONS.keys
      # :startdoc:

      #
      #  Annotate your model with this method to specify which fields
      #  you want scrubbed, and how you want them scrubbed. XssFoliate
      #  assumes all character fields are HTML fragments (as opposed to
      #  full documents, see the Loofah[http://loofah.rubyforge.org/]
      #  documentation for a full explanation of the difference).
      #
      #  Example call:
      #
      #   xss_foliate :except => :author, :strip => :body, :prune => [:title, :description]
      #
      #  *Note* that the values in the options hash can be either an
      #  array of attributes or a single attribute.
      #
      #  Options:
      #
      #   :except => [fields] # don't scrub these fields
      #   :strip  => [fields] # strip unsafe tags from these fields
      #   :escape => [fields] # escape unsafe tags from these fields
      #   :prune  => [fields] # prune unsafe tags and subtrees from these fields
      #   :text   => [fields] # remove everything except the inner text from these fields
      #
      #  XssTerminate compatibility options (note that the default
      #  behavior in XssTerminate corresponds to :text)
      #
      #   :html5lib_sanitize => [fields] # same as :escape
      #   :sanitize          => [fields] # same as :strip
      #
      #  The default is :text for all fields unless otherwise specified.
      #
      def xss_foliate(options = {})
        callback_already_declared = \
        if respond_to?(:before_validation_callback_chain)
          # Rails 2.1 and later
          before_validation_callback_chain.any? {|cb| cb.method == :xss_foliate_fields}
        else
          # Rails 2.0
          cbs = read_inheritable_attribute(:before_validation)
          (! cbs.nil?) && cbs.any? {|cb| cb == :xss_foliate_fields}
        end

        unless callback_already_declared
          before_validation        :xss_foliate_fields
          class_inheritable_reader :xss_foliate_options
          include XssFoliate::InstanceMethods
        end

        options.keys.each do |option|
          raise ArgumentError, "unknown xss_foliate option #{option}" unless VALID_OPTIONS.include?(option)
        end

        REAL_OPTIONS.each do |option|
          options[option] = Array(options[option]).collect { |val| val.to_sym }
        end

        ALIASED_OPTIONS.each do |option, real|
          options[real] += Array(options.delete(option)).collect { |val| val.to_sym } if options[option]
        end

        write_inheritable_attribute(:xss_foliate_options, options)
      end

      #
      #  Class method to determine whether or not this model is applying
      #  xss_foliation to its attributes. Could be useful in test suites.
      #
      def xss_foliated?
        options = read_inheritable_attribute(:xss_foliate_options)
        ! (options.nil? || options.empty?)
      end
    end

    module InstanceMethods
      def xss_foliate_fields # :nodoc:
        # fix a bug with Rails internal AR::Base models that get loaded before
        # the plugin, like CGI::Sessions::ActiveRecordStore::Session
        return if xss_foliate_options.nil?

        self.class.columns.each do |column|
          next unless (column.type == :string || column.type == :text)

          field = column.name.to_sym
          value = self[field]

          next if value.nil? || !value.is_a?(String)

          next if xss_foliate_options[:except].include?(field)

          next if xss_foliated_with_standard_scrubber(field)

          # :text if we're here
          fragment = Loofah.scrub_fragment(value, :strip)
          self[field] = fragment.nil? ? "" : fragment.to_s
        end
      end

      private

      def xss_foliated_with_standard_scrubber(field)
        Loofah::Scrubbers.scrubber_symbols.each do |method|
          if xss_foliate_options[method].include?(field)
            fragment = Loofah.scrub_fragment(self[field], method)
            self[field] = fragment.nil? ? "" : fragment.to_s
            return true
          end
        end
        false
      end
    end

    def self.xss_foliate_all_models
      ActiveRecord::Base.xss_foliate
    end
  end
end

ActiveRecord::Base.extend(Loofah::XssFoliate::ClassMethods)

if defined?(LOOFAH_XSS_FOLIATE_ALL_MODELS) && LOOFAH_XSS_FOLIATE_ALL_MODELS
  Loofah::XssFoliate.xss_foliate_all_models
end