css_parser 1.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bad6c42b4f2fbda14292b503c3f381ea4b3a9a4b77c74dd44da724866237901
4
- data.tar.gz: 0b47ed4ef7d197a5552af3a99db42b89a8937cca205c7b6cf865cfb930d2d265
3
+ metadata.gz: 7286523850595059190f244936748ce609fc7d22de85ef56d5c5c229bd9a4ba4
4
+ data.tar.gz: 64fe512e21e1687c4221be94793eedbf603bd5723ef32a804177f70ba96e41b7
5
5
  SHA512:
6
- metadata.gz: e91f481e093e423b873fcfd673bda8337645b066a7c278bea28175f8e11d94d9ac1dc401fc7defa1f8afca11d9125f7007aa8c12e1a4410351511f6e876bf593
7
- data.tar.gz: 0fd7db359a459575bc2b7f08b5d6f86d5d78c58247af82db87884e5a1e10e768f9f3f0247abf2d491ebf30c9df8ec623fd65c8c51bf089fa29eba9cf75a9d9ac
6
+ metadata.gz: 63fa1631ae27fb97c375eb50d2e2b29f99e029872be0ad82191834818ad989470517c7d52cb77291a6a2a58fd920c3d1aa10a6250b5b569641016c77862b57cc
7
+ data.tar.gz: e6c24be4780b12aff38d866e5f0bc6b2b17deb78a94d2bb5072ec23bd82fcd70e1bcab63e4e02e915d2796e09dcaf9f5a239614c5c792d33003bb8ef0ca89d00
@@ -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
- # Array of CSS files that have been loaded.
30
- attr_reader :loaded_uris
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
- # Class variable? see http://www.oreillynet.com/ruby/blog/2007/01/nubygems_dont_use_class_variab_1.html
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 = {absolute_paths: false,
40
- import: true,
41
- io_exceptions: true,
42
- rule_set_exceptions: true,
43
- capture_offsets: false}.merge(options)
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(/,/).each do |t|
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 !(media_types & options[:only_media_types]).empty?
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+ and +media_types+.
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
- def add_rule!(selectors, declarations, media_types = :all)
169
- rule_set = RuleSet.new(selectors, declarations)
170
- add_rule_set!(rule_set, media_types)
171
- rescue ArgumentError => e
172
- raise e if @options[:rule_set_exceptions]
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
- rule_set = OffsetAwareRuleSet.new(filename, offset, selectors, declarations)
182
- add_rule_set!(rule_set, media_types)
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 = String.new
325
- current_media_query = String.new
326
- current_declarations = String.new
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
- offset = nil
371
+ start_offset = nil
372
+ end_offset = nil
331
373
 
332
- block.scan(/\s+|\\{2,}|\\?[{}\s"]|.[^\s"{}\\]*/) do |token|
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
- offset = Regexp.last_match.offset(0) if options[:capture_offsets]
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
- add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
363
- else
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 = String.new
369
- current_declarations = String.new
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 token =~ /@media/i
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,19 +430,28 @@ 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 = String.new
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 = String.new
440
+ current_media_query = +''
392
441
  else
393
442
  token.strip!
394
- current_media_query << token << ' '
443
+ # special-case the ( and ) tokens to remove inner-whitespace
444
+ # (eg we'd prefer '(width: 500px)' to '( width: 500px )' )
445
+ case token
446
+ when '('
447
+ current_media_query << token
448
+ when ')'
449
+ current_media_query.sub!(/ ?$/, token)
450
+ else
451
+ current_media_query << token << ' '
452
+ end
395
453
  end
396
- elsif in_charset or token =~ /@charset/i
454
+ elsif in_charset or /@charset/i.match?(token)
397
455
  # iterate until we are out of the charset declaration
398
456
  in_charset = !token.include?(';')
399
457
  elsif !in_string && token.include?('}')
@@ -412,18 +470,22 @@ module CssParser
412
470
  current_selectors << token
413
471
 
414
472
  # mark this as the beginning of the selector unless we have already marked it
415
- rule_start = offset.first if options[:capture_offsets] && rule_start.nil? && token =~ /^[^\s]+$/
473
+ rule_start = start_offset if options[:capture_offsets] && rule_start.nil? && /^[^\s]+$/.match?(token)
416
474
  end
