html-proofer 3.19.4 → 4.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/bin/htmlproofer +31 -57
  3. data/lib/html-proofer.rb +1 -54
  4. data/lib/html_proofer/attribute/url.rb +237 -0
  5. data/lib/html_proofer/attribute.rb +15 -0
  6. data/lib/html_proofer/cache.rb +279 -0
  7. data/lib/html_proofer/check/favicon.rb +40 -0
  8. data/lib/html_proofer/check/images.rb +93 -0
  9. data/lib/html_proofer/check/links.rb +129 -0
  10. data/lib/html_proofer/check/open_graph.rb +39 -0
  11. data/lib/html_proofer/check/scripts.rb +46 -0
  12. data/lib/html_proofer/check.rb +92 -0
  13. data/lib/html_proofer/configuration.rb +88 -0
  14. data/lib/html_proofer/element.rb +122 -0
  15. data/lib/html_proofer/failure.rb +17 -0
  16. data/lib/{html-proofer → html_proofer}/log.rb +19 -19
  17. data/lib/html_proofer/reporter/cli.rb +33 -0
  18. data/lib/html_proofer/reporter.rb +23 -0
  19. data/lib/html_proofer/runner.rb +246 -0
  20. data/lib/html_proofer/url_validator/external.rb +194 -0
  21. data/lib/html_proofer/url_validator/internal.rb +96 -0
  22. data/lib/html_proofer/url_validator.rb +16 -0
  23. data/lib/{html-proofer → html_proofer}/utils.rb +9 -12
  24. data/lib/{html-proofer → html_proofer}/version.rb +1 -1
  25. data/lib/html_proofer/xpath_functions.rb +10 -0
  26. data/lib/html_proofer.rb +57 -0
  27. metadata +42 -22
  28. data/lib/html-proofer/cache.rb +0 -194
  29. data/lib/html-proofer/check/favicon.rb +0 -29
  30. data/lib/html-proofer/check/html.rb +0 -37
  31. data/lib/html-proofer/check/images.rb +0 -48
  32. data/lib/html-proofer/check/links.rb +0 -182
  33. data/lib/html-proofer/check/opengraph.rb +0 -46
  34. data/lib/html-proofer/check/scripts.rb +0 -42
  35. data/lib/html-proofer/check.rb +0 -75
  36. data/lib/html-proofer/configuration.rb +0 -88
  37. data/lib/html-proofer/element.rb +0 -265
  38. data/lib/html-proofer/issue.rb +0 -65
  39. data/lib/html-proofer/middleware.rb +0 -82
  40. data/lib/html-proofer/runner.rb +0 -249
  41. data/lib/html-proofer/url_validator.rb +0 -237
