sanitize 6.1.3 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/sanitize/css.rb CHANGED
@@ -1,331 +1,428 @@
1
- # encoding: utf-8
2
-
3
- require 'crass'
4
- require 'set'
5
-
6
- class Sanitize; class CSS
7
- attr_reader :config
8
-
9
- # -- Class Methods -----------------------------------------------------------
10
-
11
- # Sanitizes inline CSS style properties.
12
- #
13
- # This is most useful for sanitizing non-stylesheet fragments of CSS like you
14
- # would find in the `style` attribute of an HTML element. To sanitize a full
15
- # CSS stylesheet, use {.stylesheet}.
16
- #
17
- # @example
18
- # Sanitize::CSS.properties("background: url(foo.png); color: #fff;")
19
- #
20
- # @return [String] Sanitized CSS properties.
21
- def self.properties(css, config = {})
22
- self.new(config).properties(css)
23
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "crass"
4
+ require "set"
5
+
6
+ class Sanitize
7
+ class CSS
8
+ attr_reader :config
9
+
10
+ # -- Class Methods ---------------------------------------------------------
11
+
12
+ # Sanitizes inline CSS style properties.
13
+ #
14
+ # This is most useful for sanitizing non-stylesheet fragments of CSS like
15
+ # you would find in the `style` attribute of an HTML element. To sanitize a
16
+ # full CSS stylesheet, use {.stylesheet}.
17
+ #
18
+ # @example
19
+ # Sanitize::CSS.properties("background: url(foo.png); color: #fff;")
20
+ #
21
+ # @return [String] Sanitized CSS properties.
22
+ def self.properties(css, config = {})
23
+ new(config).properties(css)
24
+ end
24
25
 
25
- # Sanitizes a full CSS stylesheet.
26
- #
27
- # A stylesheet may include selectors, at-rules, and comments. To sanitize only
28
- # inline style properties such as the contents of an HTML `style` attribute,
29
- # use {.properties}.
30
- #
31
- # @example
32
- # css = %[
33
- # .foo {
34
- # background: url(foo.png);
35
- # color: #fff;
36
- # }
37
- #
38
- # #bar {
39
- # font: 42pt 'Comic Sans MS';
40
- # }
41
- # ]
42
- #
43
- # Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED)
44
- #
45
- # @return [String] Sanitized CSS stylesheet.
46
- def self.stylesheet(css, config = {})
47
- self.new(config).stylesheet(css)
48
- end
26
+ # Sanitizes a full CSS stylesheet.
27
+ #
28
+ # A stylesheet may include selectors, at-rules, and comments. To sanitize
29
+ # only inline style properties such as the contents of an HTML `style`
30
+ # attribute, use {.properties}.
31
+ #
32
+ # @example
33
+ # css = %[
34
+ # .foo {
35
+ # background: url(foo.png);
36
+ # color: #fff;
37
+ # }
38
+ #
39
+ # #bar {
40
+ # font: 42pt 'Comic Sans MS';
41
+ # }
42
+ # ]
43
+ #
44
+ # Sanitize::CSS.stylesheet(css, Sanitize::Config::RELAXED)
45
+ #
46
+ # @return [String] Sanitized CSS stylesheet.
47
+ def self.stylesheet(css, config = {})
48
+ new(config).stylesheet(css)
49
+ end
49
50
 
