diff --git a/README.md b/README.md index be4f699f..537a16df 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,19 @@ Below is mostly comprehensive list of checks that HTMLProofer can perform. You can configure HTMLProofer to run on a file, a directory, an array of directories, or an array of links. +There's also a rack middleware. + +### Using in a rails app + +Add to `config/application.rb` + + config.middleware.use HTMLProofer::Middleware if Rails.env.test? + config.middleware.use HTMLProofer::Middleware if Rails.env.development? + +This will raise an error at runtime if your HTML is invalid. + +Particularly helpful for projects which have extensive CI, since any invalid HTML will fail your build. + ### Using in a script 1. Require the gem. diff --git a/lib/html-proofer/middleware.rb b/lib/html-proofer/middleware.rb new file mode 100644 index 00000000..273d4b04 --- /dev/null +++ b/lib/html-proofer/middleware.rb @@ -0,0 +1,80 @@ + + +module HTMLProofer + class Middleware + + class InvalidHtmlError < StandardError + def initialize(failures) + @failures = failures + end + + def message + "HTML Validation errors (skip by adding ?SKIP_VALIDATION to URL): \n#{@failures.join("\n")}" + end + end + + def self.options + @options ||= { + type: :file, + allow_missing_href: true, # Permitted in html5 + allow_hash_href: true, + check_external_hash: true, + check_html: true, + url_ignore: [/.*/], # Don't try to check local files exist + } + end + + def initialize(app) + @app = app + end + + HTML_SIGNATURE = [ + ' 0 + raise InvalidHtmlError.new(parsed[:failures]) + end + end + result + end + end +end diff --git a/lib/html-proofer/runner.rb b/lib/html-proofer/runner.rb index bf6cbd0f..e58c6cb6 100644 --- a/lib/html-proofer/runner.rb +++ b/lib/html-proofer/runner.rb @@ -90,9 +90,8 @@ def process_files end end - def check_path(path) + def check_parsed(html, path) result = { external_urls: {}, failures: [] } - html = create_nokogiri(path) @src = [@src] if @type == :file @@ -112,6 +111,10 @@ def check_path(path) result end + def check_path(path) + check_parsed create_nokogiri(path), path + end + def validate_urls url_validator = HTMLProofer::UrlValidator.new(@logger, @external_urls, @options) @failures.concat(url_validator.run) diff --git a/spec/html-proofer/fixtures/vcr_cassettes/links/integrity_and_cors_pagination_rels_html_check_sri_true_log_level_error_type_file_.yml b/spec/html-proofer/fixtures/vcr_cassettes/links/integrity_and_cors_pagination_rels_html_check_sri_true_log_level_error_type_file_.yml new file mode 100644 index 00000000..1e528042 --- /dev/null +++ b/spec/html-proofer/fixtures/vcr_cassettes/links/integrity_and_cors_pagination_rels_html_check_sri_true_log_level_error_type_file_.yml @@ -0,0 +1,78 @@ +--- +http_interactions: +- request: + method: head + uri: https://github.com + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - Mozilla/5.0 (compatible; HTML Proofer/3.10.2; +https://github.com/gjtorikian/html-proofer) + Accept: + - application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5 + Expect: + - '' + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 30 Apr 2019 01:12:11 GMT + Content-Type: + - text/html; charset=utf-8 + Server: + - GitHub.com + Status: + - 200 OK + Vary: + - X-PJAX + - Accept-Encoding + ETag: + - W/"ddb3d55638fd1d4c24ac9dd9bc28408b" + Cache-Control: + - max-age=0, private, must-revalidate + Set-Cookie: + - has_recent_activity=1; path=/; expires=Tue, 30 Apr 2019 02:12:11 -0000 + - _octo=GH1.1.327282718.1556586731; domain=.github.com; path=/; expires=Fri, + 30 Apr 2021 01:12:11 -0000 + - logged_in=no; domain=.github.com; path=/; expires=Sat, 30 Apr 2039 01:12:11 + -0000; secure; HttpOnly + - _gh_sess=Y21MUitHSCtkcnpJeUMzeFVsQndUdDhQMk9DSUNDME90TmhzTWFTcThZK2NOWDVDbENVdjFrS1Zmd21ncjFpTjhuT2tjWFMvbEVKUUlGK1R0S3R1ZlhmMkhvZy9BL1Q4MkUxaVpHVXRRQi85bDRjbkxJQmdzK3lWV2VQdk9yeFdmZHpCcjZMdUpWazVmQXZ2cU9zL1ZXM2pHQ2NNaXYwU1FDZkNTLzZnclF6WnJNdHhCV0FqVHdmVTJpUWlYL3REakRYQ3ZlSFp5bWZOVE85OTRuNDB4QT09LS1aQTZMaXNDM0ZORzc0R0czbkprMVpRPT0%3D--afb215b5aff4934992d0087b0c30ed41ad2b6c27; + path=/; secure; HttpOnly + X-Request-Id: + - c80accfb-85c8-4c94-81f7-d6c41c997baa + Strict-Transport-Security: + - max-age=31536000; includeSubdomains; preload + X-Frame-Options: + - deny + X-Content-Type-Options: + - nosniff + X-XSS-Protection: + - 1; mode=block + Referrer-Policy: + - origin-when-cross-origin, strict-origin-when-cross-origin + Expect-CT: + - max-age=2592000, report-uri="https://api.github.com/_private/browser/errors" + Content-Security-Policy: + - 'default-src ''none''; base-uri ''self''; block-all-mixed-content; connect-src + ''self'' uploads.github.com www.githubstatus.com collector.githubapp.com api.github.com + www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com + github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com + wss://live.github.com; font-src github.githubassets.com; form-action ''self'' + github.com gist.github.com; frame-ancestors ''none''; frame-src render.githubusercontent.com; + img-src ''self'' data: github.githubassets.com identicons.github.com collector.githubapp.com + github-cloud.s3.amazonaws.com *.githubusercontent.com customer-stories-feed.github.com; + manifest-src ''self''; media-src ''none''; script-src github.githubassets.com; + style-src ''unsafe-inline'' github.githubassets.com' + X-GitHub-Request-Id: + - A7A4:22D8:27ED84:3DA226:5CC7A0EB + body: + encoding: UTF-8 + string: '' + http_version: '1.1' + adapter_metadata: + effective_url: https://github.com/ + recorded_at: Tue, 30 Apr 2019 01:12:11 GMT +recorded_with: VCR 2.9.3 diff --git a/spec/html-proofer/middleware_spec.rb b/spec/html-proofer/middleware_spec.rb new file mode 100644 index 00000000..69a1b340 --- /dev/null +++ b/spec/html-proofer/middleware_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe 'Middleware test' do + let(:request) { {'REQUEST_METHOD' => 'GET'} } + let(:response) { File.open(response_fixture) } + let(:app) { Proc.new { |*args| [200, {}, response] } } + let(:middleware) { HTMLProofer::Middleware.new(app) } + subject { middleware.call(request) } + + context 'with invalid HTML' do + let(:response_fixture) { File.join(FIXTURES_DIR, 'html', 'missing_closing_quotes.html') } + it 'raises an error' do + expect { + subject + }.to raise_error(HTMLProofer::Middleware::InvalidHtmlError) + end + end + + context 'with valid HTML' do + let(:response_fixture) { File.join(FIXTURES_DIR, 'html', 'html5_tags.html') } + it 'does not raise an error' do + subject + end + end + + context 'with non-HTML content' do + let(:response_fixture) { File.join(FIXTURES_DIR, 'images', 'gpl.png') } + it 'does not raise an error' do + subject + end + end + +end