@@ -0,0 +1,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module HTMLProofer
8
+ class Cache
9
+ include HTMLProofer::Utils
10
+
11
+ CACHE_VERSION = 2
12
+
13
+ DEFAULT_STORAGE_DIR = File.join("tmp", ".htmlproofer")
14
+ DEFAULT_CACHE_FILE_NAME = "cache.json"
15
+
16
+ URI_REGEXP = URI::DEFAULT_PARSER.make_regexp
17
+
18
+ attr_reader :exists, :cache_log, :storage_dir, :cache_file
19
+
20
+ def initialize(runner, options)
21
+ @runner = runner
22
+ @logger = @runner.logger
23
+
24
+ @cache_datetime = Time.now
25
+ @cache_time = @cache_datetime.to_time
26
+
27
+ if blank?(options)
28
+ define_singleton_method(:enabled?) { false }
29
+ else
30
+ define_singleton_method(:enabled?) { true }
31
+ setup_cache!(options)
32
+
33
+ @external_timeframe = parsed_timeframe(options[:timeframe][:external])
34
+ @internal_timeframe = parsed_timeframe(options[:timeframe][:internal])
35
+ end
36
+ end
37
+
38
+ def parsed_timeframe(timeframe)
39
+ return nil if timeframe.nil?
40
+
41
+ time, date = timeframe.match(/(\d+)(\D)/).captures
42
+ time = time.to_i
43
+ case date
44
+ when "M"
45
+ time_ago(time, :months)
46
+ when "w"
47
+ time_ago(time, :weeks)
48
+ when "d"
49
+ time_ago(time, :days)
50
+ when "h"
51
+ time_ago(time, :hours)
52
+ else
53
+ raise ArgumentError, "#{date} is not a valid timeframe!"
54
+ end
55
+ end
56
+
57
+ def add_internal(url, metadata, found)
58
+ return unless enabled?
59
+
60
+ @cache_log[:internal][url] = { time: @cache_time, metadata: [] } if @cache_log[:internal][url].nil?
61
+
62
+ @cache_log[:internal][url][:metadata] << construct_internal_link_metadata(metadata, found)
63
+ end
64
+
65
+ def add_external(url, filenames, status_code, msg)
66
+ return unless enabled?
67
+
68
+ found = status_code.between?(200, 299)
69
+
70
+ clean_url = cleaned_url(url)
71
+ @cache_log[:external][clean_url] =
72
+ { time: @cache_time.to_s, found: found, status_code: status_code, message: msg, metadata: filenames }
73
+ end
74
+
75
+ def detect_url_changes(urls_detected, type)
76
+ additions = determine_additions(urls_detected, type)
77
+
78
+ determine_deletions(urls_detected, type)
79
+
80
+ additions
81
+ end
82
+
83
+ def write
84
+ return unless enabled?
85
+
86
+ File.write(@cache_file, @cache_log.to_json)
87
+ end
88
+
89
+ def retrieve_urls(urls_detected, type)
90
+ # if there are no urls, bail
91
+ return {} if urls_detected.empty?
92
+
93
+ urls_detected = urls_detected.transform_keys do |url|
94
+ cleaned_url(url)
95
+ end
96
+
97
+ urls_to_check = detect_url_changes(urls_detected, type)
98
+
99
+ @cache_log[type].each_pair do |url, cache|
100
+ within_timeframe = type == :external ? within_external_timeframe?(cache[:time]) : within_internal_timeframe?(cache[:time])
101
+ next if within_timeframe
102
+
103
+ urls_to_check[url] = cache[:metadata] # recheck expired links
104
+ end
105
+
106
+ urls_to_check
107
+ end
108
+
109
+ def within_external_timeframe?(time)
110
+ within_timeframe?(time, @external_timeframe)
111
+ end
112
+
113
+ def within_internal_timeframe?(time)
114
+ within_timeframe?(time, @internal_timeframe)
115
+ end
116
+
117
+ def empty?
118
+ blank?(@cache_log) || (@cache_log[:internal].empty? && @cache_log[:external].empty?)
119
+ end
120
+
121
+ def size(type)
122
+ @cache_log[type].size
123
+ end
124
+
125
+ private def construct_internal_link_metadata(metadata, found)
126
+ {
127
+ source: metadata[:source],
128
+ filename: metadata[:filename],
129
+ line: metadata[:line],
130
+ base_url: metadata[:base_url],
131
+ found: found,
132
+ }
133
+ end
134
+
135
+ # prepare to add new URLs detected
136
+ private def determine_additions(urls_detected, type)
137
+ additions = type == :external ? determine_external_additions(urls_detected) : determine_internal_additions(urls_detected)
138
+
139
+ new_link_count = additions.length
140
+ new_link_text = pluralize(new_link_count, "new #{type} link", "new #{type} links")
141
+ @logger.log(:debug, "Adding #{new_link_text} to the cache")
142
+
143
+ additions
144
+ end
145
+
146
+ private def determine_external_additions(urls_detected)
147
+ urls_detected.reject do |url, _metadata|
148
+ if @cache_log[:external].include?(url)
149
+ @cache_log[:external][url][:found] # if this is false, we're trying again
150
+ else
151
+ @logger.log(:debug, "Adding #{url} to external cache")
152
+ false
153
+ end
154
+ end
155
+ end
156
+
157
+ private def determine_internal_additions(urls_detected)
158
+ urls_detected.each_with_object({}) do |(url, metadata), hsh|
159
+ # url is not even in cache
160
+ if @cache_log[:internal][url].nil?
161
+ hsh[url] = metadata
162
+ next
163
+ end
164
+
165
+ cache_metadata = @cache_log[:internal][url][:metadata]
166
+ incoming_metadata = urls_detected[url].each_with_object([]) do |incoming_url, arr|
167
+ existing_cache_metadata = cache_metadata.find { |k, _| k[:filename] == incoming_url[:filename] }
168
+
169
+ # cache for this url, from an existing path, exists as found
170
+ if !existing_cache_metadata.nil? && !existing_cache_metadata.empty? && existing_cache_metadata[:found]
171
+ metadata.find { |m| m[:filename] == existing_cache_metadata[:filename] }[:found] = true
172
+ next
173
+ end
174
+
175
+ @logger.log(:debug, "Adding #{incoming_url} to internal cache")
176
+ arr << incoming_url
177
+ end
178
+
179
+ hsh[url] = incoming_metadata
180
+ end
181
+ end
182
+
183
+ # remove from cache URLs that no longer exist
184
+ private def determine_deletions(urls_detected, type)
185
+ deletions = 0
186
+
187
+ @cache_log[type].delete_if do |url, _|
188
+ if urls_detected.include?(url)
189
+ false
190
+ elsif url_matches_type?(url, type)
191
+ @logger.log(:debug, "Removing #{url} from #{type} cache")
192
+ deletions += 1
193
+ true
194
+ end
195
+ end
196
+
197
+ del_link_text = pluralize(deletions, "outdated #{type} link", "outdated #{type} links")
198
+ @logger.log(:debug, "Removing #{del_link_text} from the cache")
199
+ end
200
+
201
+ private def setup_cache!(options)
202
+ default_structure = {
203
+ version: CACHE_VERSION,
204
+ internal: {},
205
+ external: {},
206
+ }
207
+
208
+ @storage_dir = options[:storage_dir] || DEFAULT_STORAGE_DIR
209
+
210
+ FileUtils.mkdir_p(storage_dir) unless Dir.exist?(storage_dir)
211
+
212
+ cache_file_name = options[:cache_file] || DEFAULT_CACHE_FILE_NAME
213
+
214
+ @cache_file = File.join(storage_dir, cache_file_name)
215
+
216
+ return (@cache_log = default_structure) unless File.exist?(@cache_file)
217
+
218
+ contents = File.read(@cache_file)
219
+
220
+ return (@cache_log = default_structure) if blank?(contents)
221
+
222
+ log = JSON.parse(contents, symbolize_names: true)
223
+
224
+ old_cache = (cache_version = log[:version]).nil?
225
+ @cache_log = if old_cache # previous cache version, create a new one
226
+ default_structure
227
+ elsif cache_version != CACHE_VERSION
228
+ # if cache version is newer...do something
229
+ else
230
+ log[:internal] = log[:internal].transform_keys(&:to_s)
231
+ log[:external] = log[:external].transform_keys(&:to_s)
232
+ log
233
+ end
234
+ end
235
+
236
+ # https://github.com/rails/rails/blob/3872bc0e54d32e8bf3a6299b0bfe173d94b072fc/activesupport/lib/active_support/duration.rb#L112-L117
237
+ SECONDS_PER_HOUR = 3600
238
+ SECONDS_PER_DAY = 86400
239
+ SECONDS_PER_WEEK = 604800
240
+ SECONDS_PER_MONTH = 2629746 # 1/12 of a gregorian year
241
+
242
+ private def time_ago(measurement, unit)
243
+ case unit
244
+ when :months
245
+ @cache_datetime - (SECONDS_PER_MONTH * measurement)
246
+ when :weeks
247
+ @cache_datetime - (SECONDS_PER_WEEK * measurement)
248
+ when :days
249
+ @cache_datetime - (SECONDS_PER_DAY * measurement)
250
+ when :hours
251
+ @cache_datetime - Rational(SECONDS_PER_HOUR * measurement)
252
+ end.to_time
253
+ end
254
+
255
+ private def url_matches_type?(url, type)
256
+ return true if type == :internal && url !~ URI_REGEXP
257
+ return true if type == :external && url =~ URI_REGEXP
258
+ end
259
+
260
+ private def cleaned_url(url)
261
+ cleaned_url = escape_unescape(url)
262
+
263
+ return cleaned_url unless cleaned_url.end_with?("/", "#", "?") && cleaned_url.length > 1
264
+
265
+ cleaned_url[0..-2]
266
+ end
267
+
268
+ private def escape_unescape(url)
269
+ Addressable::URI.parse(url).normalize.to_s
270
+ end
271
+
272
+ private def within_timeframe?(current_time, parsed_timeframe)
273
+ return false if current_time.nil? || parsed_timeframe.nil?
274
+
275
+ current_time = Time.parse(current_time) if current_time.is_a?(String)
276
+ (parsed_timeframe..@cache_time).cover?(current_time)
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTMLProofer
4
+ class Check
5
+ class Favicon < HTMLProofer::Check
6
+ def run
7
+ found = false
8
+ @html.css("link").each do |node|
9
+ @favicon = create_element(node)
10
+
11
+ next if @favicon.ignore?
12
+
13
+ break if (found = @favicon.node["rel"].split.last.eql?("icon"))
14
+ end
15
+
16
+ return if immediate_redirect?
17
+
18
+ if found
19
+ if @favicon.url.remote?
20
+ add_to_external_urls(@favicon.url, @favicon.line)
21
+ elsif !@favicon.url.exists?
22
+ add_failure("internal favicon #{@favicon.url.raw_attribute} does not exist", line: @favicon.line,
23
+ content: @favicon.content)
24
+ end
25
+ else
26
+ add_failure("no favicon provided")
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ # allow any instant-redirect meta tag
33
+ def immediate_redirect?
34
+ @html.xpath("//meta[@http-equiv='refresh']").attribute("content").value.start_with?("0;")
35
+ rescue StandardError
36
+ false
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTMLProofer
4
+ class Check
5
+ class Images < HTMLProofer::Check
6
+ SCREEN_SHOT_REGEX = /Screen(?: |%20)Shot(?: |%20)\d+-\d+-\d+(?: |%20)at(?: |%20)\d+.\d+.\d+/.freeze
7
+
8
+ def run
9
+ @html.css("img").each do |node|
10
+ @img = create_element(node)
11
+
12
+ next if @img.ignore?
13
+
14
+ # screenshot filenames should return because of terrible names
15
+ add_failure("image has a terrible filename (#{@img.url.raw_attribute})", line: @img.line,
16
+ content: @img.content) if terrible_filename?
17
+
18
+ # does the image exist?
19
+ if missing_src?
20
+ add_failure("image has no src or srcset attribute", line: @img.line, content: @img.content)
21
+ elsif @img.url.remote?
22
+ add_to_external_urls(@img.url, @img.line)
23
+ elsif !@img.url.exists? && !@img.multiple_srcsets?
24
+ add_failure("internal image #{@img.url.raw_attribute} does not exist", line: @img.line,
25
+ content: @img.content)
26
+ elsif @img.multiple_srcsets?
27
+ srcsets = @img.srcset.split(",").map(&:strip)
28
+ srcsets.each do |srcset|
29
+ srcset_url = HTMLProofer::Attribute::Url.new(@runner, srcset, base_url: @img.base_url)
30
+
31
+ if srcset_url.remote?
32
+ add_to_external_urls(srcset_url.url, @img.line)
33
+ elsif !srcset_url.exists?
34
+ add_failure("internal image #{srcset} does not exist", line: @img.line, content: @img.content)
35
+ end
36
+ end
37
+ end
38
+
39
+ unless ignore_element?
40
+ if missing_alt_tag? && !ignore_missing_alt?
41
+ add_failure("image #{@img.url.raw_attribute} does not have an alt attribute", line: @img.line,
42
+ content: @img.content)
43
+ elsif (empty_alt_tag? || alt_all_spaces?) && !ignore_empty_alt?
44
+ add_failure("image #{@img.url.raw_attribute} has an alt attribute, but no content", line: @img.line,
45
+ content: @img.content)
46
+ end
47
+ end
48
+
49
+ add_failure("image #{@img.url.raw_attribute} uses the http scheme", line: @img.line,
50
+ content: @img.content) if @runner.enforce_https? && @img.url.http?
51
+ end
52
+
53
+ external_urls
54
+ end
55
+
56
+ def ignore_missing_alt?
57
+ @runner.options[:ignore_missing_alt]
58
+ end
59
+
60
+ def ignore_empty_alt?
61
+ @runner.options[:ignore_empty_alt]
62
+ end
63
+
64
+ def ignore_element?
65
+ @img.url.ignore? || @img.aria_hidden?
66
+ end
67
+
68
+ def missing_alt_tag?
69
+ @img.node["alt"].nil?
70
+ end
71
+
72
+ def empty_alt_tag?
73
+ !missing_alt_tag? && @img.node["alt"].empty?
74
+ end
75
+
76
+ def empty_whitespace_alt_tag?
77
+ !missing_alt_tag? && @img.node["alt"].strip.empty?
78
+ end
79
+
80
+ def alt_all_spaces?
81
+ !missing_alt_tag? && @img.node["alt"].split.all?(" ")
82
+ end
83
+
84
+ def terrible_filename?
85
+ @img.url.to_s =~ SCREEN_SHOT_REGEX
86
+ end
87
+
88
+ def missing_src?
89
+ blank?(@img.url.to_s)
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTMLProofer
4
+ class Check
5
+ class Links < HTMLProofer::Check
6
+ def run
7
+ @html.css("a, link, source").each do |node|
8
+ @link = create_element(node)
9
+
10
+ next if @link.ignore?
11
+
12
+ if !allow_hash_href? && @link.node["href"] == "#"
13
+ add_failure("linking to internal hash #, which points to nowhere", line: @link.line, content: @link.content)
14
+ next
15
+ end
16
+
17
+ # is there even an href?
18
+ if blank?(@link.url.raw_attribute)
19
+ next if allow_missing_href?
20
+
21
+ add_failure("'#{@link.node.name}' tag is missing a reference", line: @link.line, content: @link.content)
22
+ next
23
+ end
24
+
25
+ # is it even a valid URL?
26
+ unless @link.url.valid?
27
+ add_failure("#{@link.href} is an invalid URL", line: @link.line, content: @link.content)
28
+ next
29
+ end
30
+
31
+ check_schemes
32
+
33
+ # intentionally down here because we still want valid? & missing_href? to execute
34
+ next if @link.url.non_http_remote?
35
+
36
+ if !@link.url.internal? && @link.url.remote?
37
+ check_sri if @runner.check_sri? && @link.link_tag?
38
+
39
+ # we need to skip these for now; although the domain main be valid,
40
+ # curl/Typheous inaccurately return 404s for some links. cc https://git.io/vyCFx
41
+ next if @link.node["rel"] == "dns-prefetch"
42
+
43
+ unless @link.url.path?
44
+ add_failure("#{@link.url.raw_attribute} is an invalid URL", line: @link.line, content: @link.content)
45
+ next
46
+ end
47
+
48
+ add_to_external_urls(@link.url, @link.line)
49
+ elsif @link.url.internal?
50
+ # does the local directory have a trailing slash?
51
+ if @link.url.unslashed_directory?(@link.url.absolute_path)
52
+ add_failure("internally linking to a directory #{@link.url.raw_attribute} without trailing slash",
53
+ line: @link.line, content: @link.content)
54
+ next
55
+ end
56
+
57
+ add_to_internal_urls(@link.url, @link.line)
58
+ end
59
+ end
60
+ end
61
+
62
+ def allow_missing_href?
63
+ @runner.options[:allow_missing_href]
64
+ end
65
+
66
+ def allow_hash_href?
67
+ @runner.options[:allow_hash_href]
68
+ end
69
+
70
+ def check_schemes
71
+ case @link.url.scheme
72
+ when "mailto"
73
+ handle_mailto
74
+ when "tel"
75
+ handle_tel
76
+ when "http"
77
+ return unless @runner.options[:enforce_https]
78
+
79
+ add_failure("#{@link.url.raw_attribute} is not an HTTPS link", line: @link.line, content: @link.content)
80
+ end
81
+ end
82
+
83
+ def handle_mailto
84
+ if @link.url.path.empty?
85
+ add_failure("#{@link.url.raw_attribute} contains no email address", line: @link.line,
86
+ content: @link.content) unless ignore_empty_mailto?
87
+ elsif !/#{URI::MailTo::EMAIL_REGEXP}/o.match?(@link.url.path)
88
+ add_failure("#{@link.url.raw_attribute} contains an invalid email address", line: @link.line,
89
+ content: @link.content)
90
+ end
91
+ end
92
+
93
+ def handle_tel
94
+ add_failure("#{@link.url.raw_attribute} contains no phone number", line: @link.line,
95
+ content: @link.content) if @link.url.path.empty?
96
+ end
97
+
98
+ def ignore_empty_mailto?
99
+ @runner.options[:ignore_empty_mailto]
100
+ end
101
+
102
+ # Allowed elements from Subresource Integrity specification
103
+ # https://w3c.github.io/webappsec-subresource-integrity/#link-element-for-stylesheets
104
+ SRI_REL_TYPES = %(stylesheet)
105
+
106
+ def check_sri
107
+ return unless SRI_REL_TYPES.include?(@link.node["rel"])
108
+
109
+ if blank?(@link.node["integrity"]) && blank?(@link.node["crossorigin"])
110
+ add_failure("SRI and CORS not provided in: #{@link.url.raw_attribute}", line: @link.line,
111
+ content: @link.content)
112
+ elsif blank?(@link.node["integrity"])
113
+ add_failure("Integrity is missing in: #{@link.url.raw_attribute}", line: @link.line, content: @link.content)
114
+ elsif blank?(@link.node["crossorigin"])
115
+ add_failure("CORS not provided for external resource in: #{@link.link.url.raw_attribute}", line: @link.line,
116
+ content: @link.content)
117
+ end
118
+ end
119
+
120
+ private def source_tag?
121
+ @link.node.name == "source"
122
+ end
123
+
124
+ private def anchor_tag?
125
+ @link.node.name == "a"
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTMLProofer
4
+ class Check
5
+ class OpenGraph < HTMLProofer::Check
6
+ def run
7
+ @html.css('meta[property="og:url"], meta[property="og:image"]').each do |node|
8
+ @open_graph = create_element(node)
9
+
10
+ next if @open_graph.ignore?
11
+
12
+ # does the open_graph exist?
13
+ if missing_content?
14
+ add_failure("open graph has no content attribute", line: @open_graph.line, content: @open_graph.content)
15
+ elsif empty_content?
16
+ add_failure("open graph content attribute is empty", line: @open_graph.line, content: @open_graph.content)
17
+ elsif !@open_graph.url.valid?
18
+ add_failure("#{@open_graph.src} is an invalid URL", line: @open_graph.line)
19
+ elsif @open_graph.url.remote?
20
+ add_to_external_urls(@open_graph.url, @open_graph.line)
21
+ else
22
+ add_failure("internal open graph #{@open_graph.url.raw_attribute} does not exist", line: @open_graph.line,
23
+ content: @open_graph.content) unless @open_graph.url.exists?
24
+ end
25
+ end
26
+
27
+ external_urls
28
+ end
29
+
30
+ private def missing_content?
31
+ @open_graph.node["content"].nil?
32
+ end
33
+
34
+ private def empty_content?
35
+ @open_graph.node["content"].empty?
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTMLProofer
4
+ class Check
5
+ class Scripts < HTMLProofer::Check
6
+ def run
7
+ @html.css("script").each do |node|
8
+ @script = create_element(node)
9
+
10
+ next if @script.ignore?
11
+ next unless @script.content.strip.empty?
12
+
13
+ # does the script exist?
14
+ if missing_src?
15
+ add_failure("script is empty and has no src attribute", line: @script.line, content: @script.content)
16
+ elsif @script.url.remote?
17
+ add_to_external_urls(@script.src, @script.line)
18
+ check_sri if @runner.check_sri?
19
+ elsif !@script.url.exists?
20
+ add_failure("internal script reference #{@script.src} does not exist", line: @script.line,
21
+ content: @script.content)
22
+ end
23
+ end
24
+
25
+ external_urls
26
+ end
27
+
28
+ def missing_src?
29
+ @script.node["src"].nil?
30
+ end
31
+
32
+ def check_sri
33
+ if blank?(@script.node["integrity"]) && blank?(@script.node["crossorigin"])
34
+ add_failure("SRI and CORS not provided in: #{@script.url.raw_attribute}", line: @script.line,
35
+ content: @script.content)
36
+ elsif blank?(@script.node["integrity"])
37
+ add_failure("Integrity is missing in: #{@script.url.raw_attribute}", line: @script.line,
38
+ content: @script.content)
39
+ elsif blank?(@script.node["crossorigin"])
40
+ add_failure("CORS not provided for external resource in: #{@script.url.raw_attribute}", line: @script.line,
41
+ content: @script.content)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end