output_safety.rb 4.9 KB
Newer Older
1
require 'erb'
2
require 'active_support/core_ext/kernel/singleton_class'
3 4 5

class ERB
  module Util
6
    HTML_ESCAPE = { '&' => '&amp;',  '>' => '&gt;',   '<' => '&lt;', '"' => '&quot;' }
7
    JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' }
8 9
    HTML_ESCAPE_ONCE_REGEXP = /[\"><]|&(?!([a-zA-Z]+|(#\d+));)/
    JSON_ESCAPE_REGEXP = /[&"><]/
10

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
    # A utility method for escaping HTML tag characters.
    # This method is also aliased as <tt>h</tt>.
    #
    # In your ERB templates, use this method to escape any unsafe content. For example:
    #   <%=h @person.name %>
    #
    # ==== Example:
    #   puts html_escape("is a > 0 & a < 10?")
    #   # => is a &gt; 0 &amp; a &lt; 10?
    def html_escape(s)
      s = s.to_s
      if s.html_safe?
        s
      else
        s.encode(s.encoding, :xml => :attr)[1...-1].html_safe
26 27 28
      end
    end

R
R.T. Lechow 已提交
29
    # Aliasing twice issues a warning "discarding old...". Remove first to avoid it.
30
    remove_method(:h)
31 32 33 34
    alias h html_escape

    module_function :h

35 36 37
    singleton_class.send(:remove_method, :html_escape)
    module_function :html_escape

V
Vijay Dev 已提交
38
    # A utility method for escaping HTML without affecting existing escaped entities.
39 40 41 42 43 44 45 46
    #
    # ==== Examples
    #   html_escape_once("1 < 2 &amp; 3")
    #   # => "1 &lt; 2 &amp; 3"
    #
    #   html_escape_once("&lt;&lt; Accept & Checkout")
    #   # => "&lt;&lt; Accept &amp; Checkout"
    def html_escape_once(s)
47
      result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP) { |special| HTML_ESCAPE[special] }
48 49 50 51 52
      s.html_safe? ? result.html_safe : result
    end

    module_function :html_escape_once

53 54
    # A utility method for escaping HTML entities in JSON strings
    # using \uXXXX JavaScript escape sequences for string literals:
55
    #
56 57
    #   json_escape("is a > 0 & a < 10?")
    #   # => is a \u003E 0 \u0026 a \u003C 10?
58
    #
59 60
    # Note that after this operation is performed the output is not
    # valid JSON. In particular double quotes are removed:
61
    #
62
    #   json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}')
63 64
    #   # => {name:john,created_at:2010-04-28T01:39:31Z,id:1}
    #
65 66 67 68 69
    # This method is also aliased as +j+, and available as a helper
    # in Rails templates:
    #
    #   <%=j @person.to_json %>
    #
70
    def json_escape(s)
71
      result = s.to_s.gsub(JSON_ESCAPE_REGEXP) { |special| JSON_ESCAPE[special] }
72
      s.html_safe? ? result.html_safe : result
73 74 75 76 77 78 79 80
    end

    alias j json_escape
    module_function :j
    module_function :json_escape
  end
end

81 82 83 84 85 86
class Object
  def html_safe?
    false
  end
end

87
class Numeric
88 89 90 91 92
  def html_safe?
    true
  end
end

93 94
module ActiveSupport #:nodoc:
  class SafeBuffer < String
95
    UNSAFE_STRING_METHODS = ["capitalize", "chomp", "chop", "delete", "downcase", "gsub", "lstrip", "next", "reverse", "rstrip", "slice", "squeeze", "strip", "sub", "succ", "swapcase", "tr", "tr_s", "upcase", "prepend"].freeze
96

97 98 99 100 101 102 103 104 105
    alias_method :original_concat, :concat
    private :original_concat

    class SafeConcatError < StandardError
      def initialize
        super "Could not concatenate to the buffer because it is not html safe."
      end
    end

106 107 108 109 110 111 112 113 114 115 116 117
    def [](*args)
      return super if args.size < 2

      if html_safe?
        new_safe_buffer = super
        new_safe_buffer.instance_eval { @html_safe = true }
        new_safe_buffer
      else
        to_str[*args]
      end
    end

118
    def safe_concat(value)
119
      raise SafeConcatError unless html_safe?
120 121
      original_concat(value)
    end
122

123
    def initialize(*)
124
      @html_safe = true
125 126 127 128 129
      super
    end

    def initialize_copy(other)
      super
130
      @html_safe = other.html_safe?
131 132
    end

A
Akira Matsuda 已提交
133 134 135 136 137 138
    def clone_empty
      new_safe_buffer = self[0, 0]
      new_safe_buffer.instance_variable_set(:@dirty, @dirty)
      new_safe_buffer
    end

139
    def concat(value)
140
      if !html_safe? || value.html_safe?
141 142 143 144 145
        super(value)
      else
        super(ERB::Util.h(value))
      end
    end
146
    alias << concat
J
Joshua Peek 已提交
147

148 149 150 151 152
    def +(other)
      dup.concat(other)
    end

    def html_safe?
153
      defined?(@html_safe) && @html_safe
154
    end
J
Joshua Peek 已提交
155

156 157 158
    def to_s
      self
    end
159

160 161 162 163
    def to_param
      to_str
    end

164 165 166 167
    def encode_with(coder)
      coder.represent_scalar nil, to_str
    end

168
    UNSAFE_STRING_METHODS.each do |unsafe_method|
169 170 171 172 173 174 175
      if 'String'.respond_to?(unsafe_method)
        class_eval <<-EOT, __FILE__, __LINE__ + 1
          def #{unsafe_method}(*args, &block)       # def capitalize(*args, &block)
            to_str.#{unsafe_method}(*args, &block)  #   to_str.capitalize(*args, &block)
          end                                       # end

          def #{unsafe_method}!(*args)              # def capitalize!(*args)
176
            @html_safe = false                      #   @html_safe = false
177 178 179 180
            super                                   #   super
          end                                       # end
        EOT
      end
181
    end
182
  end
183
end
J
Joshua Peek 已提交
184

185 186 187 188
class String
  def html_safe
    ActiveSupport::SafeBuffer.new(self)
  end
189
end