# # # # # # # # #
# /12/rack-notags.rb
#
# by               Jan Lelis
# e-mail:          mail@janlelis.de
# type/version:    ruby 
# snippet url:     http://rbJL.net/12/rack-notags.rb
# original post:   http://rbJL.net/12-rack-notags
# license:         (c) 2009 Jan Lelis.
#
# (c) 2009 Jan Lelis.

module Rack

  # Sometimes, simple approaches to solve a problem are the best,
  # because of the danger, that complex ones have holes...
  #
  # Usage (Rails)
  #
  # In config/environment.rb add:
  # require 'path/to/rack-notags.rb'
  # config.middleware.use Rack::NoTags
  #
  # You can activate a different filter mode with:
  # config.middleware.use Rack::NoTags, :paranoid

  class NoTags
    PATTERNS = { # replacement => [ array, of, patterns ]
      :brackets_only => {
        '&lt;' => %w[ < %3C ],
        '&gt;' => %w[ > %3E ]
      },

      # similar to Racks escape_html + url_encoded variants
      :valid_xml => {
        '&lt;' => %w[ < %3C ],
        '&gt;' => %w[ > %3E ],
        '&amp;' => %w[ & %26 ],
        '&#39;' => %w[ ' %27 ],
        '&quot;' => %w[ " %22 ]
      },

      # encodings which might be interpreted as < or > in some situations
      :paranoid => {
        '' => %w[ < > %3C %3E ] + [
/&[lg]t;?/i,
/&#0{0,5}6[02];?/,
/&#x0{0,5}3[ce];?/i ]
      }
    }

    def initialize(app, mode = :brackets_only, ignore = {})
      @app = app
      @patterns = PATTERNS[mode.to_sym] # mode selects the right pattern set
      @ignore = ignore # if one entry of the ignore list matches a post param,
                        # nothing will be filtered
    end

    def call(env)
      # get params in a nice format
      post_params = Rack::Utils.parse_query(env['rack.input'].read, "&")
      get_params = Rack::Utils.parse_query(env['QUERY_STRING'], "&")


      # remove @patterns
      unless ignore?(post_params)
        post_params = strip_all(post_params)
        get_params = strip_all(get_params)
      end

      # update envirionment
      env['rack.input'] = StringIO.new(Rack::Utils.build_query(post_params))
      env['QUERY_STRING'] = Rack::Utils.build_query(get_params)

      # call framework
      @app.call(env)
    end

  private

    # check if param is on ignore list
    def ignore?(params)
      ret = false

      @ignore.each{ |ign_param, ign_value|
        params.each{ |param, value|
          if !value.is_a?(Array) &&
             ign_param.to_s == param.to_s &&
             ign_value.to_s == value.to_s

            ret = true
          end
        }
      }

      ret
    end

    # applies each 'to-substitute'-pattern to the string
    def strip(string)
      begin
        @patterns.each{ |replacement, patterns|
          patterns.each{ |pattern|
            string = string.gsub(pattern, replacement)
          }
        }
      end while catch :still_some do
        # check if there is still any pattern that needs to be aplied
        @patterns.each{ |_, patterns|
          patterns.each{ |pattern|
            if string[pattern] # like =~ but =~ is not
                               # defined for two strings
              throw :still_some, true
            end
          }
        }
        false
      end

      string
    end

    # looks at every param-element an sends it to the strip method
    def strip_all(params)
      ret = {}
      params.each{ |param, value|
        ret[strip(param)] = value.is_a?(Array) ? value.map{|v|strip(v)} : strip(value)
      }

      ret
    end

  end
end