50
- # Sanitizes the given Crass CSS parse tree and all its children, modifying it
51
- # in place.
52
- #
53
- # @example
54
- # css = %[
55
- # .foo {
56
- # background: url(foo.png);
57
- # color: #fff;
58
- # }
59
- #
60
- # #bar {
61
- # font: 42pt 'Comic Sans MS';
62
- # }
63
- # ]
64
- #
65
- # tree = Crass.parse(css)
66
- # Sanitize::CSS.tree!(tree, Sanitize::Config::RELAXED)
67
- #
68
- # @return [Array] Sanitized Crass CSS parse tree.
69
- def self.tree!(tree, config = {})
70
- self.new(config).tree!(tree)
71
- end
51
+ # Sanitizes the given Crass CSS parse tree and all its children, modifying
52
+ # it in place.
53
+ #
54
+ # @example
55
+ # css = %[
56
+ # .foo {
57
+ # background: url(foo.png);
58
+ # color: #fff;
59
+ # }
60
+ #
61
+ # #bar {
62
+ # font: 42pt 'Comic Sans MS';
63
+ # }
64
+ # ]
65
+ #
66
+ # tree = Crass.parse(css)
67
+ # Sanitize::CSS.tree!(tree, Sanitize::Config::RELAXED)
68
+ #
69
+ # The tree must be a list of rules, such as the tree returned by
70
+ # `Crass.parse`. To sanitize a list of declarations, such as the contents
71
+ # of an HTML `style` attribute, use {.properties}.
72
+ #
73
+ # @return [Array] Sanitized Crass CSS parse tree.
74
+ def self.tree!(tree, config = {})
75
+ new(config).tree!(tree)
76
+ end
72
77
 
73
- # -- Instance Methods --------------------------------------------------------
78
+ # -- Instance Methods ------------------------------------------------------
74
79
 
75
- # Returns a new Sanitize::CSS object initialized with the settings in
76
- # _config_.
77
- def initialize(config = {})
78
- @config = Config.merge(Config::DEFAULT[:css], config[:css] || config)
80
+ # Returns a new Sanitize::CSS object initialized with the settings in
81
+ # _config_.
82
+ def initialize(config = {})
83
+ @config = Config.merge(Config::DEFAULT[:css], config[:css] || config)
79
84
 
80
- @at_rules = Set.new(@config[:at_rules])
81
- @at_rules_with_properties = Set.new(@config[:at_rules_with_properties])
82
- @at_rules_with_styles = Set.new(@config[:at_rules_with_styles])
83
- @import_url_validator = @config[:import_url_validator]
84
- end
85
+ @at_rules = Set.new(@config[:at_rules])
86
+ @at_rules_with_properties = Set.new(@config[:at_rules_with_properties])
87
+ @at_rules_with_styles = Set.new(@config[:at_rules_with_styles])
88
+ @import_url_validator = @config[:import_url_validator]
85
89
 
86
- # Sanitizes inline CSS style properties.
87
- #
88
- # This is most useful for sanitizing non-stylesheet fragments of CSS like you
89
- # would find in the `style` attribute of an HTML element. To sanitize a full
90
- # CSS stylesheet, use {#stylesheet}.
91
- #
92
- # @example
93
- # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
94
- # scss.properties("background: url(foo.png); color: #fff;")
95
- #
96
- # @return [String] Sanitized CSS properties.
97
- def properties(css)
98
- tree = Crass.parse_properties(css,
99
- :preserve_comments => @config[:allow_comments],
100
- :preserve_hacks => @config[:allow_hacks])
101
-
102
- tree!(tree)
103
- Crass::Parser.stringify(tree)
104
- end
90
+ @parse_options = {
91
+ preserve_comments: @config[:allow_comments],
92
+ preserve_hacks: @config[:allow_hacks]
93
+ }.freeze
94
+ end
105
95
 
106
- # Sanitizes a full CSS stylesheet.
107
- #
108
- # A stylesheet may include selectors, at-rules, and comments. To sanitize only
109
- # inline style properties such as the contents of an HTML `style` attribute,
110
- # use {#properties}.
111
- #
112
- # @example
113
- # css = %[
114
- # .foo {
115
- # background: url(foo.png);
116
- # color: #fff;
117
- # }
118
- #
119
- # #bar {
120
- # font: 42pt 'Comic Sans MS';
121
- # }
122
- # ]
123
- #
124
- # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
125
- # scss.stylesheet(css)
126
- #
127
- # @return [String] Sanitized CSS stylesheet.
128
- def stylesheet(css)
129
- tree = Crass.parse(css,
130
- :preserve_comments => @config[:allow_comments],
131
- :preserve_hacks => @config[:allow_hacks])
132
-
133
- tree!(tree)
134
- Crass::Parser.stringify(tree)
135
- end
96
+ # Sanitizes inline CSS style properties.
97
+ #
98
+ # This is most useful for sanitizing non-stylesheet fragments of CSS like
99
+ # you would find in the `style` attribute of an HTML element. To sanitize a
100
+ # full CSS stylesheet, use {#stylesheet}.
101
+ #
102
+ # @example
103
+ # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
104
+ # scss.properties("background: url(foo.png); color: #fff;")
105
+ #
106
+ # @return [String] Sanitized CSS properties.
107
+ def properties(css)
108
+ tree = Crass.parse_properties(css, @parse_options)
109
+ sanitize_tree!(tree, declarations: true)
110
+ Crass::Parser.stringify(tree)
111
+ end
136
112
 
