This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTMLProofer
|
||||
class Check
|
||||
class Favicon < HTMLProofer::Check
|
||||
def run
|
||||
found = false
|
||||
@html.css("link").each do |node|
|
||||
@favicon = create_element(node)
|
||||
|
||||
next if @favicon.ignore?
|
||||
|
||||
break if (found = @favicon.node["rel"].split.last.eql?("icon"))
|
||||
end
|
||||
|
||||
return if immediate_redirect?
|
||||
|
||||
if found
|
||||
if @favicon.url.protocol_relative?
|
||||
add_failure(
|
||||
"favicon link #{@favicon.url} is a protocol-relative URL, use explicit https:// instead",
|
||||
element: @favicon,
|
||||
)
|
||||
elsif @favicon.url.remote?
|
||||
add_to_external_urls(@favicon.url, @favicon.line)
|
||||
elsif !@favicon.url.exists?
|
||||
add_failure(
|
||||
"internal favicon #{@favicon.url.raw_attribute} does not exist",
|
||||
element: @favicon,
|
||||
)
|
||||
end
|
||||
else
|
||||
add_failure("no favicon provided")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# allow any instant-redirect meta tag
|
||||
def immediate_redirect?
|
||||
@html.xpath("//meta[@http-equiv='refresh']").attribute("content").value.start_with?("0;")
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTMLProofer
|
||||
class Check
|
||||
class Images < HTMLProofer::Check
|
||||
SCREEN_SHOT_REGEX = /Screen(?: |%20)Shot(?: |%20)\d+-\d+-\d+(?: |%20)at(?: |%20)\d+.\d+.\d+/
|
||||
|
||||
def run
|
||||
@html.css("img, source").each do |node|
|
||||
@img = create_element(node)
|
||||
|
||||
next if @img.ignore?
|
||||
|
||||
# screenshot filenames should return because of terrible names
|
||||
add_failure(
|
||||
"image has a terrible filename (#{@img.url.raw_attribute})",
|
||||
element: @img,
|
||||
) if terrible_filename?
|
||||
|
||||
# does the image exist?
|
||||
if missing_src?
|
||||
add_failure("image has no src or srcset attribute", element: @img)
|
||||
elsif @img.multiple_srcsets? || @img.multiple_sizes?
|
||||
@img.srcsets_wo_sizes.each do |srcset|
|
||||
srcset_url = HTMLProofer::Attribute::Url.new(@runner, srcset, base_url: @img.base_url, source: @img.url.source, filename: @img.url.filename, extract_size: true)
|
||||
|
||||
if srcset_url.protocol_relative?
|
||||
add_failure(
|
||||
"image link #{srcset_url.url} is a protocol-relative URL, use explicit https:// instead",
|
||||
element: @img,
|
||||
)
|
||||
elsif srcset_url.remote?
|
||||
add_to_external_urls(srcset_url.url, @img.line)
|
||||
elsif !srcset_url.exists?
|
||||
add_failure("internal image #{srcset} does not exist", element: @img)
|
||||
end
|
||||
end
|
||||
elsif @img.url.protocol_relative?
|
||||
add_failure(
|
||||
"image link #{@img.url} is a protocol-relative URL, use explicit https:// instead",
|
||||
element: @img,
|
||||
)
|
||||
elsif @img.url.remote?
|
||||
add_to_external_urls(@img.url, @img.line)
|
||||
elsif !@img.url.exists? && !@img.multiple_srcsets? && !@img.multiple_sizes?
|
||||
add_failure(
|
||||
"internal image #{@img.url.raw_attribute} does not exist",
|
||||
element: @img,
|
||||
)
|
||||
end
|
||||
|
||||
# if this is an img element, check that the alt attribute is present
|
||||
if @img.img_tag? && !ignore_element?
|
||||
if missing_alt_tag? && !ignore_missing_alt?
|
||||
add_failure(
|
||||
"image #{@img.url.raw_attribute} does not have an alt attribute",
|
||||
element: @img,
|
||||
)
|
||||
elsif (empty_alt_tag? || alt_all_spaces?) && !ignore_empty_alt?
|
||||
add_failure(
|
||||
"image #{@img.url.raw_attribute} has an alt attribute, but no content",
|
||||
element: @img,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
add_failure(
|
||||
"image #{@img.url.raw_attribute} uses the http scheme",
|
||||
element: @img,
|
||||
) if @runner.enforce_https? && @img.url.http?
|
||||
end
|
||||
|
||||
external_urls
|
||||
end
|
||||
|
||||
def ignore_missing_alt?
|
||||
@runner.options[:ignore_missing_alt]
|
||||
end
|
||||
|
||||
def ignore_empty_alt?
|
||||
@runner.options[:ignore_empty_alt]
|
||||
end
|
||||
|
||||
def ignore_element?
|
||||
@img.url.ignore? || @img.aria_hidden?
|
||||
end
|
||||
|
||||
def missing_alt_tag?
|
||||
@img.node["alt"].nil?
|
||||
end
|
||||
|
||||
def empty_alt_tag?
|
||||
!missing_alt_tag? && @img.node["alt"].empty?
|
||||
end
|
||||
|
||||
def empty_whitespace_alt_tag?
|
||||
!missing_alt_tag? && @img.node["alt"].strip.empty?
|
||||
end
|
||||
|
||||
def alt_all_spaces?
|
||||
!missing_alt_tag? && @img.node["alt"].split.all?(" ")
|
||||
end
|
||||
|
||||
def terrible_filename?
|
||||
@img.url.to_s =~ SCREEN_SHOT_REGEX
|
||||
end
|
||||
|
||||
def missing_src?
|
||||
blank?(@img.url.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,150 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTMLProofer
|
||||
class Check
|
||||
class Links < HTMLProofer::Check
|
||||
def run
|
||||
@html.css("a, link").each do |node|
|
||||
@link = create_element(node)
|
||||
|
||||
next if @link.ignore?
|
||||
|
||||
if !allow_hash_href? && @link.node["href"] == "#"
|
||||
add_failure("linking to internal hash #, which points to nowhere", element: @link)
|
||||
next
|
||||
end
|
||||
|
||||
# is there even an href?
|
||||
if blank?(@link.url.raw_attribute)
|
||||
next if allow_missing_href?
|
||||
|
||||
add_failure("'#{@link.node.name}' tag is missing a reference", element: @link)
|
||||
next
|
||||
end
|
||||
|
||||
# is it even a valid URL?
|
||||
unless @link.url.valid?
|
||||
add_failure("#{@link.href} is an invalid URL", element: @link)
|
||||
next
|
||||
end
|
||||
|
||||
if @link.url.protocol_relative?
|
||||
add_failure(
|
||||
"#{@link.url} is a protocol-relative URL, use explicit https:// instead",
|
||||
element: @link,
|
||||
)
|
||||
next
|
||||
end
|
||||
|
||||
check_schemes
|
||||
|
||||
# intentionally down here because we still want valid? & missing_href? to execute
|
||||
next if @link.url.non_http_remote?
|
||||
|
||||
if !@link.url.internal? && @link.url.remote?
|
||||
check_sri if @runner.check_sri? && @link.link_tag?
|
||||
|
||||
# we need to skip these for now; although the domain main be valid,
|
||||
# curl/Typheous inaccurately return 404s for some links. cc https://git.io/vyCFx
|
||||
next if @link.node["rel"] == "dns-prefetch"
|
||||
|
||||
unless @link.url.path?
|
||||
add_failure("#{@link.url.raw_attribute} is an invalid URL", element: @link)
|
||||
next
|
||||
end
|
||||
|
||||
add_to_external_urls(@link.url, @link.line)
|
||||
elsif @link.url.internal?
|
||||
# does the local directory have a trailing slash?
|
||||
if @link.url.unslashed_directory?(@link.url.absolute_path)
|
||||
add_failure(
|
||||
"internally linking to a directory #{@link.url.raw_attribute} without trailing slash",
|
||||
element: @link,
|
||||
)
|
||||
next
|
||||
end
|
||||
|
||||
add_to_internal_urls(@link.url, @link.line)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def allow_missing_href?
|
||||
@runner.options[:allow_missing_href]
|
||||
end
|
||||
|
||||
def allow_hash_href?
|
||||
@runner.options[:allow_hash_href]
|
||||
end
|
||||
|
||||
def check_schemes
|
||||
case @link.url.scheme
|
||||
when "mailto"
|
||||
handle_mailto
|
||||
when "tel"
|
||||
handle_tel
|
||||
when "http"
|
||||
return unless @runner.options[:enforce_https]
|
||||
|
||||
add_failure("#{@link.url.raw_attribute} is not an HTTPS link", element: @link)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_mailto
|
||||
if @link.url.path.empty?
|
||||
add_failure(
|
||||
"#{@link.url.raw_attribute} contains no email address",
|
||||
element: @link,
|
||||
) unless ignore_empty_mailto?
|
||||
# eg., if any do not match a valid URL
|
||||
elsif @link.url.path.split(",").any? { |email| !/#{URI::MailTo::EMAIL_REGEXP}/o.match?(email) }
|
||||
add_failure(
|
||||
"#{@link.url.raw_attribute} contains an invalid email address",
|
||||
element: @link,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_tel
|
||||
add_failure(
|
||||
"#{@link.url.raw_attribute} contains no phone number",
|
||||
element: @link,
|
||||
) if @link.url.path.empty?
|
||||
end
|
||||
|
||||
def ignore_empty_mailto?
|
||||
@runner.options[:ignore_empty_mailto]
|
||||
end
|
||||
|
||||
# Allowed elements from Subresource Integrity specification
|
||||
# https://w3c.github.io/webappsec-subresource-integrity/#link-element-for-stylesheets
|
||||
SRI_REL_TYPES = %(stylesheet)
|
||||
|
||||
def check_sri
|
||||
return unless SRI_REL_TYPES.include?(@link.node["rel"])
|
||||
|
||||
if blank?(@link.node["integrity"]) && blank?(@link.node["crossorigin"])
|
||||
add_failure(
|
||||
"SRI and CORS not provided in: #{@link.url.raw_attribute}",
|
||||
element: @link,
|
||||
)
|
||||
elsif blank?(@link.node["integrity"])
|
||||
add_failure("Integrity is missing in: #{@link.url.raw_attribute}", element: @link)
|
||||
elsif blank?(@link.node["crossorigin"])
|
||||
add_failure(
|
||||
"CORS not provided for external resource in: #{@link.link.url.raw_attribute}",
|
||||
element: @link,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private def source_tag?
|
||||
@link.node.name == "source"
|
||||
end
|
||||
|
||||
private def anchor_tag?
|
||||
@link.node.name == "a"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTMLProofer
|
||||
class Check
|
||||
class OpenGraph < HTMLProofer::Check
|
||||
def run
|
||||
@html.css('meta[property="og:url"], meta[property="og:image"]').each do |node|
|
||||
@open_graph = create_element(node)
|
||||
|
||||
next if @open_graph.ignore?
|
||||
|
||||
# does the open_graph exist?
|
||||
if missing_content?
|
||||
add_failure("open graph has no content attribute", element: @open_graph)
|
||||
elsif empty_content?
|
||||
add_failure("open graph content attribute is empty", element: @open_graph)
|
||||
elsif !@open_graph.url.valid?
|
||||
add_failure("#{@open_graph.src} is an invalid URL", element: @open_graph)
|
||||
elsif @open_graph.url.protocol_relative?
|
||||
add_failure(
|
||||
"open graph link #{@open_graph.url} is a protocol-relative URL, use explicit https:// instead",
|
||||
element: @open_graph,
|
||||
)
|
||||
elsif @open_graph.url.remote?
|
||||
add_to_external_urls(@open_graph.url, @open_graph.line)
|
||||
else
|
||||
add_failure(
|
||||
"internal open graph #{@open_graph.url.raw_attribute} does not exist",
|
||||
element: @open_graph,
|
||||
) unless @open_graph.url.exists?
|
||||
end
|
||||
end
|
||||
|
||||
external_urls
|
||||
end
|
||||
|
||||
private def missing_content?
|
||||
@open_graph.node["content"].nil?
|
||||
end
|
||||
|
||||
private def empty_content?
|
||||
@open_graph.node["content"].empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HTMLProofer
|
||||
class Check
|
||||
class Scripts < HTMLProofer::Check
|
||||
def run
|
||||
@html.css("script").each do |node|
|
||||
@script = create_element(node)
|
||||
|
||||
next if @script.ignore?
|
||||
next unless @script.content.strip.empty?
|
||||
|
||||
# does the script exist?
|
||||
if missing_src?
|
||||
add_failure("script is empty and has no src attribute", element: @script)
|
||||
elsif @script.url.protocol_relative?
|
||||
add_failure(
|
||||
"script link #{@script.url} is a protocol-relative URL, use explicit https:// instead",
|
||||
element: @script,
|
||||
)
|
||||
elsif @script.url.remote?
|
||||
add_to_external_urls(@script.url, @script.line)
|
||||
check_sri if @runner.check_sri?
|
||||
elsif !@script.url.exists?
|
||||
add_failure(
|
||||
"internal script reference #{@script.src} does not exist",
|
||||
element: @script,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
external_urls
|
||||
end
|
||||
|
||||
def missing_src?
|
||||
@script.node["src"].nil?
|
||||
end
|
||||
|
||||
def check_sri
|
||||
if blank?(@script.node["integrity"]) && blank?(@script.node["crossorigin"])
|
||||
add_failure(
|
||||
"SRI and CORS not provided in: #{@script.url.raw_attribute}",
|
||||
element: @script,
|
||||
)
|
||||
elsif blank?(@script.node["integrity"])
|
||||
add_failure(
|
||||
"Integrity is missing in: #{@script.url.raw_attribute}",
|
||||
element: @script,
|
||||
)
|
||||
elsif blank?(@script.node["crossorigin"])
|
||||
add_failure(
|
||||
"CORS not provided for external resource in: #{@script.url.raw_attribute}",
|
||||
element: @script,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user