sanitize 6.1.3 → 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,331 +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
- # 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
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
237
244
 
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
245
+ url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string }
242
246
 
243
- url_token = rule[:tokens].detect { |t| t[:node] == :url || t[:node] == :string }
247
+ # don't allow @imports with no URL value
248
+ return false unless url_token && (import_url = url_token[:value])
244
249
 
245
- # don't allow @imports with no URL value
246
- return false unless url_token && (import_url = url_token[:value])
250
+ @import_url_validator.call(import_url)
251
+ end
247
252
 
248
- @import_url_validator.call(import_url)
249
- end
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
250
257
 
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
258
+ # Preserve IE * and _ hacks if desired.
259
+ if @config[:allow_hacks]
260
+ name.slice!(0) if /\A[*_]/.match?(name)
261
+ end
255
262
 
256
- # Preserve IE * and _ hacks if desired.
257
- if @config[:allow_hacks]
258
- name.slice!(0) if name =~ /\A[*_]/
259
- end
263
+ return nil unless @config[:properties].include?(name)
260
264
 
261
- return nil unless @config[:properties].include?(name)
265
+ nodes = prop[:children].dup
266
+ combined_value = +""
262
267
 
263
- nodes = prop[:children].dup
264
- combined_value = String.new
268
+ nodes.each do |child|
269
+ value = child[:value]
265
270
 
266
- nodes.each do |child|
267
- value = child[:value]
271
+ case child[:node]
272
+ when :ident
273
+ combined_value << value.downcase if String === value
268
274
 
269
- case child[:node]
270
- when :ident
271
- combined_value << value.downcase if String === value
275
+ when :function
276
+ if child.key?(:name)
277
+ name = child[:name].downcase
272
278
 
273
- when :function
274
- if child.key?(:name)
275
- name = child[:name].downcase
279
+ if name == "url"
280
+ return nil unless valid_url?(child)
281
+ end
276
282
 
277
- if name == 'url'
278
- 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"
279
289
  end
280
290
 
281
- if image_function?(name)
282
- 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"
283
297
  end
284
298
 
285
- combined_value << name
286
- return nil if name == 'expression' || combined_value == 'expression'
287
- end
299
+ when :url
300
+ return nil unless valid_url?(child)
288
301
 
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'
302
+ when :bad_url
303
+ return nil
295
304
  end
296
-
297
- when :url
298
- return nil unless valid_url?(child)
299
-
300
- when :bad_url
301
- return nil
302
305
  end
303
- end
304
306
 
305
- prop
306
- end
307
+ prop
308
+ end
307
309
 
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]
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]
313
315
 
314
- if type == :function
315
- return false unless node.key?(:name) && node[:name].downcase == 'url'
316
- 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]
317
319
 
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
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
324
326
 
325
- node[:value].each do |token|
326
- return false unless Hash === token
327
+ node[:value].each do |token|
328
+ return false unless Hash === token
327
329
 
328
- case token[:node]
330
+ case token[:node]
329
331
  when :string
330
332
  return false unless url_string_node.nil?
331
333
  url_string_node = token
@@ -335,47 +337,45 @@ class Sanitize; class CSS
335
337
 
336
338
  else
337
339
  return false
340
+ end
338
341
  end
339
- end
340
342
 
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
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
348
350
 
349
- if url =~ Sanitize::REGEX_PROTOCOL
350
- return @config[:protocols].include?($1.downcase)
351
- else
352
- 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
353
356
  end
354
357
 
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]
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]
364
364
 
365
- node[:value].each do |token|
365
+ node[:value].each do |token|
366
366
  return false unless Hash === token
367
367
 
368
368
  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
369
+ when :string
370
+ if token[:value] =~ Sanitize::REGEX_PROTOCOL
371
+ return false unless @config[:protocols].include?($1.downcase)
375
372
  else
376
- next
373
+ return false unless @config[:protocols].include?(:relative)
374
+ end
375
+ else
376
+ next
377
377
  end
378
378
  end
379
+ end
379
380
  end
380
-
381
- end; end
381
+ end