417
475
  end
418
476
 
419
477
  # check for unclosed braces
420
478
  return unless in_declarations > 0
421
479
 
422
- unless options[:capture_offsets]
423
- return add_rule!(current_selectors, current_declarations, current_media_queries)
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
424
487
  end
425
-
426
- add_rule_with_offsets!(current_selectors, current_declarations, options[:filename], (rule_start..offset.last), current_media_queries)
488
+ add_rule!(**add_rule_options)
427
489
  end
428
490
 
429
491
  # Load a remote CSS file.
@@ -441,6 +503,8 @@ module CssParser
441
503
  if options.is_a? Hash
442
504
  opts.merge!(options)
443
505
  else
506
+ warn '[DEPRECATION] `load_uri!` with positional arguments is deprecated. ' \
507
+ 'Please use keyword arguments instead.', uplevel: 1
444
508
  opts[:base_uri] = options if options.is_a? String
445
509
  opts[:media_types] = deprecated if deprecated
446
510
  end
@@ -455,7 +519,28 @@ module CssParser
455
519
  # pass on the uri if we are capturing file offsets
456
520
  opts[:filename] = uri.to_s if opts[:capture_offsets]
457
521
 
458
- src, = read_remote_file(uri) # skip charset
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
459
544
 
460
545
  add_block!(src, opts) if src
461
546
  end
@@ -467,6 +552,8 @@ module CssParser
467
552
  if options.is_a? Hash
468
553
  opts.merge!(options)
469
554
  else
555
+ warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \
556
+ 'Please use keyword arguments instead.', uplevel: 1
470
557
  opts[:base_dir] = options if options.is_a? String
471
558
  opts[:media_types] = deprecated if deprecated
472
559
  end
@@ -475,7 +562,7 @@ module CssParser
475
562
  return unless File.readable?(file_name)
476
563
  return unless circular_reference_check(file_name)
477
564
 
478
- src = IO.read(file_name)
565
+ src = File.read(file_name)
479
566
 
480
567
  opts[:filename] = file_name if opts[:capture_offsets]
481
568
  opts[:base_dir] = File.dirname(file_name)
@@ -490,6 +577,8 @@ module CssParser
490
577
  if options.is_a? Hash
491
578
  opts.merge!(options)
492
579
  else
580
+ warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \
581
+ 'Please use keyword arguments instead.', uplevel: 1
493
582
  opts[:base_dir] = options if options.is_a? String
494
583
  opts[:media_types] = deprecated if deprecated
495
584
  end
@@ -503,7 +592,8 @@ module CssParser
503
592
  #
504
593
  # Raises a CircularReferenceError exception if io_exceptions are on,
505
594
  # otherwise returns true/false.
506
- def circular_reference_check(path)
595
+ # TODO: fix rubocop
596
+ def circular_reference_check(path) # rubocop:disable Naming/PredicateMethod
507
597
  path = path.to_s
508
598
  if @loaded_uris.include?(path)
509
599
  raise CircularReferenceError, "can't load #{path} more than once" if @options[:io_exceptions]
@@ -544,92 +634,124 @@ module CssParser
544
634
  utf8_block
545
635
  end
546
636
 
547
- # Download a file into a string.
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.
548
662
  #
549
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.
550
680
  #--
551
681
  # TODO: add option to fail silently or throw and exception on a 404
552
682
  #++
553
683
  def read_remote_file(uri) # :nodoc:
554
- if @redirect_count.nil?
555
- @redirect_count = 0
556
- else
557
- @redirect_count += 1
558
- end
684
+ uri = Addressable::URI.parse(uri.to_s)
559
685
 
560
686
  unless circular_reference_check(uri.to_s)
561
- @redirect_count = nil
562
687
  return nil, nil
563
688
  end
564
689
 
565
- if @redirect_count > MAX_REDIRECTS
566
- @redirect_count = nil
690
+ unless REMOTE_ALLOWED_SCHEMES.include?(uri.scheme)
691
+ raise RemoteFileError, uri.to_s if @options[:io_exceptions]
692
+
567
693
  return nil, nil
568
694
  end
569
695
 
570
- src = '', charset = nil
571
-
572
696
  begin