137
- # Sanitizes the given Crass CSS parse tree and all its children, modifying it
138
- # in place.
139
- #
140
- # @example
141
- # css = %[
142
- # .foo {
143
- # background: url(foo.png);
144
- # color: #fff;
145
- # }
146
- #
147
- # #bar {
148
- # font: 42pt 'Comic Sans MS';
149
- # }
150
- # ]
151
- #
152
- # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
153
- # tree = Crass.parse(css)
154
- #
155
- # scss.tree!(tree)
156
- #
157
- # @return [Array] Sanitized Crass CSS parse tree.
158
- def tree!(tree)
159
- preceded_by_property = false
160
-
161
- tree.map! do |node|
162
- next nil if node.nil?
163
-
164
- case node[:node]
165
- when :at_rule
166
- preceded_by_property = false
167
- next at_rule!(node)
168
-
169
- when :comment
170
- next node if @config[:allow_comments]
171
-
172
- when :property
173
- prop = property!(node)
174
- preceded_by_property = !prop.nil?
175
- next prop
176
-
177
- when :semicolon
178
- # Only preserve the semicolon if it was preceded by an allowlisted
179
- # property. Otherwise, omit it in order to prevent redundant semicolons.
180
- if preceded_by_property
181
- preceded_by_property = false
182
- next node
113
+ # Sanitizes a full CSS stylesheet.
114
+ #
115
+ # A stylesheet may include selectors, at-rules, and comments. To sanitize
116
+ # only inline style properties such as the contents of an HTML `style`
117
+ # attribute, use {#properties}.
118
+ #
119
+ # @example
120
+ # css = %[
121
+ # .foo {
122
+ # background: url(foo.png);
123
+ # color: #fff;
124
+ # }
125
+ #
126
+ # #bar {
127
+ # font: 42pt 'Comic Sans MS';
128
+ # }
129
+ # ]
130
+ #
131
+ # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
132
+ # scss.stylesheet(css)
133
+ #
134
+ # @return [String] Sanitized CSS stylesheet.
135
+ def stylesheet(css)
136
+ tree = Crass.parse(css, @parse_options)
137
+ tree!(tree)
138
+ Crass::Parser.stringify(tree)
139
+ end
140
+
141
+ # Sanitizes the given Crass CSS parse tree and all its children, modifying
142
+ # it in place.
143
+ #
144
+ # @example
145
+ # css = %[
146
+ # .foo {
147
+ # background: url(foo.png);
148
+ # color: #fff;
149
+ # }
150
+ #
151
+ # #bar {
152
+ # font: 42pt 'Comic Sans MS';
153
+ # }
154
+ # ]
155
+ #
156
+ # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
157
+ # tree = Crass.parse(css)
158
+ #
159
+ # scss.tree!(tree)
160
+ #
161
+ # The tree must be a list of rules, such as the tree returned by
162
+ # `Crass.parse`. To sanitize a list of declarations, such as the contents
163
+ # of an HTML `style` attribute, use {#properties}.
164
+ #
165
+ # @return [Array] Sanitized Crass CSS parse tree.
166
+ def tree!(tree)
167
+ sanitize_tree!(tree, declarations: false)
168
+ end
169
+
170
+ # -- Protected Instance Methods --------------------------------------------
171
+ protected
172
+
173
+ # Sanitizes a CSS at-rule node. Returns the sanitized node, or `nil` if the
174
+ # current config doesn't allow this at-rule.
175
+ #
176
+ # If _declarations_ is `true`, the at-rule appears in a list of
177
+ # declarations (such as the contents of a style rule) rather than in a list
178
+ # of rules.
179
+ def at_rule!(rule, declarations:)
180
+ name = rule[:name].downcase
181
+
182
+ return nil if contains_invalid_node?(rule[:prelude])
183
+
184
+ if @at_rules_with_styles.include?(name)
185
+ # Browsers that support CSS nesting parse the block of an at-rule like
186
+ # `@media` that appears in a list of declarations as more declarations
187
+ # rather than as rules, so it must be sanitized as declarations.
188
+ # Otherwise a declaration like `position:fixed;a{}` would be parsed as
189
+ # a style rule with the selector `position:fixed;a` and would bypass
190
+ # the property allowlist.
191
+ styles = if declarations
192
+ Crass::Parser.parse_properties(rule[:block], @parse_options)
193
+ else
194
+ Crass::Parser.parse_rules(rule[:block], @parse_options)
183
195
  end
