css_parser 1.16.0 → 3.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.
- checksums.yaml +4 -4
- data/lib/css_parser/parser.rb +226 -113
- data/lib/css_parser/regexps.rb +5 -3
- data/lib/css_parser/rule_set.rb +90 -46
- data/lib/css_parser/version.rb +1 -1
- data/lib/css_parser.rb +8 -12
- metadata +9 -109
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7286523850595059190f244936748ce609fc7d22de85ef56d5c5c229bd9a4ba4
|
|
4
|
+
data.tar.gz: 64fe512e21e1687c4221be94793eedbf603bd5723ef32a804177f70ba96e41b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63fa1631ae27fb97c375eb50d2e2b29f99e029872be0ad82191834818ad989470517c7d52cb77291a6a2a58fd920c3d1aa10a6250b5b569641016c77862b57cc
|
|
7
|
+
data.tar.gz: e6c24be4780b12aff38d866e5f0bc6b2b17deb78a94d2bb5072ec23bd82fcd70e1bcab63e4e02e915d2796e09dcaf9f5a239614c5c792d33003bb8ef0ca89d00
|
data/lib/css_parser/parser.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'strscan'
|
|
4
|
+
|
|
3
5
|
module CssParser
|
|
4
6
|
# Exception class used for any errors encountered while downloading remote files.
|
|
5
7
|
class RemoteFileError < IOError; end
|
|
@@ -15,9 +17,11 @@ module CssParser
|
|
|
15
17
|
# [<tt>absolute_paths</tt>] Convert relative paths to absolute paths (<tt>href</tt>, <tt>src</tt> and <tt>url('')</tt>. Boolean, default is <tt>false</tt>.
|
|
16
18
|
# [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
|
|
17
19
|
# [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
|
|
20
|
+
# [<tt>allow_local_network</tt>] Permit http(s) fetches against loopback / private / link-local / cloud-metadata addresses. Boolean, default is <tt>false</tt>. When <tt>false</tt> (the default), outbound HTTP requests are routed through <tt>ssrf_filter</tt>, which resolves the host and rejects unsafe IP ranges. Set to <tt>true</tt> only when the destination is known to be safe (e.g. local fixture servers in tests). Independent of <tt>allow_file_uris</tt>.
|
|
21
|
+
# [<tt>allow_file_uris</tt>] Permit <tt>file://</tt> URIs via <tt>load_uri!</tt>. Boolean, default is <tt>false</tt>. When <tt>false</tt> (the default), a caller that passes a <tt>file://</tt> URI to <tt>load_uri!</tt> — directly or via a CSS <tt>@import</tt> resolved against a <tt>file://</tt> base_uri — is refused, closing the local-file-disclosure vector when the URI is influenced by user input. <tt>load_file!</tt> is unaffected: it is the explicit local-file API and takes a caller-supplied path. Independent of <tt>allow_local_network</tt>.
|
|
18
22
|
class Parser
|
|
19
|
-
USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)"
|
|
20
|
-
|
|
23
|
+
USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)".freeze
|
|
24
|
+
RULESET_TOKENIZER_RX = /\s+|\\{2,}|\\?[{}\s"]|[()]|.[^\s"{}()\\]*/.freeze
|
|
21
25
|
STRIP_CSS_COMMENTS_RX = %r{/\*.*?\*/}m.freeze
|
|
22
26
|
STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze
|
|
23
27
|
|
|
@@ -26,27 +30,31 @@ module CssParser
|
|
|
26
30
|
|
|
27
31
|
MAX_REDIRECTS = 3
|
|
28
32
|
|
|
29
|
-
#
|
|
30
|
-
|
|
33
|
+
# Schemes accepted by `read_remote_file`. `file://` is intentionally
|
|
34
|
+
# NOT in this list — local files are handled directly by `load_uri!`
|
|
35
|
+
# and `load_file!`. Keeping `file://` out of the remote read path
|
|
36
|
+
# closes the cross-scheme redirect (HTTP 3xx → `file://`) vector that
|
|
37
|
+
# was GHSA-9pmc-p236-855h.
|
|
38
|
+
REMOTE_ALLOWED_SCHEMES = %w[http https].freeze
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
#++
|
|
35
|
-
@folded_declaration_cache = {}
|
|
36
|
-
class << self; attr_reader :folded_declaration_cache; end
|
|
40
|
+
# Array of CSS files that have been loaded.
|
|
41
|
+
attr_reader :loaded_uris
|
|
37
42
|
|
|
38
43
|
def initialize(options = {})
|
|
39
|
-
@options = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
@options = {
|
|
45
|
+
absolute_paths: false,
|
|
46
|
+
import: true,
|
|
47
|
+
io_exceptions: true,
|
|
48
|
+
rule_set_exceptions: true,
|
|
49
|
+
capture_offsets: false,
|
|
50
|
+
user_agent: USER_AGENT,
|
|
51
|
+
allow_local_network: false,
|
|
52
|
+
allow_file_uris: false
|
|
53
|
+
}.merge(options)
|
|
44
54
|
|
|
45
55
|
# array of RuleSets
|
|
46
56
|
@rules = []
|
|
47
57
|
|
|
48
|
-
@redirect_count = nil
|
|
49
|
-
|
|
50
58
|
@loaded_uris = []
|
|
51
59
|
|
|
52
60
|
# unprocessed blocks of CSS
|
|
@@ -131,14 +139,14 @@ module CssParser
|
|
|
131
139
|
block.scan(RE_AT_IMPORT_RULE).each do |import_rule|
|
|
132
140
|
media_types = []
|
|
133
141
|
if (media_string = import_rule[-1])
|
|
134
|
-
media_string.split(
|
|
142
|
+
media_string.split(',').each do |t|
|
|
135
143
|
media_types << CssParser.sanitize_media_query(t) unless t.empty?
|
|
136
144
|
end
|
|
137
145
|
else
|
|
138
146
|
media_types = [:all]
|
|
139
147
|
end
|
|
140
148
|
|
|
141
|
-
next unless options[:only_media_types].include?(:all) or media_types.empty? or
|
|
149
|
+
next unless options[:only_media_types].include?(:all) or media_types.empty? or media_types.intersect?(options[:only_media_types])
|
|
142
150
|
|
|
143
151
|
import_path = import_rule[0].to_s.gsub(/['"]*/, '').strip
|
|
144
152
|
|
|
@@ -162,14 +170,44 @@ module CssParser
|
|
|
162
170
|
parse_block_into_rule_sets!(block, options)
|
|
163
171
|
end
|
|
164
172
|
|
|
165
|
-
# Add a CSS rule by setting the +selectors+, +declarations+
|
|
173
|
+
# Add a CSS rule by setting the +selectors+, +declarations+
|
|
174
|
+
# and +media_types+. Optional pass +filename+ , +offset+ for source
|
|
175
|
+
# reference too.
|
|
166
176
|
#
|
|
167
|
-
# +media_types+ can be a symbol or an array of symbols.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
# +media_types+ can be a symbol or an array of symbols. default to :all
|
|
178
|
+
# optional fields for source location for source location
|
|
179
|
+
# +filename+ can be a string or uri pointing to the file or url location.
|
|
180
|
+
# +offset+ should be Range object representing the start and end byte locations where the rule was found in the file.
|
|
181
|
+
def add_rule!(*args, selectors: nil, block: nil, filename: nil, offset: nil, media_types: :all) # rubocop:disable Metrics/ParameterLists
|
|
182
|
+
if args.any?
|
|
183
|
+
media_types = nil
|
|
184
|
+
if selectors || block || filename || offset || media_types
|
|
185
|
+
raise ArgumentError, "don't mix positional and keyword arguments arguments"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
warn '[DEPRECATION] `add_rule!` with positional arguments is deprecated. ' \
|
|
189
|
+
'Please use keyword arguments instead.', uplevel: 1
|
|
190
|
+
|
|
191
|
+
case args.length
|
|
192
|
+
when 2
|
|
193
|
+
selectors, block = args
|
|
194
|
+
when 3
|
|
195
|
+
selectors, block, media_types = args
|
|
196
|
+
else
|
|
197
|
+
raise ArgumentError
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
begin
|
|
202
|
+
rule_set = RuleSet.new(
|
|
203
|
+
selectors: selectors, block: block,
|
|
204
|
+
offset: offset, filename: filename
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
add_rule_set!(rule_set, media_types)
|
|
208
|
+
rescue ArgumentError => e
|
|
209
|
+
raise e if @options[:rule_set_exceptions]
|
|
210
|
+
end
|
|
173
211
|
end
|
|
174
212
|
|
|
175
213
|
# Add a CSS rule by setting the +selectors+, +declarations+, +filename+, +offset+ and +media_types+.
|
|
@@ -178,8 +216,11 @@ module CssParser
|
|
|
178
216
|
# +offset+ should be Range object representing the start and end byte locations where the rule was found in the file.
|
|
179
217
|
# +media_types+ can be a symbol or an array of symbols.
|
|
180
218
|
def add_rule_with_offsets!(selectors, declarations, filename, offset, media_types = :all)
|
|
181
|
-
|
|
182
|
-
|
|
219
|
+
warn '[DEPRECATION] `add_rule_with_offsets!` is deprecated. Please use `add_rule!` instead.', uplevel: 1
|
|
220
|
+
add_rule!(
|
|
221
|
+
selectors: selectors, block: declarations, media_types: media_types,
|
|
222
|
+
filename: filename, offset: offset
|
|
223
|
+
)
|
|
183
224
|
end
|
|
184
225
|
|
|
185
226
|
# Add a CssParser RuleSet object.
|
|
@@ -321,17 +362,21 @@ module CssParser
|
|
|
321
362
|
in_at_media_rule = false
|
|
322
363
|
in_media_block = false
|
|
323
364
|
|
|
324
|
-
current_selectors =
|
|
325
|
-
current_media_query =
|
|
326
|
-
current_declarations =
|
|
365
|
+
current_selectors = +''
|
|
366
|
+
current_media_query = +''
|
|
367
|
+
current_declarations = +''
|
|
327
368
|
|
|
328
369
|
# once we are in a rule, we will use this to store where we started if we are capturing offsets
|
|
329
370
|
rule_start = nil
|
|
330
|
-
|
|
371
|
+
start_offset = nil
|
|
372
|
+
end_offset = nil
|
|
331
373
|
|
|
332
|
-
|
|
374
|
+
scanner = StringScanner.new(block)
|
|
375
|
+
until scanner.eos?
|
|
333
376
|
# save the regex offset so that we know where in the file we are
|
|
334
|
-
|
|
377
|
+
start_offset = scanner.pos
|
|
378
|
+
token = scanner.scan(RULESET_TOKENIZER_RX)
|
|
379
|
+
end_offset = scanner.pos
|
|
335
380
|
|
|
336
381
|
if token.start_with?('"') # found un-escaped double quote
|
|
337
382
|
in_string = !in_string
|
|
@@ -358,20 +403,24 @@ module CssParser
|
|
|
358
403
|
current_declarations.strip!
|
|
359
404
|
|
|
360
405
|
unless current_declarations.empty?
|
|
406
|
+
add_rule_options = {
|
|
407
|
+
selectors: current_selectors, block: current_declarations,
|
|
408
|
+
media_types: current_media_queries
|
|
409
|
+
}
|
|
361
410
|
if options[:capture_offsets]
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
add_rule!(current_selectors, current_declarations, current_media_queries)
|
|
411
|
+
add_rule_options[:filename] = options[:filename]
|
|
412
|
+
add_rule_options[:offset] = rule_start..end_offset
|
|
365
413
|
end
|
|
414
|
+
add_rule!(**add_rule_options)
|
|
366
415
|
end
|
|
367
416
|
|
|
368
|
-
current_selectors =
|
|
369
|
-
current_declarations =
|
|
417
|
+
current_selectors = +''
|
|
418
|
+
current_declarations = +''
|
|
370
419
|
|
|
371
420
|
# restart our search for selectors and declarations
|
|
372
421
|
rule_start = nil if options[:capture_offsets]
|
|
373
422
|
end
|
|
374
|
-
elsif
|
|
423
|
+
elsif /@media/i.match?(token)
|
|
375
424
|
# found '@media', reset current media_types
|
|
376
425
|
in_at_media_rule = true
|
|
377
426
|
current_media_queries = []
|
|
@@ -381,14 +430,14 @@ module CssParser
|
|
|
381
430
|
in_at_media_rule = false
|
|
382
431
|
in_media_block = true
|
|
383
432
|
current_media_queries << CssParser.sanitize_media_query(current_media_query)
|
|
384
|
-
current_media_query =
|
|
433
|
+
current_media_query = +''
|
|
385
434
|
elsif token.include?(',')
|
|
386
435
|
# new media query begins
|
|
387
436
|
token.tr!(',', ' ')
|
|
388
437
|
token.strip!
|
|
389
438
|
current_media_query << token << ' '
|
|
390
439
|
current_media_queries << CssParser.sanitize_media_query(current_media_query)
|
|
391
|
-
current_media_query =
|
|
440
|
+
current_media_query = +''
|
|
392
441
|
else
|
|
393
442
|
token.strip!
|
|
394
443
|
# special-case the ( and ) tokens to remove inner-whitespace
|
|
@@ -402,7 +451,7 @@ module CssParser
|
|
|
402
451
|
current_media_query << token << ' '
|
|
403
452
|
end
|
|
404
453
|
end
|
|
405
|
-
elsif in_charset or
|
|
454
|
+
elsif in_charset or /@charset/i.match?(token)
|
|
406
455
|
# iterate until we are out of the charset declaration
|
|
407
456
|
in_charset = !token.include?(';')
|
|
408
457
|
elsif !in_string && token.include?('}')
|
|
@@ -421,18 +470,22 @@ module CssParser
|
|
|
421
470
|
current_selectors << token
|
|
422
471
|
|
|
423
472
|
# mark this as the beginning of the selector unless we have already marked it
|
|
424
|
-
rule_start =
|
|
473
|
+
rule_start = start_offset if options[:capture_offsets] && rule_start.nil? && /^[^\s]+$/.match?(token)
|
|
425
474
|
end
|
|
426
475
|
end
|
|
427
476
|
|
|
428
477
|
# check for unclosed braces
|
|
429
478
|
return unless in_declarations > 0
|
|
430
479
|
|
|
431
|
-
|
|
432
|
-
|
|
480
|
+
add_rule_options = {
|
|
481
|
+
selectors: current_selectors, block: current_declarations,
|
|
482
|
+
media_types: current_media_queries
|
|
483
|
+
}
|
|
484
|
+
if options[:capture_offsets]
|
|
485
|
+
add_rule_options[:filename] = options[:filename]
|
|
486
|
+
add_rule_options[:offset] = rule_start..end_offset
|
|
433
487
|
end
|
|
434
|
-
|
|
435
|
-
add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
|
|
488
|
+
add_rule!(**add_rule_options)
|
|
436
489
|
end
|
|
437
490
|
|
|
438
491
|
# Load a remote CSS file.
|
|
@@ -450,6 +503,8 @@ module CssParser
|
|
|
450
503
|
if options.is_a? Hash
|
|
451
504
|
opts.merge!(options)
|
|
452
505
|
else
|
|
506
|
+
warn '[DEPRECATION] `load_uri!` with positional arguments is deprecated. ' \
|
|
507
|
+
'Please use keyword arguments instead.', uplevel: 1
|
|
453
508
|
opts[:base_uri] = options if options.is_a? String
|
|
454
509
|
opts[:media_types] = deprecated if deprecated
|
|
455
510
|
end
|
|
@@ -464,7 +519,28 @@ module CssParser
|
|
|
464
519
|
# pass on the uri if we are capturing file offsets
|
|
465
520
|
opts[:filename] = uri.to_s if opts[:capture_offsets]
|
|
466
521
|
|
|
467
|
-
|
|
522
|
+
# file:// is handled here, not inside read_remote_file. The
|
|
523
|
+
# remote-read path must never service file:// URIs, so a 3xx
|
|
524
|
+
# `Location: file://...` redirect cannot be turned into a local
|
|
525
|
+
# File.read.
|
|
526
|
+
#
|
|
527
|
+
# file:// via `load_uri!` is also gated by `allow_file_uris`:
|
|
528
|
+
# an attacker who can influence a URI passed here (e.g. via a CSS
|
|
529
|
+
# @import resolved against an attacker-controlled base_uri) could
|
|
530
|
+
# otherwise turn it into arbitrary local file disclosure. Callers
|
|
531
|
+
# that legitimately need to load local files should use
|
|
532
|
+
# `load_file!` (the explicit local-file API).
|
|
533
|
+
src = if uri.scheme == 'file'
|
|
534
|
+
unless @options[:allow_file_uris]
|
|
535
|
+
raise RemoteFileError, uri.to_s if @options[:io_exceptions]
|
|
536
|
+
|
|
537
|
+
return
|
|
538
|
+
end
|
|
539
|
+
read_local_file(uri)
|
|
540
|
+
else
|
|
541
|
+
src_and_charset, = read_remote_file(uri) # skip charset
|
|
542
|
+
src_and_charset
|
|
543
|
+
end
|
|
468
544
|
|
|
469
545
|
add_block!(src, opts) if src
|
|
470
546
|
end
|
|
@@ -476,6 +552,8 @@ module CssParser
|
|
|
476
552
|
if options.is_a? Hash
|
|
477
553
|
opts.merge!(options)
|
|
478
554
|
else
|
|
555
|
+
warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \
|
|
556
|
+
'Please use keyword arguments instead.', uplevel: 1
|
|
479
557
|
opts[:base_dir] = options if options.is_a? String
|
|
480
558
|
opts[:media_types] = deprecated if deprecated
|
|
481
559
|
end
|
|
@@ -484,7 +562,7 @@ module CssParser
|
|
|
484
562
|
return unless File.readable?(file_name)
|
|
485
563
|
return unless circular_reference_check(file_name)
|
|
486
564
|
|
|
487
|
-
src =
|
|
565
|
+
src = File.read(file_name)
|
|
488
566
|
|
|
489
567
|
opts[:filename] = file_name if opts[:capture_offsets]
|
|
490
568
|
opts[:base_dir] = File.dirname(file_name)
|
|
@@ -499,6 +577,8 @@ module CssParser
|
|
|
499
577
|
if options.is_a? Hash
|
|
500
578
|
opts.merge!(options)
|
|
501
579
|
else
|
|
580
|
+
warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \
|
|
581
|
+
'Please use keyword arguments instead.', uplevel: 1
|
|
502
582
|
opts[:base_dir] = options if options.is_a? String
|
|
503
583
|
opts[:media_types] = deprecated if deprecated
|
|
504
584
|
end
|
|
@@ -512,7 +592,8 @@ module CssParser
|
|
|
512
592
|
#
|
|
513
593
|
# Raises a CircularReferenceError exception if io_exceptions are on,
|
|
514
594
|
# otherwise returns true/false.
|
|
515
|
-
|
|
595
|
+
# TODO: fix rubocop
|
|
596
|
+
def circular_reference_check(path) # rubocop:disable Naming/PredicateMethod
|
|
516
597
|
path = path.to_s
|
|
517
598
|
if @loaded_uris.include?(path)
|
|
518
599
|
raise CircularReferenceError, "can't load #{path} more than once" if @options[:io_exceptions]
|
|
@@ -553,92 +634,124 @@ module CssParser
|
|
|
553
634
|
utf8_block
|
|
554
635
|
end
|
|
555
636
|
|
|
556
|
-
#
|
|
637
|
+
# Read a local file:// URI. Called only from `load_uri!` — never
|
|
638
|
+
# from the remote read path — so an HTTP redirect cannot reach this
|
|
639
|
+
# branch (GHSA-9pmc-p236-855h).
|
|
640
|
+
def read_local_file(uri) # :nodoc:
|
|
641
|
+
# Internal invariant: this method is the implementation of the
|
|
642
|
+
# `allow_file_uris: true` branch of `load_uri!`. If it is ever
|
|
643
|
+
# reached without that flag set, a future change has bypassed the
|
|
644
|
+
# LFI gate; refuse to read rather than silently leak.
|
|
645
|
+
unless @options[:allow_file_uris]
|
|
646
|
+
raise "BUG: #{self.class}##{__method__} reached with " \
|
|
647
|
+
'allow_file_uris=false (LFI gate bypassed)'
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
return nil unless circular_reference_check(uri.to_s)
|
|
651
|
+
|
|
652
|
+
path = uri.path
|
|
653
|
+
path.gsub!(%r{^/}, '') if Gem.win_platform?
|
|
654
|
+
File.read(path, mode: 'rb')
|
|
655
|
+
rescue
|
|
656
|
+
raise RemoteFileError, uri.to_s if @options[:io_exceptions]
|
|
657
|
+
|
|
658
|
+
nil
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# Download a remote http(s) file into a string.
|
|
557
662
|
#
|
|
558
663
|
# Returns the file's data and character set in an array.
|
|
664
|
+
#
|
|
665
|
+
# In the default (secure) configuration, requests are issued via
|
|
666
|
+
# `SsrfFilter.get`, which:
|
|
667
|
+
# - rejects any scheme other than http/https (defeats redirect-to-
|
|
668
|
+
# `file://` / `gopher://` / `dict://` etc.);
|
|
669
|
+
# - resolves the hostname with `Resolv` and rejects requests whose
|
|
670
|
+
# resolved IP is loopback, RFC-1918, link-local, multicast, or any
|
|
671
|
+
# other range typically used for internal services (defeats SSRF
|
|
672
|
+
# via literal IPs and via CNAME / attacker-controlled A records);
|
|
673
|
+
# - re-validates scheme and IP on every redirect hop.
|
|
674
|
+
#
|
|
675
|
+
# When `allow_local_network: true` is set on the Parser, the SSRF
|
|
676
|
+
# check is bypassed and plain `Net::HTTP` is used — but the scheme
|
|
677
|
+
# is still validated on every redirect hop, so cross-scheme
|
|
678
|
+
# redirect to `file://` (the original GHSA-9pmc-p236-855h sink)
|
|
679
|
+
# remains closed even on this opt-in path.
|
|
559
680
|
#--
|
|
560
681
|
# TODO: add option to fail silently or throw and exception on a 404
|
|
561
682
|
#++
|
|
562
683
|
def read_remote_file(uri) # :nodoc:
|
|
563
|
-
|
|
564
|
-
@redirect_count = 0
|
|
565
|
-
else
|
|
566
|
-
@redirect_count += 1
|
|
567
|
-
end
|
|
684
|
+
uri = Addressable::URI.parse(uri.to_s)
|
|
568
685
|
|
|
569
686
|
unless circular_reference_check(uri.to_s)
|
|
570
|
-
@redirect_count = nil
|
|
571
687
|
return nil, nil
|
|
572
688
|
end
|
|
573
689
|
|
|
574
|
-
|
|
575
|
-
|
|
690
|
+
unless REMOTE_ALLOWED_SCHEMES.include?(uri.scheme)
|
|
691
|
+
raise RemoteFileError, uri.to_s if @options[:io_exceptions]
|
|
692
|
+
|
|
576
693
|
return nil, nil
|
|
577
694
|
end
|
|
578
695
|
|
|
579
|
-
src = '', charset = nil
|
|
580
|
-
|
|
581
696
|
begin
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
uri.port = 443 unless uri.port
|
|
593
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
594
|
-
http.use_ssl = true
|
|
595
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
596
|
-
else
|
|
597
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
598
|
-
end
|
|
599
|
-
|
|
600
|
-
res = http.get(uri.request_uri, {'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip'})
|
|
601
|
-
src = res.body
|
|
602
|
-
charset = res.respond_to?(:charset) ? res.encoding : 'utf-8'
|
|
603
|
-
|
|
604
|
-
if res.code.to_i >= 400
|
|
605
|
-
@redirect_count = nil
|
|
606
|
-
raise RemoteFileError, uri.to_s if @options[:io_exceptions]
|
|
697
|
+
res = if @options[:allow_local_network]
|
|
698
|
+
fetch_via_net_http(uri)
|
|
699
|
+
else
|
|
700
|
+
SsrfFilter.get(
|
|
701
|
+
uri.to_s,
|
|
702
|
+
scheme_whitelist: REMOTE_ALLOWED_SCHEMES,
|
|
703
|
+
max_redirects: MAX_REDIRECTS,
|
|
704
|
+
headers: {'User-Agent' => @options[:user_agent]}
|
|
705
|
+
)
|
|
706
|
+
end
|
|
607
707
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
unless res['Location'].nil?
|
|
611
|
-
return read_remote_file Addressable::URI.parse(Addressable::URI.escape(res['Location']))
|
|
612
|
-
end
|
|
613
|
-
end
|
|
708
|
+
if res.code.to_i >= 400
|
|
709
|
+
raise RemoteFileError, uri.to_s if @options[:io_exceptions]
|
|
614
710
|
|
|
615
|
-
|
|
616
|
-
when 'gzip'
|
|
617
|
-
io = Zlib::GzipReader.new(StringIO.new(res.body))
|
|
618
|
-
src = io.read
|
|
619
|
-
when 'deflate'
|
|
620
|
-
io = Zlib::Inflate.new
|
|
621
|
-
src = io.inflate(res.body)
|
|
622
|
-
end
|
|
711
|
+
return '', nil
|
|
623
712
|
end
|
|
624
713
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
src = ic.iconv(src)
|
|
631
|
-
end
|
|
632
|
-
end
|
|
714
|
+
charset = res.respond_to?(:charset) ? res.encoding : 'utf-8'
|
|
715
|
+
src = res.body
|
|
716
|
+
src.encode!('UTF-8', charset) if charset
|
|
717
|
+
|
|
718
|
+
[src, charset]
|
|
633
719
|
rescue
|
|
634
|
-
@redirect_count = nil
|
|
635
720
|
raise RemoteFileError, uri.to_s if @options[:io_exceptions]
|
|
636
721
|
|
|
637
|
-
|
|
722
|
+
[nil, nil]
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
# Net::HTTP path used only when `allow_local_network: true`. Validates
|
|
727
|
+
# the URI scheme on every redirect hop so a `Location: file://...`
|
|
728
|
+
# cannot be followed even on this opt-in code path.
|
|
729
|
+
def fetch_via_net_http(uri, redirect_count = 0) # :nodoc:
|
|
730
|
+
# Internal invariant: this method is the implementation of the
|
|
731
|
+
# `allow_local_network: true` branch of `read_remote_file`. If it
|
|
732
|
+
# is ever reached without that flag set, a future change has
|
|
733
|
+
# bypassed the SSRF gate; refuse to fetch rather than silently
|
|
734
|
+
# connect. The recursive call on a redirect inherits this guard
|
|
735
|
+
# because the option does not change mid-request.
|
|
736
|
+
unless @options[:allow_local_network]
|
|
737
|
+
raise "BUG: #{self.class}##{__method__} reached with " \
|
|
738
|
+
'allow_local_network=false (SSRF gate bypassed)'
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
raise RemoteFileError, uri.to_s unless REMOTE_ALLOWED_SCHEMES.include?(uri.scheme)
|
|
742
|
+
raise RemoteFileError, uri.to_s if redirect_count > MAX_REDIRECTS
|
|
743
|
+
|
|
744
|
+
http = Net::HTTP.new(uri.host, uri.port || uri.default_port)
|
|
745
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
746
|
+
|
|
747
|
+
res = http.get(uri.request_uri, {'User-Agent' => @options[:user_agent]})
|
|
748
|
+
|
|
749
|
+
if res.code.to_i >= 300 && res.code.to_i < 400 && res['Location']
|
|
750
|
+
redirect_uri = Addressable::URI.parse(Addressable::URI.escape(res['Location']))
|
|
751
|
+
return fetch_via_net_http(redirect_uri, redirect_count + 1)
|
|
638
752
|
end
|
|
639
753
|
|
|
640
|
-
|
|
641
|
-
[src, charset]
|
|
754
|
+
res
|
|
642
755
|
end
|
|
643
756
|
|
|
644
757
|
private
|
|
@@ -669,7 +782,7 @@ module CssParser
|
|
|
669
782
|
nodes = {}
|
|
670
783
|
lines.each do |line|
|
|
671
784
|
parts = line.split(':', 2)
|
|
672
|
-
if parts[1]
|
|
785
|
+
if parts[1].include?(':')
|
|
673
786
|
nodes[parts[0]] = css_node_to_h(hash, parts[0], parts[1])
|
|
674
787
|
else
|
|
675
788
|
nodes[parts[0].to_s.strip] = parts[1].to_s.strip
|
data/lib/css_parser/regexps.rb
CHANGED
|
@@ -11,7 +11,7 @@ module CssParser
|
|
|
11
11
|
RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE | Regexp::NOENCODING) # [^\0-\177]
|
|
12
12
|
RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE | Regexp::NOENCODING)
|
|
13
13
|
RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])')
|
|
14
|
-
RE_IDENT = Regexp.new("[
|
|
14
|
+
RE_IDENT = Regexp.new("[-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE | Regexp::NOENCODING)
|
|
15
15
|
|
|
16
16
|
# General strings
|
|
17
17
|
RE_STRING1 = /("(.[^\n\r\f"]*|\\#{RE_NL}|#{RE_ESCAPE})*")/.freeze
|
|
@@ -259,8 +259,10 @@ module CssParser
|
|
|
259
259
|
inherit
|
|
260
260
|
currentColor
|
|
261
261
|
].freeze
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
# CSS <number> allows the integer part to be omitted (e.g. `.1`), per CSS Values & Units.
|
|
263
|
+
# `(?:\d*\.)?\d+` accepts `1`, `1.5`, and `.5` while still rejecting bare `1.`.
|
|
264
|
+
RE_COLOUR_NUMERIC = /\b(hsl|rgb)\s*\(-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?\)/i.freeze
|
|
265
|
+
RE_COLOUR_NUMERIC_ALPHA = /\b(hsla|rgba)\s*\(-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?\)/i.freeze
|
|
264
266
|
RE_COLOUR_HEX = /\s*#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/.freeze
|
|
265
267
|
RE_COLOUR_NAMED = /\s*\b(#{NAMED_COLOURS.join('|')})\b/i.freeze
|
|
266
268
|
RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED)
|
data/lib/css_parser/rule_set.rb
CHANGED
|
@@ -11,8 +11,10 @@ module CssParser
|
|
|
11
11
|
BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment'].freeze
|
|
12
12
|
LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image'].freeze
|
|
13
13
|
FONT_STYLE_PROPERTIES = ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'].freeze
|
|
14
|
+
FONT_WEIGHT_PROPERTIES = ['font-style', 'font-weight', 'font-variant'].freeze
|
|
14
15
|
BORDER_STYLE_PROPERTIES = ['border-width', 'border-style', 'border-color'].freeze
|
|
15
16
|
BORDER_PROPERTIES = ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].freeze
|
|
17
|
+
DIMENSION_DIRECTIONS = [:top, :right, :bottom, :left].freeze
|
|
16
18
|
|
|
17
19
|
NUMBER_OF_DIMENSIONS = 4
|
|
18
20
|
|
|
@@ -26,6 +28,12 @@ module CssParser
|
|
|
26
28
|
|
|
27
29
|
WHITESPACE_REPLACEMENT = '___SPACE___'
|
|
28
30
|
|
|
31
|
+
# Tokens for parse_declarations!
|
|
32
|
+
COLON = ':'.freeze
|
|
33
|
+
SEMICOLON = ';'.freeze
|
|
34
|
+
LPAREN = '('.freeze
|
|
35
|
+
RPAREN = ')'.freeze
|
|
36
|
+
IMPORTANT = '!important'.freeze
|
|
29
37
|
class Declarations
|
|
30
38
|
class Value
|
|
31
39
|
attr_reader :value
|
|
@@ -58,7 +66,7 @@ module CssParser
|
|
|
58
66
|
|
|
59
67
|
extend Forwardable
|
|
60
68
|
|
|
61
|
-
def_delegators :declarations, :each
|
|
69
|
+
def_delegators :declarations, :each, :each_value
|
|
62
70
|
|
|
63
71
|
def initialize(declarations = {})
|
|
64
72
|
self.declarations = {}
|
|
@@ -81,17 +89,20 @@ module CssParser
|
|
|
81
89
|
# puts declarations['margin']
|
|
82
90
|
# => #<CssParser::RuleSet::Declarations::Value:0x00000000030c1838 @important=true, @order=2, @value="0px auto">
|
|
83
91
|
#
|
|
84
|
-
# If the property already exists its value will be over-written
|
|
92
|
+
# If the property already exists its value will be over-written unless it was !important and the new value
|
|
93
|
+
# is not !important.
|
|
85
94
|
# If the value is empty - property will be deleted
|
|
86
95
|
def []=(property, value)
|
|
87
96
|
property = normalize_property(property)
|
|
97
|
+
currently_important = declarations[property]&.important
|
|
88
98
|
|
|
89
|
-
if value.is_a?(Value)
|
|
99
|
+
if value.is_a?(Value) && (!currently_important || value.important)
|
|
90
100
|
declarations[property] = value
|
|
91
101
|
elsif value.to_s.strip.empty?
|
|
92
102
|
delete property
|
|
93
103
|
else
|
|
94
|
-
|
|
104
|
+
value = Value.new(value)
|
|
105
|
+
declarations[property] = value if !currently_important || value.important
|
|
95
106
|
end
|
|
96
107
|
rescue ArgumentError => e
|
|
97
108
|
raise e.exception, "#{property} #{e.message}"
|
|
@@ -142,7 +153,7 @@ module CssParser
|
|
|
142
153
|
|
|
143
154
|
if preserve_importance
|
|
144
155
|
importance = get_value(property).important
|
|
145
|
-
replacement_declarations.
|
|
156
|
+
replacement_declarations.each_value { |value| value.important = importance }
|
|
146
157
|
end
|
|
147
158
|
|
|
148
159
|
replacement_keys = declarations.keys
|
|
@@ -190,7 +201,7 @@ module CssParser
|
|
|
190
201
|
end
|
|
191
202
|
|
|
192
203
|
def to_s(options = {})
|
|
193
|
-
str = declarations.reduce(
|
|
204
|
+
str = declarations.reduce(+'') do |memo, (prop, value)|
|
|
194
205
|
importance = options[:force_important] || value.important ? ' !important' : ''
|
|
195
206
|
memo << "#{prop}: #{value.value}#{importance}; "
|
|
196
207
|
end
|
|
@@ -223,6 +234,12 @@ module CssParser
|
|
|
223
234
|
|
|
224
235
|
extend Forwardable
|
|
225
236
|
|
|
237
|
+
# optional field for storing source reference
|
|
238
|
+
# File offset range
|
|
239
|
+
attr_reader :offset
|
|
240
|
+
# the local or remote location
|
|
241
|
+
attr_accessor :filename
|
|
242
|
+
|
|
226
243
|
# Array of selector strings.
|
|
227
244
|
attr_reader :selectors
|
|
228
245
|
|
|
@@ -237,9 +254,38 @@ module CssParser
|
|
|
237
254
|
alias []= add_declaration!
|
|
238
255
|
alias remove_declaration! delete
|
|
239
256
|
|
|
240
|
-
def initialize(selectors, block,
|
|
257
|
+
def initialize(*args, selectors: nil, block: nil, offset: nil, filename: nil, specificity: nil) # rubocop:disable Metrics/ParameterLists
|
|
258
|
+
if args.any?
|
|
259
|
+
if selectors || block || offset || filename || specificity
|
|
260
|
+
raise ArgumentError, "don't mix positional and keyword arguments"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
warn '[DEPRECATION] positional arguments are deprecated use keyword instead.', uplevel: 1
|
|
264
|
+
|
|
265
|
+
case args.length
|
|
266
|
+
when 2
|
|
267
|
+
selectors, block = args
|
|
268
|
+
when 3
|
|
269
|
+
selectors, block, specificity = args
|
|
270
|
+
when 4
|
|
271
|
+
filename, offset, selectors, block = args
|
|
272
|
+
when 5
|
|
273
|
+
filename, offset, selectors, block, specificity = args
|
|
274
|
+
else
|
|
275
|
+
raise ArgumentError
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
241
279
|
@selectors = []
|
|
242
280
|
@specificity = specificity
|
|
281
|
+
|
|
282
|
+
unless offset.nil? == filename.nil?
|
|
283
|
+
raise ArgumentError, 'require both offset and filename or no offset and no filename'
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
@offset = offset
|
|
287
|
+
@filename = filename
|
|
288
|
+
|
|
243
289
|
parse_selectors!(selectors) if selectors
|
|
244
290
|
parse_declarations!(block)
|
|
245
291
|
end
|
|
@@ -308,7 +354,7 @@ module CssParser
|
|
|
308
354
|
|
|
309
355
|
replacement =
|
|
310
356
|
if value.match(CssParser::RE_INHERIT)
|
|
311
|
-
BACKGROUND_PROPERTIES.
|
|
357
|
+
BACKGROUND_PROPERTIES.to_h { |key| [key, 'inherit'] }
|
|
312
358
|
else
|
|
313
359
|
{
|
|
314
360
|
'background-image' => value.slice!(CssParser::RE_IMAGE),
|
|
@@ -414,17 +460,17 @@ module CssParser
|
|
|
414
460
|
else
|
|
415
461
|
font_props['font-family'] = m
|
|
416
462
|
end
|
|
417
|
-
elsif
|
|
418
|
-
|
|
463
|
+
elsif /normal|inherit/i.match?(m)
|
|
464
|
+
FONT_WEIGHT_PROPERTIES.each do |font_prop|
|
|
419
465
|
font_props[font_prop] ||= m
|
|
420
466
|
end
|
|
421
|
-
elsif
|
|
467
|
+
elsif /italic|oblique/i.match?(m)
|
|
422
468
|
font_props['font-style'] = m
|
|
423
|
-
elsif
|
|
469
|
+
elsif /small-caps/i.match?(m)
|
|
424
470
|
font_props['font-variant'] = m
|
|
425
|
-
elsif
|
|
471
|
+
elsif /[1-9]00$|bold|bolder|lighter/i.match?(m)
|
|
426
472
|
font_props['font-weight'] = m
|
|
427
|
-
elsif
|
|
473
|
+
elsif CssParser::FONT_UNITS_RX.match?(m)
|
|
428
474
|
if m.include?('/')
|
|
429
475
|
font_props['font-size'], font_props['line-height'] = m.split('/', 2)
|
|
430
476
|
else
|
|
@@ -447,8 +493,8 @@ module CssParser
|
|
|
447
493
|
value = declaration.value.dup
|
|
448
494
|
|
|
449
495
|
replacement =
|
|
450
|
-
if
|
|
451
|
-
LIST_STYLE_PROPERTIES.
|
|
496
|
+
if CssParser::RE_INHERIT.match?(value)
|
|
497
|
+
LIST_STYLE_PROPERTIES.to_h { |key| [key, 'inherit'] }
|
|
452
498
|
else
|
|
453
499
|
{
|
|
454
500
|
'list-style-type' => value.slice!(CssParser::RE_LIST_STYLE_TYPE),
|
|
@@ -513,15 +559,15 @@ module CssParser
|
|
|
513
559
|
#
|
|
514
560
|
# TODO: this is extremely similar to create_background_shorthand! and should be combined
|
|
515
561
|
def create_border_shorthand! # :nodoc:
|
|
516
|
-
values = BORDER_STYLE_PROPERTIES.
|
|
562
|
+
values = BORDER_STYLE_PROPERTIES.filter_map do |property|
|
|
517
563
|
next unless (declaration = declarations[property])
|
|
518
564
|
next if declaration.important
|
|
519
565
|
# can't merge if any value contains a space (i.e. has multiple values)
|
|
520
566
|
# we temporarily remove any spaces after commas for the check (inside rgba, etc...)
|
|
521
|
-
next if declaration.value.gsub(/,\s/, ',').strip
|
|
567
|
+
next if /\s/.match?(declaration.value.gsub(/,\s/, ',').strip)
|
|
522
568
|
|
|
523
569
|
declaration.value
|
|
524
|
-
end
|
|
570
|
+
end
|
|
525
571
|
|
|
526
572
|
return if values.size != BORDER_STYLE_PROPERTIES.size
|
|
527
573
|
|
|
@@ -538,7 +584,7 @@ module CssParser
|
|
|
538
584
|
return if declarations.size < NUMBER_OF_DIMENSIONS
|
|
539
585
|
|
|
540
586
|
DIMENSIONS.each do |property, dimensions|
|
|
541
|
-
values =
|
|
587
|
+
values = DIMENSION_DIRECTIONS.each_with_index.with_object({}) do |(side, index), result|
|
|
542
588
|
next unless (declaration = declarations[dimensions[index]])
|
|
543
589
|
|
|
544
590
|
result[side] = declaration.value
|
|
@@ -561,7 +607,7 @@ module CssParser
|
|
|
561
607
|
def create_font_shorthand! # :nodoc:
|
|
562
608
|
return unless FONT_STYLE_PROPERTIES.all? { |prop| declarations.key?(prop) }
|
|
563
609
|
|
|
564
|
-
new_value =
|
|
610
|
+
new_value = +''
|
|
565
611
|
['font-style', 'font-variant', 'font-weight'].each do |property|
|
|
566
612
|
unless declarations[property].value == 'normal'
|
|
567
613
|
new_value << declarations[property].value << ' '
|
|
@@ -598,7 +644,7 @@ module CssParser
|
|
|
598
644
|
return [:top] if values.values.uniq.count == 1
|
|
599
645
|
|
|
600
646
|
# `/* top | right | bottom | left */`
|
|
601
|
-
return
|
|
647
|
+
return DIMENSION_DIRECTIONS if values[:left] != values[:right]
|
|
602
648
|
|
|
603
649
|
# Vertical are the same & horizontal are the same, `/* vertical | horizontal */`
|
|
604
650
|
return [:top, :left] if values[:top] == values[:bottom]
|
|
@@ -612,20 +658,32 @@ module CssParser
|
|
|
612
658
|
return unless block
|
|
613
659
|
|
|
614
660
|
continuation = nil
|
|
615
|
-
block.split(
|
|
616
|
-
decs = (continuation ? continuation
|
|
617
|
-
if decs
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
value = matches[2]
|
|
623
|
-
add_declaration!(property, value)
|
|
624
|
-
continuation = nil
|
|
661
|
+
block.split(SEMICOLON) do |decs|
|
|
662
|
+
decs = (continuation ? "#{continuation};#{decs}" : decs)
|
|
663
|
+
if unmatched_open_parenthesis?(decs)
|
|
664
|
+
# Semicolon happened within parenthesis, so it is a part of the value
|
|
665
|
+
# the rest of the value is in the next segment
|
|
666
|
+
continuation = decs
|
|
667
|
+
next
|
|
625
668
|
end
|
|
669
|
+
|
|
670
|
+
next unless (colon = decs.index(COLON))
|
|
671
|
+
|
|
672
|
+
property = decs[0, colon]
|
|
673
|
+
value = decs[(colon + 1)..]
|
|
674
|
+
property.strip!
|
|
675
|
+
value.strip!
|
|
676
|
+
next if property.empty? || value.empty? || value.casecmp?(IMPORTANT)
|
|
677
|
+
|
|
678
|
+
add_declaration!(property, value)
|
|
679
|
+
continuation = nil
|
|
626
680
|
end
|
|
627
681
|
end
|
|
628
682
|
|
|
683
|
+
def unmatched_open_parenthesis?(declarations)
|
|
684
|
+
(lparen_index = declarations.index(LPAREN)) && !declarations.index(RPAREN, lparen_index)
|
|
685
|
+
end
|
|
686
|
+
|
|
629
687
|
#--
|
|
630
688
|
# TODO: way too simplistic
|
|
631
689
|
#++
|
|
@@ -650,18 +708,4 @@ module CssParser
|
|
|
650
708
|
end
|
|
651
709
|
end
|
|
652
710
|
end
|
|
653
|
-
|
|
654
|
-
class OffsetAwareRuleSet < RuleSet
|
|
655
|
-
# File offset range
|
|
656
|
-
attr_reader :offset
|
|
657
|
-
|
|
658
|
-
# the local or remote location
|
|
659
|
-
attr_accessor :filename
|
|
660
|
-
|
|
661
|
-
def initialize(filename, offset, selectors, block, specificity = nil)
|
|
662
|
-
super(selectors, block, specificity)
|
|
663
|
-
@offset = offset
|
|
664
|
-
@filename = filename
|
|
665
|
-
end
|
|
666
|
-
end
|
|
667
711
|
end
|
data/lib/css_parser/version.rb
CHANGED
data/lib/css_parser.rb
CHANGED
|
@@ -4,9 +4,7 @@ require 'addressable/uri'
|
|
|
4
4
|
require 'uri'
|
|
5
5
|
require 'net/https'
|
|
6
6
|
require 'digest/md5'
|
|
7
|
-
require '
|
|
8
|
-
require 'stringio'
|
|
9
|
-
require 'iconv' unless String.method_defined?(:encode)
|
|
7
|
+
require 'ssrf_filter'
|
|
10
8
|
|
|
11
9
|
require 'css_parser/version'
|
|
12
10
|
require 'css_parser/rule_set'
|
|
@@ -53,12 +51,10 @@ module CssParser
|
|
|
53
51
|
# TODO: declaration_hashes should be able to contain a RuleSet
|
|
54
52
|
# this should be a Class method
|
|
55
53
|
def self.merge(*rule_sets)
|
|
56
|
-
@folded_declaration_cache = {}
|
|
57
|
-
|
|
58
54
|
# in case called like CssParser.merge([rule_set, rule_set])
|
|
59
55
|
rule_sets.flatten! if rule_sets[0].is_a?(Array)
|
|
60
56
|
|
|
61
|
-
unless rule_sets.all?
|
|
57
|
+
unless rule_sets.all?(CssParser::RuleSet)
|
|
62
58
|
raise ArgumentError, 'all parameters must be CssParser::RuleSets.'
|
|
63
59
|
end
|
|
64
60
|
|
|
@@ -71,17 +67,17 @@ module CssParser
|
|
|
71
67
|
rule_set.expand_shorthand!
|
|
72
68
|
|
|
73
69
|
specificity = rule_set.specificity
|
|
74
|
-
specificity ||= rule_set.selectors.
|
|
70
|
+
specificity ||= rule_set.selectors.filter_map { |s| calculate_specificity(s) }.max || 0
|
|
75
71
|
|
|
76
72
|
rule_set.each_declaration do |property, value, is_important|
|
|
77
73
|
# Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order
|
|
78
|
-
if
|
|
74
|
+
if !properties.key?(property)
|
|
79
75
|
properties[property] = {value: value, specificity: specificity, is_important: is_important}
|
|
80
76
|
elsif is_important
|
|
81
|
-
if
|
|
77
|
+
if !properties[property][:is_important] || properties[property][:specificity] <= specificity
|
|
82
78
|
properties[property] = {value: value, specificity: specificity, is_important: is_important}
|
|
83
79
|
end
|
|
84
|
-
elsif properties[property][:specificity] < specificity
|
|
80
|
+
elsif properties[property][:specificity] < specificity || properties[property][:specificity] == specificity
|
|
85
81
|
unless properties[property][:is_important]
|
|
86
82
|
properties[property] = {value: value, specificity: specificity, is_important: is_important}
|
|
87
83
|
end
|
|
@@ -111,7 +107,7 @@ module CssParser
|
|
|
111
107
|
#++
|
|
112
108
|
def self.calculate_specificity(selector)
|
|
113
109
|
a = 0
|
|
114
|
-
b = selector.scan(
|
|
110
|
+
b = selector.scan('#').length
|
|
115
111
|
c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC).length
|
|
116
112
|
d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX_NC).length
|
|
117
113
|
|
|
@@ -139,7 +135,7 @@ module CssParser
|
|
|
139
135
|
css.gsub(URI_RX) do
|
|
140
136
|
uri = Regexp.last_match(1).to_s.gsub(/["']+/, '')
|
|
141
137
|
# Don't process URLs that are already absolute
|
|
142
|
-
unless uri.match(%r{^[a-z]+://}i)
|
|
138
|
+
unless uri.match?(%r{^[a-z]+://}i)
|
|
143
139
|
begin
|
|
144
140
|
uri = base_uri.join(uri)
|
|
145
141
|
rescue
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: css_parser
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex Dunae
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: addressable
|
|
@@ -25,117 +24,19 @@ dependencies:
|
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
25
|
version: '0'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
34
|
-
type: :development
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
41
|
-
- !ruby/object:Gem::Dependency
|
|
42
|
-
name: bump
|
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
|
44
|
-
requirements:
|
|
45
|
-
- - ">="
|
|
46
|
-
- !ruby/object:Gem::Version
|
|
47
|
-
version: '0'
|
|
48
|
-
type: :development
|
|
49
|
-
prerelease: false
|
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
-
requirements:
|
|
52
|
-
- - ">="
|
|
53
|
-
- !ruby/object:Gem::Version
|
|
54
|
-
version: '0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: maxitest
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - ">="
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
62
|
-
type: :development
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - ">="
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
69
|
-
- !ruby/object:Gem::Dependency
|
|
70
|
-
name: memory_profiler
|
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
|
72
|
-
requirements:
|
|
73
|
-
- - ">="
|
|
74
|
-
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0'
|
|
76
|
-
type: :development
|
|
77
|
-
prerelease: false
|
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
-
requirements:
|
|
80
|
-
- - ">="
|
|
81
|
-
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0'
|
|
83
|
-
- !ruby/object:Gem::Dependency
|
|
84
|
-
name: rake
|
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
|
86
|
-
requirements:
|
|
87
|
-
- - ">="
|
|
88
|
-
- !ruby/object:Gem::Version
|
|
89
|
-
version: '0'
|
|
90
|
-
type: :development
|
|
91
|
-
prerelease: false
|
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
-
requirements:
|
|
94
|
-
- - ">="
|
|
95
|
-
- !ruby/object:Gem::Version
|
|
96
|
-
version: '0'
|
|
97
|
-
- !ruby/object:Gem::Dependency
|
|
98
|
-
name: rubocop
|
|
27
|
+
name: ssrf_filter
|
|
99
28
|
requirement: !ruby/object:Gem::Requirement
|
|
100
29
|
requirements:
|
|
101
30
|
- - "~>"
|
|
102
31
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '1.
|
|
104
|
-
type: :
|
|
32
|
+
version: '1.5'
|
|
33
|
+
type: :runtime
|
|
105
34
|
prerelease: false
|
|
106
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
36
|
requirements:
|
|
108
37
|
- - "~>"
|
|
109
38
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '1.
|
|
111
|
-
- !ruby/object:Gem::Dependency
|
|
112
|
-
name: rubocop-rake
|
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
|
114
|
-
requirements:
|
|
115
|
-
- - ">="
|
|
116
|
-
- !ruby/object:Gem::Version
|
|
117
|
-
version: '0'
|
|
118
|
-
type: :development
|
|
119
|
-
prerelease: false
|
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
-
requirements:
|
|
122
|
-
- - ">="
|
|
123
|
-
- !ruby/object:Gem::Version
|
|
124
|
-
version: '0'
|
|
125
|
-
- !ruby/object:Gem::Dependency
|
|
126
|
-
name: webrick
|
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
|
128
|
-
requirements:
|
|
129
|
-
- - ">="
|
|
130
|
-
- !ruby/object:Gem::Version
|
|
131
|
-
version: '0'
|
|
132
|
-
type: :development
|
|
133
|
-
prerelease: false
|
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
-
requirements:
|
|
136
|
-
- - ">="
|
|
137
|
-
- !ruby/object:Gem::Version
|
|
138
|
-
version: '0'
|
|
39
|
+
version: '1.5'
|
|
139
40
|
description: A set of classes for parsing CSS in Ruby.
|
|
140
41
|
email: code@dunae.ca
|
|
141
42
|
executables: []
|
|
@@ -155,7 +56,7 @@ metadata:
|
|
|
155
56
|
changelog_uri: https://github.com/premailer/css_parser/blob/master/CHANGELOG.md
|
|
156
57
|
source_code_uri: https://github.com/premailer/css_parser
|
|
157
58
|
bug_tracker_uri: https://github.com/premailer/css_parser/issues
|
|
158
|
-
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
159
60
|
rdoc_options: []
|
|
160
61
|
require_paths:
|
|
161
62
|
- lib
|
|
@@ -163,15 +64,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
163
64
|
requirements:
|
|
164
65
|
- - ">="
|
|
165
66
|
- !ruby/object:Gem::Version
|
|
166
|
-
version: '
|
|
67
|
+
version: '3.3'
|
|
167
68
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
168
69
|
requirements:
|
|
169
70
|
- - ">="
|
|
170
71
|
- !ruby/object:Gem::Version
|
|
171
72
|
version: '0'
|
|
172
73
|
requirements: []
|
|
173
|
-
rubygems_version:
|
|
174
|
-
signing_key:
|
|
74
|
+
rubygems_version: 4.0.3
|
|
175
75
|
specification_version: 4
|
|
176
76
|
summary: Ruby CSS parser.
|
|
177
77
|
test_files: []
|