html-proofer 2.3.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.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/bin/htmlproofer +85 -0
  3. data/lib/html-proofer/cache.rb +153 -0
  4. data/lib/html-proofer/check/favicon.rb +15 -0
  5. data/lib/html-proofer/check/html.rb +21 -0
  6. data/lib/html-proofer/check/images.rb +47 -0
  7. data/lib/html-proofer/check/links.rb +120 -0
  8. data/lib/html-proofer/check/scripts.rb +28 -0
  9. data/lib/html-proofer/check.rb +63 -0
  10. data/lib/html-proofer/configuration.rb +56 -0
  11. data/lib/html-proofer/element.rb +165 -0
  12. data/lib/{html/proofer/check_runner → html-proofer}/issue.rb +8 -10
  13. data/lib/html-proofer/log.rb +49 -0
  14. data/lib/html-proofer/runner.rb +160 -0
  15. data/lib/html-proofer/url_validator.rb +218 -0
  16. data/lib/html-proofer/utils.rb +40 -0
  17. data/lib/html-proofer/version.rb +3 -0
  18. data/lib/html-proofer.rb +47 -0
  19. metadata +64 -289
  20. data/.gitignore +0 -26
  21. data/.travis.yml +0 -12
  22. data/Gemfile +0 -3
  23. data/LICENSE.txt +0 -22
  24. data/README.md +0 -265
  25. data/Rakefile +0 -21
  26. data/bin/htmlproof +0 -71
  27. data/html-proofer.gemspec +0 -31
  28. data/lib/html/proofer/check_runner.rb +0 -62
  29. data/lib/html/proofer/checkable.rb +0 -164
  30. data/lib/html/proofer/checks/favicon.rb +0 -21
  31. data/lib/html/proofer/checks/html.rb +0 -40
  32. data/lib/html/proofer/checks/images.rb +0 -57
  33. data/lib/html/proofer/checks/links.rb +0 -107
  34. data/lib/html/proofer/checks/scripts.rb +0 -39
  35. data/lib/html/proofer/log.rb +0 -38
  36. data/lib/html/proofer/url_validator.rb +0 -135
  37. data/lib/html/proofer/utils.rb +0 -24
  38. data/lib/html/proofer/version.rb +0 -6
  39. data/lib/html/proofer.rb +0 -189
  40. data/script/bootstrap +0 -5
  41. data/spec/html/proofer/checkable_spec.rb +0 -47
  42. data/spec/html/proofer/command_spec.rb +0 -88
  43. data/spec/html/proofer/favicon_spec.rb +0 -47
  44. data/spec/html/proofer/fixtures/favicon/favicon_absent.html +0 -4
  45. data/spec/html/proofer/fixtures/favicon/favicon_absent_apple.html +0 -7
  46. data/spec/html/proofer/fixtures/favicon/favicon_broken.html +0 -7
  47. data/spec/html/proofer/fixtures/favicon/favicon_broken_but_ignored.html +0 -7
  48. data/spec/html/proofer/fixtures/favicon/favicon_present.html +0 -7
  49. data/spec/html/proofer/fixtures/favicon/favicon_present_shortcut.html +0 -7
  50. data/spec/html/proofer/fixtures/html/div_inside_head.html +0 -6
  51. data/spec/html/proofer/fixtures/html/html5_tags.html +0 -9
  52. data/spec/html/proofer/fixtures/html/invalid_tag.html +0 -3
  53. data/spec/html/proofer/fixtures/html/missing_closing_quotes.html +0 -5
  54. data/spec/html/proofer/fixtures/html/opening_and_ending_tag_mismatch.html +0 -7
  55. data/spec/html/proofer/fixtures/html/svg_elements.html +0 -729
  56. data/spec/html/proofer/fixtures/html/unescaped_ampersand_in_attribute.html +0 -4
  57. data/spec/html/proofer/fixtures/html/unmatched_end_tag.html +0 -5
  58. data/spec/html/proofer/fixtures/images/emptyImageAltText.html +0 -10
  59. data/spec/html/proofer/fixtures/images/existingImageExternal.html +0 -9
  60. data/spec/html/proofer/fixtures/images/gpl.png +0 -0
  61. data/spec/html/proofer/fixtures/images/ignorableAltViaOptions.html +0 -13
  62. data/spec/html/proofer/fixtures/images/ignorableImages.html +0 -10
  63. data/spec/html/proofer/fixtures/images/ignoreAltButNotLink.html +0 -13
  64. data/spec/html/proofer/fixtures/images/image_missing_protocol_invalid.html +0 -9
  65. data/spec/html/proofer/fixtures/images/image_missing_protocol_valid.html +0 -9
  66. data/spec/html/proofer/fixtures/images/missingImageAlt.html +0 -9
  67. data/spec/html/proofer/fixtures/images/missingImageAltText.html +0 -9
  68. data/spec/html/proofer/fixtures/images/missingImageExternal.html +0 -9
  69. data/spec/html/proofer/fixtures/images/missingImageInternal.html +0 -9
  70. data/spec/html/proofer/fixtures/images/missingImageSrc.html +0 -9
  71. data/spec/html/proofer/fixtures/images/relativeToSelf.html +0 -2
  72. data/spec/html/proofer/fixtures/images/rootRelativeImages.html +0 -2
  73. data/spec/html/proofer/fixtures/images/srcSetCheck.html +0 -7
  74. data/spec/html/proofer/fixtures/images/srcSetIgnorable.html +0 -13
  75. data/spec/html/proofer/fixtures/images/srcSetMissingAlt.html +0 -7
  76. data/spec/html/proofer/fixtures/images/srcSetMissingImage.html +0 -7
  77. data/spec/html/proofer/fixtures/images/terribleImageName.html +0 -9
  78. data/spec/html/proofer/fixtures/images/workingDataURIImage.html +0 -3
  79. data/spec/html/proofer/fixtures/links/_site/folder.html/index.html +0 -1
  80. data/spec/html/proofer/fixtures/links/anchors_in_pre.html +0 -12
  81. data/spec/html/proofer/fixtures/links/attributeWithDash.html +0 -8
  82. data/spec/html/proofer/fixtures/links/blank_mailto_link.html +0 -9
  83. data/spec/html/proofer/fixtures/links/blank_tel_link.html +0 -9
  84. data/spec/html/proofer/fixtures/links/brokenHashExternal.html +0 -11
  85. data/spec/html/proofer/fixtures/links/brokenHashInternal.html +0 -14
  86. data/spec/html/proofer/fixtures/links/brokenHashOnTheWeb.html +0 -2
  87. data/spec/html/proofer/fixtures/links/brokenInternalLink.html +0 -0
  88. data/spec/html/proofer/fixtures/links/brokenLinkExternal.html +0 -11
  89. data/spec/html/proofer/fixtures/links/brokenLinkInternal.html +0 -10
  90. data/spec/html/proofer/fixtures/links/brokenLinkWithNumber.html +0 -8
  91. data/spec/html/proofer/fixtures/links/brokenUnixLinks.html +0 -5
  92. data/spec/html/proofer/fixtures/links/broken_hash_with_query.html +0 -9
  93. data/spec/html/proofer/fixtures/links/checkSSLLinks.html +0 -1
  94. data/spec/html/proofer/fixtures/links/ensure_typhoeus_options.html +0 -1
  95. data/spec/html/proofer/fixtures/links/erstiebegr%C3%BC%C3%9Fung.html +0 -0
  96. data/spec/html/proofer/fixtures/links/erstiebegr/303/274/303/237ung.html +0 -0
  97. data/spec/html/proofer/fixtures/links/escape_pipes.html +0 -9
  98. data/spec/html/proofer/fixtures/links/file.foo +0 -11
  99. data/spec/html/proofer/fixtures/links/folder/anchorLink.html +0 -0
  100. data/spec/html/proofer/fixtures/links/folder/assets/barrel.png +0 -0
  101. data/spec/html/proofer/fixtures/links/folder/index.html +0 -0
  102. data/spec/html/proofer/fixtures/links/folder/multiples/catalog/file.html +0 -8
  103. data/spec/html/proofer/fixtures/links/folder/multiples/javadoc/file.html +0 -8
  104. data/spec/html/proofer/fixtures/links/folder/relativeImage.html +0 -2
  105. data/spec/html/proofer/fixtures/links/folder-php/index.php +0 -2
  106. data/spec/html/proofer/fixtures/links/githubHash.html +0 -1
  107. data/spec/html/proofer/fixtures/links/gpl.png +0 -0
  108. data/spec/html/proofer/fixtures/links/hashReferringToSelf.html +0 -44
  109. data/spec/html/proofer/fixtures/links/head_link_href.html +0 -7
  110. data/spec/html/proofer/fixtures/links/head_link_href_absent.html +0 -7
  111. data/spec/html/proofer/fixtures/links/head_link_href_empty.html +0 -7
  112. data/spec/html/proofer/fixtures/links/ignorableLinks.html +0 -13
  113. data/spec/html/proofer/fixtures/links/ignorableLinksViaOptions.html +0 -13
  114. data/spec/html/proofer/fixtures/links/invalid_mailto_link.html +0 -9
  115. data/spec/html/proofer/fixtures/links/javascript_link.html +0 -9
  116. data/spec/html/proofer/fixtures/links/linkToFolder.html +0 -9
  117. data/spec/html/proofer/fixtures/links/linkTranslatedViaHrefSwap.html +0 -9
  118. data/spec/html/proofer/fixtures/links/linkWithHttps.html +0 -9
  119. data/spec/html/proofer/fixtures/links/linkWithRedirect.html +0 -12
  120. data/spec/html/proofer/fixtures/links/link_directory_without_slash.html +0 -1
  121. data/spec/html/proofer/fixtures/links/link_missing_protocol_invalid.html +0 -9
  122. data/spec/html/proofer/fixtures/links/link_missing_protocol_valid.html +0 -9
  123. data/spec/html/proofer/fixtures/links/link_pointing_to_directory.html +0 -1
  124. data/spec/html/proofer/fixtures/links/links_in_pre.html +0 -13
  125. data/spec/html/proofer/fixtures/links/mailto_link.html +0 -9
  126. data/spec/html/proofer/fixtures/links/missingLinkHref.html +0 -9
  127. data/spec/html/proofer/fixtures/links/multipleProblems.html +0 -11
  128. data/spec/html/proofer/fixtures/links/nodupe.html +0 -1
  129. data/spec/html/proofer/fixtures/links/non_standard_characters.html +0 -10
  130. data/spec/html/proofer/fixtures/links/notarealhash.html +0 -0
  131. data/spec/html/proofer/fixtures/links/other_protocols.html +0 -4
  132. data/spec/html/proofer/fixtures/links/placeholder_with_empty_id.html +0 -1
  133. data/spec/html/proofer/fixtures/links/placeholder_with_id.html +0 -1
  134. data/spec/html/proofer/fixtures/links/placeholder_with_name.html +0 -1
  135. data/spec/html/proofer/fixtures/links/redirected_error.html +0 -1
  136. data/spec/html/proofer/fixtures/links/relativeLinks.html +0 -4
  137. data/spec/html/proofer/fixtures/links/rootLink/folder/index.html +0 -1
  138. data/spec/html/proofer/fixtures/links/rootLink/index.html +0 -1
  139. data/spec/html/proofer/fixtures/links/rootLink/rootLink.html +0 -9
  140. data/spec/html/proofer/fixtures/links/tel_link.html +0 -9
  141. data/spec/html/proofer/fixtures/links/urlencoded-href.html +0 -2
  142. data/spec/html/proofer/fixtures/links/utf8Link.html +0 -2
  143. data/spec/html/proofer/fixtures/resources/Screen Shot 2012-08-09 at 7.51.18 AM.png +0 -0
  144. data/spec/html/proofer/fixtures/resources/books/assets/getting-good-with-git.jpg +0 -0
  145. data/spec/html/proofer/fixtures/resources/books/assets/pro-git.jpg +0 -0
  146. data/spec/html/proofer/fixtures/resources/books/assets/version-control-with-git.jpg +0 -0
  147. data/spec/html/proofer/fixtures/resources/books/nestedRelativeImages.html +0 -5
  148. data/spec/html/proofer/fixtures/resources/gpl.png +0 -0
  149. data/spec/html/proofer/fixtures/scripts/ignorableLinksViaOptions.html +0 -4
  150. data/spec/html/proofer/fixtures/scripts/script.js +0 -1
  151. data/spec/html/proofer/fixtures/scripts/script_broken_external.html +0 -9
  152. data/spec/html/proofer/fixtures/scripts/script_content.html +0 -9
  153. data/spec/html/proofer/fixtures/scripts/script_content_absent.html +0 -9
  154. data/spec/html/proofer/fixtures/scripts/script_in_pre.html +0 -5
  155. data/spec/html/proofer/fixtures/scripts/script_missing_internal.html +0 -9
  156. data/spec/html/proofer/fixtures/scripts/script_valid_internal.html +0 -9
  157. data/spec/html/proofer/fixtures/sorting/issue/broken_image_one.html +0 -1
  158. data/spec/html/proofer/fixtures/sorting/issue/broken_image_two.html +0 -4
  159. data/spec/html/proofer/fixtures/sorting/path/multiple_issues.html +0 -11
  160. data/spec/html/proofer/fixtures/sorting/path/single_issue.html +0 -1
  161. data/spec/html/proofer/fixtures/sorting/status/a_404.html +0 -1
  162. data/spec/html/proofer/fixtures/sorting/status/broken_link.html +0 -3
  163. data/spec/html/proofer/fixtures/sorting/status/missing_redirect.html +0 -1
  164. data/spec/html/proofer/fixtures/utils/lang-jp.html +0 -1
  165. data/spec/html/proofer/html_spec.rb +0 -57
  166. data/spec/html/proofer/images_spec.rb +0 -147
  167. data/spec/html/proofer/links_spec.rb +0 -364
  168. data/spec/html/proofer/scripts_spec.rb +0 -47
  169. data/spec/html/proofer/utils_spec.rb +0 -14
  170. data/spec/html/proofer_spec.rb +0 -122
  171. data/spec/spec_helper.rb +0 -52
