sanitize 6.1.2 → 7.0.0

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,325 +1,333 @@
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
+ # @return [Array] Sanitized Crass CSS parse tree.
70
+ def self.tree!(tree, config = {})
71
+ new(config).tree!(tree)
72
+ end
72
73
 
73
- # -- Instance Methods --------------------------------------------------------
74
+ # -- Instance Methods ------------------------------------------------------
74
75
 
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)
76
+ # Returns a new Sanitize::CSS object initialized with the settings in
77
+ # _config_.
78
+ def initialize(config = {})
79
+ @config = Config.merge(Config::DEFAULT[:css], config[:css] || config)
79
80
 
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
81
+ @at_rules = Set.new(@config[:at_rules])
82
+ @at_rules_with_properties = Set.new(@config[:at_rules_with_properties])
83
+ @at_rules_with_styles = Set.new(@config[:at_rules_with_styles])
84
+ @import_url_validator = @config[:import_url_validator]
85
+ end
85
86
 
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
87
+ # Sanitizes inline CSS style properties.
88
+ #
89
+ # This is most useful for sanitizing non-stylesheet fragments of CSS like
90
+ # you would find in the `style` attribute of an HTML element. To sanitize a
91
+ # full CSS stylesheet, use {#stylesheet}.
92
+ #
93
+ # @example
94
+ # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
95
+ # scss.properties("background: url(foo.png); color: #fff;")
96
+ #
97
+ # @return [String] Sanitized CSS properties.
98
+ def properties(css)
99
+ tree = Crass.parse_properties(css,
100
+ preserve_comments: @config[:allow_comments],
101
+ preserve_hacks: @config[:allow_hacks])
102
+
103
+ tree!(tree)
104
+ Crass::Parser.stringify(tree)
105
+ end
105
106
 
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
107
+ # Sanitizes a full CSS stylesheet.
108
+ #
109
+ # A stylesheet may include selectors, at-rules, and comments. To sanitize
110
+ # only inline style properties such as the contents of an HTML `style`
111
+ # attribute, use {#properties}.
112
+ #
113
+ # @example
114
+ # css = %[
115
+ # .foo {
116
+ # background: url(foo.png);
117
+ # color: #fff;
118
+ # }
119
+ #
120
+ # #bar {
121
+ # font: 42pt 'Comic Sans MS';
122
+ # }
123
+ # ]
124
+ #
125
+ # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
126
+ # scss.stylesheet(css)
127
+ #
128
+ # @return [String] Sanitized CSS stylesheet.
129
+ def stylesheet(css)
130
+ tree = Crass.parse(css,
131
+ preserve_comments: @config[:allow_comments],
132
+ preserve_hacks: @config[:allow_hacks])
133
+
134
+ tree!(tree)
135
+ Crass::Parser.stringify(tree)
136
+ end
137
+
138
+ # Sanitizes the given Crass CSS parse tree and all its children, modifying
139
+ # it in place.
140
+ #
141
+ # @example
142
+ # css = %[
143
+ # .foo {
144
+ # background: url(foo.png);
145
+ # color: #fff;
146
+ # }
147
+ #
148
+ # #bar {
149
+ # font: 42pt 'Comic Sans MS';
150
+ # }
151
+ # ]
152
+ #
153
+ # scss = Sanitize::CSS.new(Sanitize::Config::RELAXED)
154
+ # tree = Crass.parse(css)
155
+ #
156
+ # scss.tree!(tree)
157
+ #
158
+ # @return [Array] Sanitized Crass CSS parse tree.
159
+ def tree!(tree)
160
+ preceded_by_property = false
161
+
162
+ tree.map! do |node|
163
+ next nil if node.nil?
164
+
165
+ case node[:node]
166
+ when :at_rule
167
+ preceded_by_property = false
168
+ next at_rule!(node)
169
+
170
+ when :comment
171
+ next node if @config[:allow_comments]
172
+
173
+ when :property
174
+ prop = property!(node)
175
+ preceded_by_property = !prop.nil?
176
+ next prop
177
+
178
+ when :semicolon
179
+ # Only preserve the semicolon if it was preceded by an allowlisted
180
+ # property. Otherwise, omit it in order to prevent redundant
181
+ # semicolons.
182
+ if preceded_by_property
183
+ preceded_by_property = false
184
+ next node
185
+ end
136
186
 
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
187
+ when :style_rule
181
188
  preceded_by_property = false
189
+ tree!(node[:children])
182
190
  next node
183
- end
184
191
 
185
- when :style_rule
186
- preceded_by_property = false
187
- tree!(node[:children])
188
- next node
192
+ when :whitespace
193
+ next node
194
+ end
189
195
 
190
- when :whitespace
191
- next node
196
+ nil
192
197
  end
193
198
 