573
- uri = Addressable::URI.parse(uri.to_s)
574
-
575
- if uri.scheme == 'file'
576
- # local file
577
- path = uri.path
578
- path.gsub!(%r{^/}, '') if Gem.win_platform?
579
- src = File.read(path, mode: 'rb')
580
- else
581
- # remote file
582
- if uri.scheme == 'https'
583
- uri.port = 443 unless uri.port
584
- http = Net::HTTP.new(uri.host, uri.port)
585
- http.use_ssl = true
586
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
587
- else
588
- http = Net::HTTP.new(uri.host, uri.port)
589
- end
590
-
591
- res = http.get(uri.request_uri, {'User-Agent' => USER_AGENT, 'Accept-Encoding' => 'gzip'})
592
- src = res.body
593
- charset = res.respond_to?(:charset) ? res.encoding : 'utf-8'
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
594
707
 
595
- if res.code.to_i >= 400
596
- @redirect_count = nil
597
- raise RemoteFileError, uri.to_s if @options[:io_exceptions]
708
+ if res.code.to_i >= 400
709
+ raise RemoteFileError, uri.to_s if @options[:io_exceptions]
598
710
 
599
- return '', nil
600
- elsif res.code.to_i >= 300 and res.code.to_i < 400
601
- unless res['Location'].nil?
602
- return read_remote_file Addressable::URI.parse(Addressable::URI.escape(res['Location']))
603
- end
604
- end
605
-
606
- case res['content-encoding']
607
- when 'gzip'
608
- io = Zlib::GzipReader.new(StringIO.new(res.body))
609
- src = io.read
610
- when 'deflate'
611
- io = Zlib::Inflate.new
612
- src = io.inflate(res.body)
613
- end
711
+ return '', nil
614
712
  end
615
713
 
616
- if charset
617
- if String.method_defined?(:encode)
618
- src.encode!('UTF-8', charset)
619
- else
620
- ic = Iconv.new('UTF-8//IGNORE', charset)
621
- src = ic.iconv(src)
622
- end
623
- 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]
624
719
  rescue
625
- @redirect_count = nil
626
720
  raise RemoteFileError, uri.to_s if @options[:io_exceptions]
627
721
 
628
- return nil, nil
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)
629
752
  end
630
753
 
631
- @redirect_count = nil
632
- [src, charset]
754
+ res
633
755
  end
634
756
 
635
757
  private
@@ -660,7 +782,7 @@ module CssParser
660
782
  nodes = {}
661
783
  lines.each do |line|
662
784
  parts = line.split(':', 2)
663
- if parts[1] =~ /:/
785
+ if parts[1].include?(':')
664
786
  nodes[parts[0]] = css_node_to_h(hash, parts[0], parts[1])
665
787
  else
666
788
  nodes[parts[0].to_s.strip] = parts[1].to_s.strip