184
196
 
185
- when :style_rule
186
- preceded_by_property = false
187
- tree!(node[:children])
188
- next node
197
+ rule[:block] = sanitize_tree!(styles, declarations: declarations)
198
+
199
+ elsif @at_rules_with_properties.include?(name)
200
+ props = Crass::Parser.parse_properties(rule[:block], @parse_options)
201
+ rule[:block] = sanitize_tree!(props, declarations: true)
189
202
 
190
- when :whitespace
191
- next node
203
+ elsif @at_rules.include?(name)
204
+ return nil if name == "import" && !import_url_allowed?(rule)
205
+ return nil if rule.has_key?(:block)
206
+ else
207
+ return nil
192
208
  end
193
209
 
194
- nil
210
+ rule
195
211
  end
196
212
 
197
- tree
198
- end
199
-
200
- # -- Protected Instance Methods ----------------------------------------------
201
- protected
213
+ # Returns `true` if the given array of Crass nodes contains an invalid node
214
+ # (see {#invalid_node?}) at any depth.
215
+ def contains_invalid_node?(nodes)
216
+ contains_node?(nodes) { |node| invalid_node?(node) }
217
+ end
202
218
 
203
- # Sanitizes a CSS at-rule node. Returns the sanitized node, or `nil` if the
204
- # current config doesn't allow this at-rule.
205
- def at_rule!(rule)
206
- name = rule[:name].downcase
219
+ # Returns `true` if the given array of Crass nodes contains a node at any
220
+ # depth for which the given block returns a truthy value.
221
+ def contains_node?(nodes)
222
+ return false unless Array === nodes
207
223
 
208
- if @at_rules_with_styles.include?(name)
209
- styles = Crass::Parser.parse_rules(rule[:block],
210
- :preserve_comments => @config[:allow_comments],
211
- :preserve_hacks => @config[:allow_hacks])
224
+ stack = nodes.dup
212
225
 
213
- rule[:block] = tree!(styles)
226
+ until stack.empty?
227
+ node = stack.pop
228
+ next unless Hash === node
229
+ return true if yield(node)
214
230
 
215
- elsif @at_rules_with_properties.include?(name)
216
- props = Crass::Parser.parse_properties(rule[:block],
217
- :preserve_comments => @config[:allow_comments],
218
- :preserve_hacks => @config[:allow_hacks])
231
+ value = node[:value]
232
+ stack.concat(value) if Array === value
233
+ end
219
234
 
220
- rule[:block] = tree!(props)
235
+ false
236
+ end
221
237
 
222
- elsif @at_rules.include?(name)
223
- return nil if name == "import" && !import_url_allowed?(rule)
224
- return nil if rule.has_key?(:block)
225
- else
226
- return nil
238
+ # Returns `true` if the given CSS function name is an image-related function
239
+ # that may contain image URLs that need to be validated.
240
+ def image_function?(name)
241
+ ["image", "image-set", "-webkit-image-set"].include?(name)
227
242
  end