194
- nil
199
+ tree
195
200
  end
196
201
 
197
- tree
198
- end
202
+ # -- Protected Instance Methods --------------------------------------------
203
+ protected
199
204
 
200
- # -- Protected Instance Methods ----------------------------------------------
201
- protected
205
+ # Sanitizes a CSS at-rule node. Returns the sanitized node, or `nil` if the
206
+ # current config doesn't allow this at-rule.
207
+ def at_rule!(rule)
208
+ name = rule[:name].downcase
202
209
 
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
210
+ if @at_rules_with_styles.include?(name)
211
+ styles = Crass::Parser.parse_rules(rule[:block],
212
+ preserve_comments: @config[:allow_comments],
213
+ preserve_hacks: @config[:allow_hacks])
207
214
 
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])
215
+ rule[:block] = tree!(styles)
212
216
 
213
- rule[:block] = tree!(styles)
217
+ elsif @at_rules_with_properties.include?(name)
218
+ props = Crass::Parser.parse_properties(rule[:block],
219
+ preserve_comments: @config[:allow_comments],
220
+ preserve_hacks: @config[:allow_hacks])
214
221
 
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])
222
+ rule[:block] = tree!(props)
219
223
 
220
- rule[:block] = tree!(props)
224
+ elsif @at_rules.include?(name)
225
+ return nil if name == "import" && !import_url_allowed?(rule)
226
+ return nil if rule.has_key?(:block)
227
+ else
228
+ return nil
229
+ end
221
230
 
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
231
+ rule
227
232
  end
228
233
 
229
- rule
230
- end
234
+ # Returns `true` if the given CSS function name is an image-related function
235
+ # that may contain image URLs that need to be validated.
236
+ def image_function?(name)
237
+ ["image", "image-set", "-webkit-image-set"].include?(name)
238
+ end
231
239
 
232
- # Passes the URL value of an @import rule to a block to ensure
233
- # it's an allowed URL
234
- def import_url_allowed?(rule)
235
- return true unless @import_url_validator
240
+ # Passes the URL value of an @import rule to a block to ensure
241
+ # it's an allowed URL
242
+ def import_url_allowed?(rule)
243
+ return true unless @import_url_validator
236
244
 
237
- url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string }
245
+ url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string }
238
246
 
239
- # don't allow @imports with no URL value
240
- return false unless url_token && (import_url = url_token[:value])
247
+ # don't allow @imports with no URL value
248
+ return false unless url_token && (import_url = url_token[:value])
241
249
 
242
- @import_url_validator.call(import_url)
243
- end
250
+ @import_url_validator.call(import_url)
251
+ end
244
252
 
245
- # Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the
246
- # current config doesn't allow this property.
247
- def property!(prop)
248
- name = prop[:name].downcase
253
+ # Sanitizes a CSS property node. Returns the sanitized node, or `nil` if the
254
+ # current config doesn't allow this property.
255
+ def property!(prop)
256
+ name = prop[:name].downcase
249
257
 
250
- # Preserve IE * and _ hacks if desired.
251
- if @config[:allow_hacks]
252
- name.slice!(0) if name =~ /\A[*_]/
253
- end
258
+ # Preserve IE * and _ hacks if desired.
259
+ if @config[:allow_hacks]
260
+ name.slice!(0) if /\A[*_]/.match?(name)
261
+ end
262
+
263
+ return nil unless @config[:properties].include?(name)
254
264
 
255
- return nil unless @config[:properties].include?(name)
265
+ nodes = prop[:children].dup
266
+ combined_value = +""
256
267
 
257
- nodes = prop[:children].dup
258
- combined_value = String.new
268
+ nodes.each do |child|
269
+ value = child[:value]
259
270
 
260
- nodes.each do |child|
261
- value = child[:value]
271
+ case child[:node]
272
+ when :ident
273
+ combined_value << value.downcase if String === value
262
274
 
263
- case child[:node]
264
- when :ident
265
- combined_value << value.downcase if String === value
275
+ when :function
276
+ if child.key?(:name)
277
+ name = child[:name].downcase
266
278
 
267
- when :function
268
- if child.key?(:name)
269
- name = child[:name].downcase
279
+ if name == "url"
280
+ return nil unless valid_url?(child)
281
+ end
270
282
 
271
- if name == 'url'
272
- return nil unless valid_url?(child)
283
+ if image_function?(name)
284
+ return nil unless valid_image?(child)
285
+ end
286
+
287
+ combined_value << name
288
+ return nil if name == "expression" || combined_value == "expression"
273
289
  end
274
290
 
275
- if name == 'image-set' || name == 'image'
276
- return nil unless valid_image?(child)
291
+ if Array === value
292
+ nodes.concat(value)
293
+ elsif String === value
294
+ lowercase_value = value.downcase
295
+ combined_value << lowercase_value
296
+ return nil if lowercase_value == "expression" || combined_value == "expression"
277
297
  end