@@ -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("[\-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9\-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE | Regexp::NOENCODING)
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
@@ -52,8 +52,10 @@ module CssParser
52
52
  # Special units
53
53
  BOX_MODEL_UNITS_RX = /(auto|inherit|0|(-*([0-9]+|[0-9]*\.[0-9]+)(rem|vw|vh|vm|vmin|vmax|e[mx]+|px|[cm]+m|p[tc+]|in|%)))([\s;]|\Z)/imx.freeze
54
54
  RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
55
- RE_BACKGROUND_POSITION = Regexp.new("((((#{RE_LENGTH_OR_PERCENTAGE})|left|center|right|top|bottom)[\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
56
- RE_BACKGROUND_SIZE = Regexp.new("\\s*/\\s*((((#{RE_LENGTH_OR_PERCENTAGE})|auto|cover|contain|initial|inherit)[\\s]*){1,2})", Regexp::IGNORECASE | Regexp::EXTENDED)
55
+ RE_SINGLE_BACKGROUND_POSITION = /#{RE_LENGTH_OR_PERCENTAGE}|left|center|right|top|bottom/i.freeze
56
+ RE_SINGLE_BACKGROUND_SIZE = /#{RE_LENGTH_OR_PERCENTAGE}|auto|cover|contain|initial|inherit/i.freeze
57
+ RE_BACKGROUND_POSITION = /#{RE_SINGLE_BACKGROUND_POSITION}\s+#{RE_SINGLE_BACKGROUND_POSITION}|#{RE_SINGLE_BACKGROUND_POSITION}/.freeze
58
+ RE_BACKGROUND_SIZE = %r{\s*/\s*(#{RE_SINGLE_BACKGROUND_SIZE}\s+#{RE_SINGLE_BACKGROUND_SIZE}|#{RE_SINGLE_BACKGROUND_SIZE})}.freeze
57
59
  FONT_UNITS_RX = /((x+-)*small|medium|larger*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|%)*)/i.freeze
58
60
  RE_BORDER_STYLE = /(\s*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)(\s*$)?/imx.freeze
59
61
  RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)
@@ -257,8 +259,10 @@ module CssParser
257
259
  inherit
258
260
  currentColor
259
261
  ].freeze
260
- RE_COLOUR_NUMERIC = /\b(hsl|rgb)\s*\(-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?,-?\s*-?\d+(\.\d+)?%?\s*%?\)/i.freeze
261
- 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
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
262
266
  RE_COLOUR_HEX = /\s*#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/.freeze
263
267
  RE_COLOUR_NAMED = /\s*\b(#{NAMED_COLOURS.join('|')})\b/i.freeze
264
268
  RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED)
@@ -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
- declarations[property] = Value.new(value)
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.each { |_key, value| value.important = importance }
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(String.new) do |memo, (prop, value)|
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, specificity = nil)
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.map { |key| [key, 'inherit'] }.to_h
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 m =~ /normal|inherit/i
418
- ['font-style', 'font-weight', 'font-variant'].each do |font_prop|
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 m =~ /italic|oblique/i
467
+ elsif /italic|oblique/i.match?(m)
422
468
  font_props['font-style'] = m
423
- elsif m =~ /small-caps/i
469
+ elsif /small-caps/i.match?(m)
424
470
  font_props['font-variant'] = m
425
- elsif m =~ /[1-9]00$|bold|bolder|lighter/i
471
+ elsif /[1-9]00$|bold|bolder|lighter/i.match?(m)
426
472
  font_props['font-weight'] = m
427
- elsif m =~ CssParser::FONT_UNITS_RX
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 value =~ CssParser::RE_INHERIT
451
- LIST_STYLE_PROPERTIES.map { |key| [key, 'inherit'] }.to_h
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.map do |property|
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 =~ /\s/
567
+ next if /\s/.match?(declaration.value.gsub(/,\s/, ',').strip)
522
568
 
523
569
  declaration.value
524
- end.compact
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 = [:top, :right, :bottom, :left].each_with_index.with_object({}) do |(side, index), result|
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 = String.new
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 [:top, :right, :bottom, :left] if values[:left] != values[:right]
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(/[;$]+/m).each do |decs|
616
- decs = (continuation ? continuation + decs : decs)
617
- if decs =~ /\([^)]*\Z/ # if it has an unmatched parenthesis
618
- continuation = "#{decs};"
619
- elsif (matches = decs.match(/\s*(.[^:]*)\s*:\s*(.+?)(?:;?\s*\Z)/i))
620
- # skip end_of_declaration
621
- property = matches[1]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CssParser
4
- VERSION = '1.13.0'.freeze
4
+ VERSION = '3.0.0'.freeze
5
5
  end
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 'zlib'
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? { |rs| rs.is_a?(CssParser::RuleSet) }
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.map { |s| calculate_specificity(s) }.compact.max || 0
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 not properties.key?(property)
74
+ if !properties.key?(property)
79
75
  properties[property] = {value: value, specificity: specificity, is_important: is_important}
80
76
  elsif is_important
81
- if not properties[property][:is_important] or properties[property][:specificity] <= specificity
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 or 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(/\#/).length
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: 1.13.0
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: 2022-12-29 00:00:00.000000000 Z
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: benchmark-ips
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.8'
104
- type: :development
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.8'
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
- post_install_message:
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: '2.7'
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: 3.1.6
174
- signing_key:
74
+ rubygems_version: 4.0.3
175
75
  specification_version: 4
176
76
  summary: Ruby CSS parser.
177
77
  test_files: []