228
243
 
229
- rule
230
- end
244
+ # Passes the URL value of an @import rule to a block to ensure
245
+ # it's an allowed URL
246
+ def import_url_allowed?(rule)
247
+ return true unless @import_url_validator
231
248
 
232
- # Returns `true` if the given CSS function name is an image-related function
233
- # that may contain image URLs that need to be validated.
234
- def image_function?(name)
235
- ['image', 'image-set', '-webkit-image-set'].include?(name)
236
- end
249
+ url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string }
250
+
251
+ # don't allow @imports with no URL value
252
+ return false unless url_token && (import_url = url_token[:value])
237
253
 
238
- # Passes the URL value of an @import rule to a block to ensure
239
- # it's an allowed URL
240
- def import_url_allowed?(rule)
241
- return true unless @import_url_validator
254
+ @import_url_validator.call(import_url)
255
+ end
242
256
 
243
- url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string }
257
+ # Returns `true` if the given Crass node is a `:bad_url` or `:error` node.
258
+ #
259
+ # A bad URL is invalid CSS that a browser will discard, but a parser that
260
+ # disagrees with the browser about where a bad URL ends could be tricked
261
+ # into hiding CSS inside it.
262
+ #
263
+ # An `:error` node is CSS that Crass couldn't or wouldn't parse, such as a
264
+ # block nested more deeply than Crass's maximum nesting depth. Its contents
265
+ # are never inspected, but they're preserved verbatim when the tree is
266
+ # stringified, so a browser would still see them.
267
+ #
268
+ # It's safest to reject any construct that contains either one.
269
+ def invalid_node?(node)
270
+ node[:node] == :bad_url || node[:node] == :error
271
+ end
244
272
 
245
- # don't allow @imports with no URL value
246
- return false unless url_token && (import_url = url_token[:value])
273
+ # Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the
274
+ # current config doesn't allow this property.
275
+ def property!(prop)
276
+ name = prop[:name].downcase
247
277
 
248
- @import_url_validator.call(import_url)
249
- end
278
+ # Preserve IE * and _ hacks if desired.
279
+ if @config[:allow_hacks]
280
+ name.slice!(0) if /\A[*_]/.match?(name)
281
+ end
250
282
 
251
- # Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the
252
- # current config doesn't allow this property.
253
- def property!(prop)
254
- name = prop[:name].downcase
283
+ return nil unless @config[:properties].include?(name)
255
284
 
256
- # Preserve IE * and _ hacks if desired.
257
- if @config[:allow_hacks]
258
- name.slice!(0) if name =~ /\A[*_]/
259
- end
285
+ nodes = prop[:children].dup
286
+ combined_value = +""
260
287
 
261
- return nil unless @config[:properties].include?(name)
288
+ nodes.each do |child|
289
+ # Reject properties that contain an invalid node at any depth.
290
+ return nil if invalid_node?(child)
262
291
 
263
- nodes = prop[:children].dup
264
- combined_value = String.new
292
+ value = child[:value]
265
293
 
266
- nodes.each do |child|
267
- value = child[:value]
294
+ case child[:node]
295
+ when :ident
296
+ combined_value << value.downcase if String === value
268
297
 
269
- case child[:node]
270
- when :ident
271
- combined_value << value.downcase if String === value
298
+ when :function
299
+ if child.key?(:name)
300
+ name = child[:name].downcase
272
301
 
273
- when :function
274
- if child.key?(:name)
275
- name = child[:name].downcase
302
+ if name == "url"
303
+ return nil unless valid_url?(child)
304
+ end
305
+
306
+ if image_function?(name)
307
+ return nil unless valid_image?(child)
308
+ end
276
309
 
277
- if name == 'url'
278
- return nil unless valid_url?(child)
310
+ combined_value << name
311
+ return nil if name == "expression" || combined_value == "expression"
279
312
  end
