Module: Phlex::SGML::Attributes

Extended by:
Attributes
Included in:
Attributes
Defined in:
lib/phlex/sgml/attributes.rb

Constant Summary collapse

UNSAFE_ATTRIBUTES =
Set.new(%w[srcdoc sandbox http-equiv]).freeze
REF_ATTRIBUTES =
Set.new(%w[href src action formaction lowsrc dynsrc background ping xlinkhref]).freeze
NAMED_CHARACTER_REFERENCES =
{
  "colon" => ":",
  "tab" => "\t",
  "newline" => "\n",
}.freeze
UNSAFE_ATTRIBUTE_NAME_CHARS =
%r([<>&"'/=\s\x00])

Instance Method Summary collapse

Instance Method Details

#decode_html_character_references(value) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/phlex/sgml/attributes.rb', line 171

def decode_html_character_references(value)
  value
    .gsub(/&#x([0-9a-f]+);?/i) {
      begin
        [$1.to_i(16)].pack("U*")
      rescue
        ""
      end
    }
    .gsub(/&#(\d+);?/) {
      begin
        [$1.to_i].pack("U*")
      rescue
        ""
      end
    }
    .gsub(/&([a-z][a-z0-9]+);?/i) {
      NAMED_CHARACTER_REFERENCES[$1.downcase] || ""
    }
end

#generate_attributes(attributes, buffer = +"")) ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/phlex/sgml/attributes.rb', line 15

def generate_attributes(attributes, buffer = +"")
  attributes.each do |k, v|
    next unless v

    name = case k
      when String then k
      when Symbol then k.name.tr("_", "-")
      else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols.")
    end

    value = case v
    when true
      true
    when String
      v.gsub('"', "&quot;")
    when Symbol
      v.name.tr("_", "-").gsub('"', "&quot;")
    when Integer, Float
      v.to_s
    when Date
      v.iso8601
    when Time
      v.respond_to?(:iso8601) ? v.iso8601 : v.strftime("%Y-%m-%dT%H:%M:%S%:z")
    when Hash
      case k
      when :style
        generate_styles(v).gsub('"', "&quot;")
      else
        generate_nested_attributes(v, "#{name}-", buffer)
      end
    when Array
      case k
      when :style
        generate_styles(v).gsub('"', "&quot;")
      else
        generate_nested_tokens(v)
      end
    when Set
      case k
      when :style
        generate_styles(v).gsub('"', "&quot;")
      else
        generate_nested_tokens(v.to_a)
      end
    when Phlex::SGML::SafeObject
      v.to_s.gsub('"', "&quot;")
    else
      if v.respond_to?(:to_h)
        generate_nested_attributes(v.to_h, "#{name}-", buffer)
      else
        raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
      end
    end

    lower_name = name.downcase

    unless Phlex::SGML::SafeObject === v
      normalized_name = lower_name.delete("^a-z-")

      if value != true && REF_ATTRIBUTES.include?(normalized_name)
        case value
        when String
          decoded_value = decode_html_character_references(value)

          if decoded_value.downcase.delete("^a-z:").start_with?("javascript:")
            # We just ignore these because they were likely not specified by the developer.
            next
          end
        else
          raise Phlex::ArgumentError.new("Invalid attribute value for #{k}: #{v.inspect}.")
        end
      end

      if normalized_name.bytesize > 2 && normalized_name.start_with?("on") && !normalized_name.include?("-")
        raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
      end

      if UNSAFE_ATTRIBUTES.include?(normalized_name)
        raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
      end
    end

    if name.match?(UNSAFE_ATTRIBUTE_NAME_CHARS)
      raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
    end

    if lower_name.to_sym == :id && k != :id
      raise Phlex::ArgumentError.new(":id attribute should only be passed as a lowercase symbol.")
    end

    case value
    when true
      buffer << " " << name
    when String
      buffer << " " << name << '="' << value << '"'
    end
  end

  buffer
end

#generate_nested_attributes(attributes, base_name, buffer = +"")) ⇒ Object

Provides the nested-attributes case for serializing out attributes. This allows us to skip many of the checks the __attributes__ method must perform.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/phlex/sgml/attributes.rb', line 118

def generate_nested_attributes(attributes, base_name, buffer = +"")
  attributes.each do |k, v|
    next unless v

    if (root_key = (:_ == k))
      name = ""
      original_base_name = base_name
      base_name = base_name.delete_suffix("-")
    else
      name = case k
        when String then k
        when Symbol then k.name.tr("_", "-")
        else raise Phlex::ArgumentError.new("Attribute keys should be Strings or Symbols")
      end

      if name.match?(UNSAFE_ATTRIBUTE_NAME_CHARS)
        raise Phlex::ArgumentError.new("Unsafe attribute name detected: #{k}.")
      end
    end

    case v
    when true
      buffer << " " << base_name << name
    when String
      buffer << " " << base_name << name << '="' << v.gsub('"', "&quot;") << '"'
    when Symbol
      buffer << " " << base_name << name << '="' << v.name.tr("_", "-").gsub('"', "&quot;") << '"'
    when Integer, Float
      buffer << " " << base_name << name << '="' << v.to_s << '"'
    when Hash
      generate_nested_attributes(v, "#{base_name}#{name}-", buffer)
    when Array
      if (value = generate_nested_tokens(v))
        buffer << " " << base_name << name << '="' << value << '"'
      end
    when Set
      if (value = generate_nested_tokens(v.to_a))
        buffer << " " << base_name << name << '="' << value << '"'
      end
    when Phlex::SGML::SafeObject
      buffer << " " << base_name << name << '="' << v.to_s.gsub('"', "&quot;") << '"'
    else
      raise Phlex::ArgumentError.new("Invalid attribute value #{v.inspect}.")
    end

    if root_key
      base_name = original_base_name
    end

    buffer
  end
end

#generate_nested_tokens(tokens, sep = " ", gsub_from = nil, gsub_to = "") ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/phlex/sgml/attributes.rb', line 192

def generate_nested_tokens(tokens, sep = " ", gsub_from = nil, gsub_to  = "")
  buffer = +""

  i, length = 0, tokens.length

  while i < length
    token = tokens[i]

    case token
    when String
      token = token.gsub(gsub_from, gsub_to) if gsub_from
      if i > 0
        buffer << sep << token
      else
        buffer << token
      end
    when Symbol
      if i > 0
        buffer << sep << token.name.tr("_", "-")
      else
        buffer << token.name.tr("_", "-")
      end
    when Integer, Float, Phlex::SGML::SafeObject
      if i > 0
        buffer << sep << token.to_s
      else
        buffer << token.to_s
      end
    when Array
      if token.length > 0 && (value = generate_nested_tokens(token, sep, gsub_from, gsub_to))
        if i > 0
          buffer << sep << value
        else
          buffer << value
        end
      end
    when Set
      if token.length > 0 && (value = generate_nested_tokens(token.to_a, sep, gsub_from, gsub_to))
        if i > 0
          buffer << sep << value
        else
          buffer << value
        end
      end
    when nil
      # Do nothing
    else
      raise Phlex::ArgumentError.new("Invalid token type: #{token.class}.")
    end

    i += 1
  end

  return if buffer.empty?

  buffer.gsub('"', "&quot;")
end

#generate_styles(styles) ⇒ Object

The result is unsafe so should be escaped.



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/phlex/sgml/attributes.rb', line 251

def generate_styles(styles)
  case styles
  when Array, Set
    styles.filter_map do |s|
      case s
      when String
        if s == "" || s.end_with?(";")
          s
        else
          "#{s};"
        end
      when Phlex::SGML::SafeObject
        value = s.to_s
        value.end_with?(";") ? value : "#{value};"
      when Hash
        next generate_styles(s)
      when nil
        next nil
      else
        raise Phlex::ArgumentError.new("Invalid style: #{s.inspect}.")
      end
    end.join(" ")
  when Hash
    buffer = +""
    i = 0
    styles.each do |k, v|
      prop = case k
      when String
        k
      when Symbol
        k.name.tr("_", "-")
      else
        raise Phlex::ArgumentError.new("Style keys should be Strings or Symbols.")
      end

      value = case v
      when String
        v
      when Symbol
        v.name.tr("_", "-")
      when Integer, Float, Phlex::SGML::SafeObject
        v.to_s
      when nil
        nil
      else
        raise Phlex::ArgumentError.new("Invalid style value: #{v.inspect}")
      end

      if value
        if i == 0
          buffer << prop << ": " << value << ";"
        else
          buffer << " " << prop << ": " << value << ";"
        end
      end

      i += 1
    end

    buffer
  end
end