@@ -0,0 +1,165 @@
1
+ require 'addressable/uri'
2
+ require_relative './utils'
3
+
4
+ module HTMLProofer
5
+ # Represents the element currently being processed
6
+ class Element
7
+ include HTMLProofer::Utils
8
+
9
+ attr_reader :id, :name, :alt, :href, :link, :src, :line
10
+
11
+ def initialize(obj, check)
12
+ # Contruct readable ivars for every element
13
+ obj.attributes.each_pair do |attribute, value|
14
+ name = "#{attribute.tr('-:.', '_')}".to_sym
15
+ (class << self; self; end).send(:attr_reader, name)
16
+ instance_variable_set("@#{name}", value.value)
17
+ end
18
+
19
+ @text = obj.content
20
+ @check = check
21
+ @checked_paths = {}
22
+ @type = check.class.name
23
+ @line = obj.line
24
+
25
+ # fix up missing protocols
26
+ @href.insert 0, 'http:' if @href =~ %r{^//}
27
+ @src.insert 0, 'http:' if @src =~ %r{^//}
28
+ end
29
+
30
+ def url
31
+ url = @src || @srcset || @href || ''
32
+ return url if @check.options[:url_swap].empty?
33
+ swap(url, @check.options[:url_swap])
34
+ end
35
+
36
+ def valid?
37
+ !parts.nil?
38
+ end
39
+
40
+ def parts
41
+ @parts ||= Addressable::URI.parse url
42
+ rescue URI::Error, Addressable::URI::InvalidURIError
43
+ @parts = nil
44
+ end
45
+
46
+ def path
47
+ Addressable::URI.unencode parts.path unless parts.nil?
48
+ end
49
+
50
+ def hash
51
+ parts.fragment unless parts.nil?
52
+ end
53
+
54
+ def scheme
55
+ parts.scheme unless parts.nil?
56
+ end
57
+
58
+ # path is to an external server
59
+ def remote?
60
+ %w( http https ).include? scheme
61
+ end
62
+
63
+ def non_http_remote?
64
+ !scheme.nil? && !remote?
65
+ end
66
+
67
+ def ignore?
68
+ return true if @data_proofer_ignore
69
+
70
+ return true if url.match(/^javascript:/)
71
+
72
+ # ignore base64 encoded images
73
+ if %w(ImageCheck FaviconCheck).include? @type
74
+ return true if url.match(/^data:image/)
75
+ end
76
+
77
+ # ignore user defined URLs
78
+ return true if ignores_pattern_check(@check.options[:url_ignore])
79
+
80
+ # ignore user defined alts
81
+ return false unless 'ImageCheck' == @type
82
+ return true if ignores_pattern_check(@check.options[:alt_ignore])
83
+ end
84
+
85
+ def ignore_empty_alt?
86
+ @check.options[:empty_alt_ignore]
87
+ end
88
+
89
+ def allow_hash_href?
90
+ @check.options[:allow_hash_href]
91
+ end
92
+
93
+ # path is external to the file
94
+ def external?
95
+ !internal?
96
+ end
97
+
98
+ # path is an anchor or a query
99
+ def internal?
100
+ url.start_with? '#', '?'
101
+ end
102
+
103
+ def file_path
104
+ return if path.nil?
105
+
106
+ path_dot_ext = ''
107
+
108
+ if @check.options[:assume_extension]
109
+ path_dot_ext = path + @check.options[:extension]
110
+ end
111
+
112
+ if path =~ %r{^/} # path relative to root
113
+ base = File.directory?(@check.src) ? @check.src : File.dirname(@check.src)
114
+ elsif File.exist?(File.expand_path(path, @check.src)) || File.exist?(File.expand_path(path_dot_ext, @check.src)) # relative links, path is a file
115
+ base = File.dirname @check.path
116
+ elsif File.exist?(File.join(File.dirname(@check.path), path)) || File.exist?(File.join(File.dirname(@check.path), path_dot_ext)) # relative links in nested dir, path is a file
117
+ base = File.dirname @check.path
118
+ else # relative link, path is a directory
119
+ base = @check.path
120
+ end
121
+
122
+ file = File.join base, path
123
+
124
+ # implicit index support
125
+ if File.directory?(file) && !unslashed_directory?(file)
126
+ file = File.join file, @check.options[:directory_index_file]
127
+ elsif @check.options[:assume_extension] && File.file?("#{file}#{@check.options[:extension]}")
128
+ file = "#{file}#{@check.options[:extension]}"
129
+ end
130
+
131
+ file
132
+ end
133
+
134
+ # checks if a file exists relative to the current pwd
135
+ def exists?
136
+ return @checked_paths[absolute_path] if @checked_paths.key? absolute_path
137
+ @checked_paths[absolute_path] = File.exist? absolute_path
138
+ end
139
+
140
+ def absolute_path
141
+ path = file_path || @check.path
142
+ File.expand_path path, Dir.pwd
143
+ end
144
+
145
+ def ignores_pattern_check(links)
146
+ links.each do |ignore|
147
+ if ignore.is_a? String
148
+ return true if ignore == url
149
+ elsif ignore.is_a? Regexp
150
+ return true if ignore =~ url
151
+ end
152
+ end
153
+
154
+ false
155
+ end
156
+
157
+ def unslashed_directory?(file)
158
+ File.directory?(file) && !file.end_with?(File::SEPARATOR) && !follow_location?
159
+ end
160
+
161
+ def follow_location?
162
+ @check.options[:typhoeus] && @check.options[:typhoeus][:followlocation]
163
+ end
164
+ end
165
+ end
@@ -1,18 +1,16 @@
1
- # encoding: utf-8
2
- class HTML::Proofer::CheckRunner
3
-
1
+ module HTMLProofer
4
2
  class Issue
5
- attr_reader :path, :desc, :status, :line_number
3
+ attr_reader :path, :desc, :status, :line
6
4
 
7
- def initialize(path, desc, line_number = nil, status = -1)
8
- @line_number = line_number.nil? ? '' : " (line #{line_number})"
5
+ def initialize(path, desc, line: nil, status: -1)
6
+ @line = line.nil? ? '' : " (line #{line})"
9
7
  @path = path
10
8
  @desc = desc
11
9
  @status = status
12
10
  end
13
11
 
14
12
  def to_s
15
- "#{@path}: #{@desc}#{@line_number}"
13
+ "#{@path}: #{@desc}#{@line}"
16
14
  end
17
15
  end
18
16
 
@@ -48,13 +46,13 @@ class HTML::Proofer::CheckRunner
48
46
 
49
47
  sorted_issues.each do |issue|
50
48
  if matcher != issue.send(first_report)
51
- @logger.log :error, :red, "- #{issue.send(first_report)}"
49
+ @logger.log :error, "- #{issue.send(first_report)}"
52
50
  matcher = issue.send(first_report)
53
51
  end
54
52
  if first_report == :status
55
- @logger.log :error, :red, " * #{issue}"
53
+ @logger.log :error, " * #{issue}"
56
54
  else
57
- @logger.log :error, :red, " * #{issue.send(second_report)}#{issue.line_number}"
55
+ @logger.log :error, " * #{issue.send(second_report)}#{issue.line}"
58
56
  end
59
57
  end
60
58
  end
@@ -0,0 +1,49 @@
1
+ require 'yell'
2
+ require 'colored'
3
+
4
+ module HTMLProofer
5
+ class Log
6
+ include Yell::Loggable
7
+
8
+ def initialize(log_level)
9
+ @logger = Yell.new(:format => false, \
10
+ :name => 'HTMLProofer', \
11
+ :level => "gte.#{log_level}") do |l|
12
+ l.adapter :stdout, :level => [:debug, :info, :warn]
13
+ l.adapter :stderr, :level => [:error, :fatal]
14
+ end
15
+ end
16
+
17
+ def log(level, message)
18
+ color = case level
19
+ when :debug
20
+ :light_blue
21
+ when :info
22
+ :blue
23
+ when :warn
24
+ :yellow
25
+ when :error, :fatal
26
+ :red
27
+ end
28
+
29
+ log_with_color(level, color, message)
30
+ end
31
+
32
+ def log_with_color(level, color, message)
33
+ @logger.send level, colorize(color, message)
34
+ end
35
+
36
+ def colorize(color, message)
37
+ if $stdout.isatty && $stderr.isatty
38
+ Colored.colorize(message, foreground: color)
39
+ else
40
+ message
41
+ end
42
+ end
43
+
44
+ # dumb override to play nice with Typhoeus/Ethon
45
+ def debug(message = nil)
46
+ log(:debug, message) unless message.nil?
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,160 @@
1
+ module HTMLProofer
2
+ class Runner
3
+ include HTMLProofer::Utils
4
+
5
+ attr_reader :options, :external_urls
6
+
7
+ def initialize(src, opts = {})
8
+ @src = src
9
+
10
+ @options = HTMLProofer::Configuration::PROOFER_DEFAULTS.merge(opts)
11
+
12
+ @options[:typhoeus] = HTMLProofer::Configuration::TYPHOEUS_DEFAULTS.merge(opts[:typhoeus] || {})
13
+ @options[:hydra] = HTMLProofer::Configuration::HYDRA_DEFAULTS.merge(opts[:hydra] || {})
14
+
15
+ @options[:parallel] = HTMLProofer::Configuration::PARALLEL_DEFAULTS.merge(opts[:parallel] || {})
16
+ @options[:validation] = HTMLProofer::Configuration::VALIDATION_DEFAULTS.merge(opts[:validation] || {})
17
+ @options[:cache] = HTMLProofer::Configuration::CACHE_DEFAULTS.merge(opts[:cache] || {})
18
+
19
+ @type = @options.delete(:type)
20
+ @logger = HTMLProofer::Log.new(@options[:log_level])
21
+
22
+ if !@options[:cache].empty? && !File.exist?(STORAGE_DIR)
23
+ FileUtils.mkdir_p(STORAGE_DIR)
24
+ end
25
+
26
+ @failures = []
27
+ end
28
+
29
+ def run
30
+ @logger.log :info, "Running #{checks} on #{@src} on *#{@options[:extension]}... \n\n"
31
+
32
+ if @type == :links
33
+ check_list_of_links unless @options[:disable_external]
34
+ else
35
+ check_files
36
+ file_text = pluralize(files.length, 'file', 'files')
37
+ @logger.log :info, "Ran on #{file_text}!\n\n"
38
+ end
39
+
40
+ if @failures.empty?
41
+ @logger.log_with_color :info, :green, 'HTML-Proofer finished successfully.'
42
+ else
43
+ print_failed_tests
44
+ end
45
+ end
46
+
47
+ def check_list_of_links
48
+ if @options[:url_swap]
49
+ @src = @src.map do |url|
50
+ swap(url, @options[:url_swap])
51
+ end
52
+ end
53
+ @external_urls = Hash[*@src.map { |s| [s, nil] }.flatten]
54
+ validate_urls
55
+ end
56
+
57
+ # Collects any external URLs found in a directory of files. Also collectes
58
+ # every failed test from process_files.
59
+ # Sends the external URLs to Typhoeus for batch processing.
60
+ def check_files
61
+ @external_urls = {}
62
+
63
+ process_files.each do |item|
64
+ @external_urls.merge!(item[:external_urls])
65
+ @failures.concat(item[:failures])
66
+ end
67
+
68
+ # TODO: lazy. if we're checking only external links,
69
+ # we'll just trash all the failed tests. really, we should
70
+ # just not run those other checks at all.
71
+ if @options[:external_only]
72
+ @failures = []
73
+ validate_urls
74
+ elsif !@options[:disable_external]
75
+ validate_urls
76
+ end
77
+ end
78
+
79
+ # Walks over each implemented check and runs them on the files, in parallel.
80
+ def process_files
81
+ if @options[:parallel].empty?
82
+ files.map { |path| check_path(path) }
83
+ else
84
+ Parallel.map(files, @options[:parallel]) { |path| check_path(path) }
85
+ end
86
+ end
87
+
88
+ def check_path(path)
89
+ result = { :external_urls => {}, :failures => [] }
90
+ html = create_nokogiri(path)
91
+
92
+ @src = [@src] if @type == :file
93
+
94
+ @src.each do |src|
95
+ checks.each do |klass|
96
+ @logger.log :debug, "Checking #{klass.to_s.downcase} on #{path} ..."
97
+ check = Object.const_get(klass).new(src, path, html, @options)
98
+ check.run
99
+ result[:external_urls].merge!(check.external_urls)
100
+ result[:failures].concat(check.issues)
101
+ end
102
+ end
103
+ result
104
+ end
105
+
106
+ def validate_urls
107
+ url_validator = HTMLProofer::UrlValidator.new(@logger, @external_urls, @options)
108
+ @failures.concat(url_validator.run)
109
+ @external_urls = url_validator.external_urls
110
+ end
111
+
112
+ def files
113
+ @files ||= if @type == :directory
114
+ @src.map do |src|
115
+ pattern = File.join(src, '**', "*#{@options[:extension]}")
116
+ files = Dir.glob(pattern).select { |fn| File.file? fn }
117
+ files.reject { |f| ignore_file?(f) }
118
+ end.flatten
119
+ elsif @type == :file && File.extname(@src) == @options[:extension]
120
+ [@src].reject { |f| ignore_file?(f) }
121
+ else
122
+ []
123
+ end
124
+ end
125
+
126
+ def ignore_file?(file)
127
+ @options[:file_ignore].each do |pattern|
128
+ return true if pattern.is_a?(String) && pattern == file
129
+ return true if pattern.is_a?(Regexp) && pattern =~ file
130
+ end
131
+
132
+ false
133
+ end
134
+
135
+ def checks
136
+ return @checks unless @checks.nil?
137
+ @checks = HTMLProofer::Check.subchecks.map(&:name)
138
+ @checks.delete('FaviconCheck') unless @options[:check_favicon]
139
+ @checks.delete('HtmlCheck') unless @options[:check_html]
140
+ @options[:checks_to_ignore].each { |ignored| @checks.delete(ignored) }
141
+ @checks
142
+ end
143
+
144
+ def failed_tests
145
+ result = []
146
+ return result if @failures.empty?
147
+ @failures.each { |f| result << f.to_s }
148
+ result
149
+ end
150
+
151
+ def print_failed_tests
152
+ sorted_failures = SortedIssues.new(@failures, @options[:error_sort], @logger)
153
+
154
+ sorted_failures.sort_and_report
155
+ count = @failures.length
156
+ failure_text = pluralize(count, 'failure', 'failures')
157
+ fail @logger.colorize :red, "HTML-Proofer found #{failure_text}!"
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,218 @@
1
+ require 'typhoeus'
2
+ require 'uri'
3
+ require_relative './utils'
4
+ require_relative './cache'
5
+
6
+ module HTMLProofer
7
+ class UrlValidator
8
+ include HTMLProofer::Utils
9
+
10
+ attr_reader :external_urls
11
+
12
+ def initialize(logger, external_urls, options)
13
+ @logger = logger
14
+ @external_urls = external_urls
15
+ @failed_tests = []
16
+ @options = options
17
+ @hydra = Typhoeus::Hydra.new(@options[:hydra])
18
+ @cache = Cache.new(@logger, @options[:cache])
19
+ end
20
+
21
+ def run
22
+ @external_urls = remove_query_values
23
+
24
+ if @cache.use_cache?
25
+ urls_to_check = load_cache
26
+ external_link_checker(urls_to_check)
27
+ @cache.write
28
+ else
29
+ external_link_checker(@external_urls)
30
+ end
31
+
32
+ @failed_tests
33
+ end
34
+
35
+ def remove_query_values
36
+ return nil if @external_urls.nil?
37
+ paths_with_queries = {}
38
+ iterable_external_urls = @external_urls.dup
39
+ @external_urls.keys.each do |url|
40
+ uri = begin
41
+ Addressable::URI.parse(url)
42
+ rescue URI::Error, Addressable::URI::InvalidURIError
43
+ @logger.log :error, "#{url} is an invalid URL"
44
+ nil
45
+ end
46
+ next if uri.nil? || uri.query.nil?
47
+ iterable_external_urls.delete(url) unless new_url_query_values?(uri, paths_with_queries)
48
+ end
49
+ iterable_external_urls
50
+ end
51
+
52
+ # remember queries we've seen, ignore future ones
53
+ def new_url_query_values?(uri, paths_with_queries)
54
+ queries = uri.query_values.keys.join('-')
55
+ domain_path = extract_domain_path(uri)
56
+ if paths_with_queries[domain_path].nil?
57
+ paths_with_queries[domain_path] = [queries]
58
+ true
59
+ elsif !paths_with_queries[domain_path].include?(queries)
60
+ paths_with_queries[domain_path] << queries
61
+ true
62
+ else
63
+ false
64
+ end
65
+ end
66
+
67
+ def extract_domain_path(uri)
68
+ uri.host + uri.path
69
+ end
70
+
71
+ def load_cache
72
+ cache_count = @cache.size
73
+ cache_text = pluralize(cache_count, 'link', 'links')
74
+
75
+ @logger.log :info, "Found #{cache_text} in the cache..."
76
+
77
+ @cache.retrieve_urls(@external_urls)
78
+ end
79
+
80
+ # Proofer runs faster if we pull out all the external URLs and run the checks
81
+ # at the end. Otherwise, we're halting the consuming process for every file during
82
+ # `process_files`.
83
+ #
84
+ # In addition, sorting the list lets libcurl keep connections to the same hosts alive.
85
+ #
86
+ # Finally, we'll first make a HEAD request, rather than GETing all the contents.
87
+ # If the HEAD fails, we'll fall back to GET, as some servers are not configured
88
+ # for HEAD. If we've decided to check for hashes, we must do a GET--HEAD is
89
+ # not available as an option.
90
+ def external_link_checker(external_urls)
91
+ external_urls = Hash[external_urls.sort]
92
+
93
+ count = external_urls.length
94
+ check_text = pluralize(count, 'external link', 'external links')
95
+ @logger.log :info, "Checking #{check_text}..."
96
+
97
+ # Route log from Typhoeus/Ethon to our own logger
98
+ Ethon.logger = @logger
99
+
100
+ establish_queue(external_urls)
101
+
102
+ @hydra.run
103
+ end
104
+
105
+ def establish_queue(external_urls)
106
+ external_urls.each_pair do |url, filenames|
107
+ url = begin
108
+ clean_url(url)
109
+ rescue URI::Error, Addressable::URI::InvalidURIError
110
+ add_external_issue(filenames, "#{url} is an invalid URL")
111
+ next
112
+ end
113
+
114
+ method = if hash?(url) && @options[:check_external_hash]
115
+ :get
116
+ else
117
+ :head
118
+ end
119
+ queue_request(method, url, filenames)
120
+ end
121
+ end
122
+
123
+ def clean_url(href)
124
+ Addressable::URI.parse(href).normalize
125
+ end
126
+
127
+ def queue_request(method, href, filenames)
128
+ opts = @options[:typhoeus].merge({ :method => method })
129
+ request = Typhoeus::Request.new(href, opts)
130
+ request.on_complete { |response| response_handler(response, filenames) }
131
+ @hydra.queue request
132
+ end
133
+
134
+ def response_handler(response, filenames)
135
+ effective_url = response.options[:effective_url]
136
+ href = response.request.base_url.to_s
137
+ method = response.request.options[:method]
138
+ response_code = response.code
139
+
140
+ debug_msg = "Received a #{response_code} for #{href}"
141
+ debug_msg << " in #{filenames.join(' ')}" unless filenames.nil?
142
+ @logger.log :debug, debug_msg
143
+
144
+ return if @options[:http_status_ignore].include?(response_code)
145
+
146
+ if response_code.between?(200, 299)
147
+ unless check_hash_in_2xx_response(href, effective_url, response, filenames)
148
+ @cache.add(href, filenames, response_code)
149
+ end
150
+ elsif response.timed_out?
151
+ handle_timeout(href, filenames, response_code)
152
+ elsif response_code == 0
153
+ handle_failure(href, filenames, response_code)
154
+ elsif method == :head
155
+ queue_request(:get, href, filenames)
156
+ else
157
+ return if @options[:only_4xx] && !response_code.between?(400, 499)
158
+ # Received a non-successful http response.
159
+ msg = "External link #{href} failed: #{response_code} #{response.return_message}"
160
+ add_external_issue(filenames, msg, response_code)
161
+ @cache.add(href, filenames, response_code, msg)
162
+ end
163
+ end
164
+
165
+ # Even though the response was a success, we may have been asked to check
166
+ # if the hash on the URL exists on the page
167
+ def check_hash_in_2xx_response(href, effective_url, response, filenames)
168
+ return false if @options[:only_4xx]
169
+ return false unless @options[:check_external_hash]
170
+ return false unless (hash = hash?(href))
171
+
172
+ body_doc = create_nokogiri(response.body)
173
+
174
+ # user-content is a special addition by GitHub.
175
+ xpath = %(//*[@name="#{hash}"]|//*[@id="#{hash}"])
176
+ if URI.parse(href).host.match(/github\.com/i)
177
+ xpath << %(|//*[@name="user-content-#{hash}"]|//*[@id="user-content-#{hash}"])
178
+ end
179
+
180
+ return unless body_doc.xpath(xpath).empty?
181
+
182
+ msg = "External link #{href} failed: #{effective_url} exists, but the hash '#{hash}' does not"
183
+ add_external_issue(filenames, msg, response.code)
184
+ @cache.add(href, filenames, response.code, msg)
185
+ true
186
+ end
187
+
188
+ def handle_timeout(href, filenames, response_code)
189
+ msg = "External link #{href} failed: got a time out (response code #{response_code})"
190
+ @cache.add(href, filenames, 0, msg)
191
+ return if @options[:only_4xx]
192
+ add_external_issue(filenames, msg, response_code)
193
+ end
194
+
195
+ def handle_failure(href, filenames, response_code)
196
+ msg = "External link #{href} failed: response code #{response_code} means something's wrong"
197
+ @cache.add(href, filenames, 0, msg)
198
+ return if @options[:only_4xx]
199
+ add_external_issue(filenames, msg, response_code)
200
+ end
201
+
202
+ def add_external_issue(filenames, desc, status = nil)
203
+ # possible if we're checking an array of links
204
+ if filenames.nil?
205
+ @failed_tests << Issue.new('', desc, status: status)
206
+ else
207
+ filenames.each { |f| @failed_tests << Issue.new(f, desc, status: status) }
208
+ end
209
+ end
210
+
211
+ # Does the URL have a hash?
212
+ def hash?(url)
213
+ URI.parse(url).fragment
214
+ rescue URI::InvalidURIError
215
+ false
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,40 @@
1
+ require 'nokogiri'
2
+
3
+ module HTMLProofer
4
+ module Utils
5
+ STORAGE_DIR = File.join('tmp', '.htmlproofer')
6
+
7
+ def pluralize(count, single, plural)
8
+ "#{count} " << (count == 1 ? single : plural)
9
+ end
10
+
11
+ def create_nokogiri(path)
12
+ if File.exist? path
13
+ content = File.open(path).read
14
+ else
15
+ content = path
16
+ end
17
+
18
+ Nokogiri::HTML(clean_content(content))
19
+ end
20
+ module_function :create_nokogiri
21
+
22
+ def swap(href, replacement)
23
+ replacement.each do |link, replace|
24
+ href = href.gsub(link, replace)
25
+ end
26
+ href
27
+ end
28
+ module_function :swap
29
+
30
+ # address a problem with Nokogiri's parsing URL entities
31
+ # problem from http://git.io/vBYU1
32
+ # solution from http://git.io/vBYUi
33
+ def clean_content(string)
34
+ string.gsub(%r{https?://([^>]+)}i) do |url|
35
+ url.gsub(/&(?!amp;)/, '&amp;')
36
+ end
37
+ end
38
+ module_function :clean_content
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module HTMLProofer
2
+ VERSION = '3.0.0'
3
+ end