280
313
 
281
- if image_function?(name)
282
- return nil unless valid_image?(child)
314
+ if Array === value
315
+ nodes.concat(value)
316
+ elsif String === value
317
+ lowercase_value = value.downcase
318
+ combined_value << lowercase_value
319
+ return nil if lowercase_value == "expression" || combined_value == "expression"
283
320
  end
284
321
 
285
- combined_value << name
286
- return nil if name == 'expression' || combined_value == 'expression'
322
+ when :url
323
+ return nil unless valid_url?(child)
324
+
325
+ when :simple_block
326
+ # Reject properties that contain a `{}` block at any depth.
327
+ #
328
+ # A `{}` block isn't valid in a standard property value, and browsers
329
+ # that support CSS nesting will reparse an invalid declaration that
330
+ # contains one as a nested style rule (e.g.
331
+ # `color:hover{position:fixed}` becomes the rule
332
+ # `color:hover { position: fixed }`), which would allow disallowed
333
+ # properties to be smuggled past the sanitizer.
334
+ return nil if child[:start] == "{"
335
+
336
+ nodes.concat(value) if Array === value
287
337
  end
338
+ end
288
339
 
289
- if Array === value
290
- nodes.concat(value)
291
- elsif String === value
292
- lowercase_value = value.downcase
293
- combined_value << lowercase_value
294
- return nil if lowercase_value == 'expression' || combined_value == 'expression'
295
- end
340
+ prop
341
+ end
296
342
 
297
- when :url
298
- return nil unless valid_url?(child)
343
+ # Sanitizes the given Crass CSS parse tree and all its children, modifying
344
+ # it in place. See {#tree!}.
345
+ #
346
+ # If _declarations_ is `true`, the tree is a list of declarations (such as
347
+ # the contents of a style rule) rather than a list of rules.
348
+ def sanitize_tree!(tree, declarations:)
349
+ preceded_by_property = false
299
350
 
300
- when :bad_url
301
- return nil
351
+ tree.map! do |node|
352
+ next nil if node.nil?
353
+
354
+ case node[:node]
355
+ when :at_rule
356
+ preceded_by_property = false
357
+ next at_rule!(node, declarations: declarations)
358
+
359
+ when :comment
360
+ next node if @config[:allow_comments]
361
+
362
+ when :property
363
+ prop = property!(node)
364
+ preceded_by_property = !prop.nil?
365
+ next prop
366
+
367
+ when :semicolon
368
+ # Only preserve the semicolon if it was preceded by an allowlisted
369
+ # property. Otherwise, omit it in order to prevent redundant
370
+ # semicolons.
371
+ if preceded_by_property
372
+ preceded_by_property = false
373
+ next node
374
+ end
375
+
376
+ when :style_rule
377
+ preceded_by_property = false
378
+
379
+ # Reject style rules whose selectors contain an invalid node (see
380
+ # {#invalid_node?}) or a semicolon at any depth.
381
+ #
382
+ # A semicolon is never valid in a selector, but if a style rule
383
+ # appears where a browser expects declarations (such as the contents
384
+ # of an `@media` rule in a list of declarations), the browser will
385
+ # treat everything before the semicolon as a declaration. For
386
+ # example, `position:fixed;a{}` would be treated as the declaration
387
+ # `position:fixed` followed by the style rule `a{}`.
388
+ next nil if contains_node?(node.dig(:selector, :tokens)) { |token|
389
+ invalid_node?(token) || token[:node] == :semicolon
390
+ }
391
+
392
+ sanitize_tree!(node[:children], declarations: true)
393
+ next node
394
+
395
+ when :whitespace
396
+ next node
397
+ end
398
+
399
+ nil
302
400
  end
303
- end
304
401
 
305
- prop
306
- end
402
+ tree
403
+ end
307
404
 