278
298
 
279
- combined_value << name
280
- return nil if name == 'expression' || combined_value == 'expression'
281
- end
299
+ when :url
300
+ return nil unless valid_url?(child)
282
301
 
283
- if Array === value
284
- nodes.concat(value)
285
- elsif String === value
286
- lowercase_value = value.downcase
287
- combined_value << lowercase_value
288
- return nil if lowercase_value == 'expression' || combined_value == 'expression'
302
+ when :bad_url
303
+ return nil
289
304
  end
290
-
291
- when :url
292
- return nil unless valid_url?(child)
293
-
294
- when :bad_url
295
- return nil
296
305
  end
297
- end
298
306
 
299
- prop
300
- end
307
+ prop
308
+ end
301
309
 
302
- # Returns `true` if the given node (which may be of type `:url` or
303
- # `:function`, since the CSS syntax can produce both) uses an allowlisted
304
- # protocol.
305
- def valid_url?(node)
306
- type = node[:node]
310
+ # Returns `true` if the given node (which may be of type `:url` or
311
+ # `:function`, since the CSS syntax can produce both) uses an allowlisted
312
+ # protocol.
313
+ def valid_url?(node)
314
+ type = node[:node]
307
315
 
308
- if type == :function
309
- return false unless node.key?(:name) && node[:name].downcase == 'url'
310
- return false unless Array === node[:value]
316
+ if type == :function
317
+ return false unless node.key?(:name) && node[:name].downcase == "url"
318
+ return false unless Array === node[:value]
311
319
 
312
- # A URL function's `:value` should be an array containing no more than one
313
- # `:string` node and any number of `:whitespace` nodes.
314
- #
315
- # If it contains more than one `:string` node, or if it contains any other
316
- # nodes except `:whitespace` nodes, it's not valid.
317
- url_string_node = nil
320
+ # A URL function's `:value` should be an array containing no more than
321
+ # one `:string` node and any number of `:whitespace` nodes.
322
+ #
323
+ # If it contains more than one `:string` node, or if it contains any
324
+ # other nodes except `:whitespace` nodes, it's not valid.
325
+ url_string_node = nil
318
326
 
319
- node[:value].each do |token|
320
- return false unless Hash === token
327
+ node[:value].each do |token|
328
+ return false unless Hash === token
321
329
 
322
- case token[:node]
330
+ case token[:node]
323
331
  when :string
324
332
  return false unless url_string_node.nil?
325
333
  url_string_node = token
@@ -329,47 +337,45 @@ class Sanitize; class CSS
329
337
 
330
338
  else
331
339
  return false
340
+ end
332
341
  end
333
- end
334
342
 
335
- return false if url_string_node.nil?
336
- url = url_string_node[:value]
337
- elsif type == :url
338
- url = node[:value]
339
- else
340
- return false
341
- end
343
+ return false if url_string_node.nil?
344
+ url = url_string_node[:value]
345
+ elsif type == :url
346
+ url = node[:value]
347
+ else
348
+ return false
349
+ end
342
350
 
343
- if url =~ Sanitize::REGEX_PROTOCOL
344
- return @config[:protocols].include?($1.downcase)
345
- else
346
- return @config[:protocols].include?(:relative)
351
+ if url =~ Sanitize::REGEX_PROTOCOL
352
+ @config[:protocols].include?($1.downcase)
353
+ else
354
+ @config[:protocols].include?(:relative)
355
+ end
347
356
  end
348
357
 
349
- false
350
- end
351
-
352
- # Returns `true` if the given node (which is an `image` or `image-set` function) contains only strings
353
- # using an allowlisted protocol.
354
- def valid_image?(node)
355
- return false unless node[:node] == :function
356
- return false unless node.key?(:name) && ['image', 'image-set'].include?(node[:name].downcase)
357
- return false unless Array === node[:value]
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]
358
364
 
359
- node[:value].each do |token|
365
+ node[:value].each do |token|
360
366
  return false unless Hash === token
361
367
 
362
368
  case token[:node]
363
- when :string
364
- if token[:value] =~ Sanitize::REGEX_PROTOCOL
365
- return false unless @config[:protocols].include?($1.downcase)
366
- else
367
- return false unless @config[:protocols].include?(:relative)
368
- end
369
+ when :string
370
+ if token[:value] =~ Sanitize::REGEX_PROTOCOL
371
+ return false unless @config[:protocols].include?($1.downcase)
369
372
  else
370
- next
373
+ return false unless @config[:protocols].include?(:relative)
374
+ end
375
+ else
376
+ next
371
377
  end
372
378
  end
379
+ end
373
380
  end
374
-
375
- end; end
381
+ end