308
- # Returns `true` if the given node (which may be of type `:url` or
309
- # `:function`, since the CSS syntax can produce both) uses an allowlisted
310
- # protocol.
311
- def valid_url?(node)
312
- type = node[:node]
405
+ # Returns `true` if the given node (which may be of type `:url` or
406
+ # `:function`, since the CSS syntax can produce both) uses an allowlisted
407
+ # protocol.
408
+ def valid_url?(node)
409
+ type = node[:node]
313
410
 
314
- if type == :function
315
- return false unless node.key?(:name) && node[:name].downcase == 'url'
316
- return false unless Array === node[:value]
411
+ if type == :function
412
+ return false unless node.key?(:name) && node[:name].downcase == "url"
413
+ return false unless Array === node[:value]
317
414
 
318
- # A URL function's `:value` should be an array containing no more than one
319
- # `:string` node and any number of `:whitespace` nodes.
320
- #
321
- # If it contains more than one `:string` node, or if it contains any other
322
- # nodes except `:whitespace` nodes, it's not valid.
323
- url_string_node = nil
415
+ # A URL function's `:value` should be an array containing no more than
416
+ # one `:string` node and any number of `:whitespace` nodes.
417
+ #
418
+ # If it contains more than one `:string` node, or if it contains any
419
+ # other nodes except `:whitespace` nodes, it's not valid.
420
+ url_string_node = nil
324
421
 
325
- node[:value].each do |token|
326
- return false unless Hash === token
422
+ node[:value].each do |token|
423
+ return false unless Hash === token
327
424
 
328
- case token[:node]
425
+ case token[:node]
329
426
  when :string
330
427
  return false unless url_string_node.nil?
331
428
  url_string_node = token
@@ -335,47 +432,45 @@ class Sanitize; class CSS
335
432
 
336
433
  else
337
434
  return false
435
+ end
338
436
  end
339
- end
340
437
 
341
- return false if url_string_node.nil?
342
- url = url_string_node[:value]
343
- elsif type == :url
344
- url = node[:value]
345
- else
346
- return false
347
- end
438
+ return false if url_string_node.nil?
439
+ url = url_string_node[:value]
440
+ elsif type == :url
441
+ url = node[:value]
442
+ else
443
+ return false
444
+ end
348
445
 
349
- if url =~ Sanitize::REGEX_PROTOCOL
350
- return @config[:protocols].include?($1.downcase)
351
- else
352
- return @config[:protocols].include?(:relative)
446
+ if url =~ Sanitize::REGEX_PROTOCOL
447
+ @config[:protocols].include?($1.downcase)
448
+ else
449
+ @config[:protocols].include?(:relative)
450
+ end
353
451
  end
354
452
 
355
- false
356
- end
357
-
358
- # Returns `true` if the given node is an image-related function and contains
359
- # only strings that use an allowlisted protocol.
360
- def valid_image?(node)
361
- return false unless node[:node] == :function
362
- return false unless node.key?(:name) && image_function?(node[:name].downcase)
363
- return false unless Array === node[:value]
453
+ # Returns `true` if the given node is an image-related function and contains
454
+ # only strings that use an allowlisted protocol.
455
+ def valid_image?(node)
456
+ return false unless node[:node] == :function
457
+ return false unless node.key?(:name) && image_function?(node[:name].downcase)
458
+ return false unless Array === node[:value]
364
459
 
365
- node[:value].each do |token|
460
+ node[:value].each do |token|
366
461
  return false unless Hash === token
367
462
 
368
463
  case token[:node]
369
- when :string
370
- if token[:value] =~ Sanitize::REGEX_PROTOCOL
371
- return false unless @config[:protocols].include?($1.downcase)
372
- else
373
- return false unless @config[:protocols].include?(:relative)
374
- end
464
+ when :string
465
+ if token[:value] =~ Sanitize::REGEX_PROTOCOL
466
+ return false unless @config[:protocols].include?($1.downcase)
375
467
  else
376
- next
468
+ return false unless @config[:protocols].include?(:relative)
469
+ end
470
+ else
471
+ next
377
472
  end
378
473
  end
474
+ end
379
475
  end
380
-
381
- end; end
476
+ end