blinkr 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,66 @@
1
+ .h2
2
+ | Duplicate Descriptions
3
+ .label.label-primary.pull-right = descriptions.length
4
+ - descriptions.each_with_index do |(description, pages), index|
5
+
6
+ .panel.panel-danger
7
+ .panel-heading
8
+ a> data-toggle="collapse" href="#meta-d-#{index}" id="control-meta-d-#{index}"
9
+ i.fa.fa-caret-square-o-right>
10
+ ' #{description}
11
+ .panel-body.collapse id="meta-d-#{index}"
12
+ ul.list-group
13
+ - pages.each do |url, page|
14
+ li.list-group-item
15
+ ' #{url}
16
+ a> href="#{url}" target='_blank'
17
+ i.fa.fa-external-link
18
+ a href="view-source:#{url}" target='_blank'
19
+ i.fa.fa-file-code-o
20
+ javascript:
21
+ $('#meta-d-#{index}').on('show.bs.collapse', function () {
22
+ $('a#control-meta-d-#{index}').html('<i class="fa fa-caret-square-o-down"></i>');
23
+ });
24
+ $('#meta-d-#{index}').on('hide.bs.collapse', function () {
25
+ $('a#control-meta-d-#{index}').html('<i class="fa fa-caret-square-o-right"></i>');
26
+ });
27
+
28
+ - if descriptions.length == 0
29
+ .panel.panel-success
30
+ .panel-heading
31
+ a id='resources'
32
+ | No duplicate descriptions detected
33
+
34
+ .h2
35
+ | Duplicate Titles
36
+ .label.label-primary.pull-right = titles.length
37
+ - titles.each_with_index do |(title, pages), index|
38
+
39
+ .panel.panel-danger
40
+ .panel-heading
41
+ a> data-toggle="collapse" href="#meta-t-#{index}" id="control-meta-t-#{index}"
42
+ i.fa.fa-caret-square-o-right>
43
+ ' #{title}
44
+ .panel-body.collapse id="meta-t-#{index}"
45
+ ul.list-group
46
+ - pages.each do |url, page|
47
+ li.list-group-item
48
+ ' #{url}
49
+ a> href="#{url}" target='_blank'
50
+ i.fa.fa-external-link
51
+ a href="view-source:#{url}" target='_blank'
52
+ i.fa.fa-file-code-o
53
+ javascript:
54
+ $('#meta-t-#{index}').on('show.bs.collapse', function () {
55
+ $('a#control-meta-t-#{index}').html('<i class="fa fa-caret-square-o-down"></i>');
56
+ });
57
+ $('#meta-t-#{index}').on('hide.bs.collapse', function () {
58
+ $('a#control-meta-t-#{index}').html('<i class="fa fa-caret-square-o-right"></i>');
59
+ });
60
+
61
+ - if titles.length == 0
62
+ .panel.panel-success
63
+ .panel-heading
64
+ a id='resources'
65
+ | No duplicate titles detected
66
+
@@ -0,0 +1,84 @@
1
+ require 'ostruct'
2
+ require 'slim'
3
+
4
+ module Blinkr
5
+ module Extensions
6
+ class Meta
7
+
8
+ TMPL = File.expand_path('meta.html.slim', File.dirname(__FILE__))
9
+
10
+ def initialize config
11
+ @config = config
12
+ @descriptions = {}
13
+ @titles = {}
14
+ end
15
+
16
+ def collect page
17
+ description page
18
+ title page
19
+ end
20
+
21
+ def analyze context, typhoeus
22
+ context.duplicate_descriptions = @descriptions.reject{ |description, pages| pages.length <= 1 }
23
+ context.duplicate_titles = @titles.reject{ |title, pages| pages.length <= 1 }
24
+ end
25
+
26
+ def append context
27
+ Slim::Template.new(TMPL).render(OpenStruct.new({ :descriptions => context.duplicate_descriptions, :titles => context.duplicate_titles }))
28
+ end
29
+
30
+ private
31
+
32
+ def title page
33
+ elms = page.body.css('title')
34
+ if elms.length > 1
35
+ snippets = []
36
+ lines = []
37
+ elms.each do |elm|
38
+ lines << elm.line
39
+ snippets << elm.to_s
40
+ end
41
+ page.errors << OpenStruct.new({ :severity => 'info', :category => 'HTML Compatibility/Correctness', :type => '<title> tag declared more than once', :title => %Q{<title> declared more than once (lines #{lines.join(', ')})}, :message => %Q{<title> declared more than onc}, :snippet => snippets.join('\n'), :icon => 'fa-header' })
42
+ elsif elms.empty?
43
+ page.errors << OpenStruct.new({ :severity => 'warning', :category => 'SEO', :type => '<title> tag missing', :title => %Q{<title> tag missing}, :message => %Q{<title> tag missing}, :icon => 'fa-header' })
44
+ else
45
+ title = elms.first.text
46
+ @titles[title] ||= {}
47
+ @titles[title][page.response.effective_url] = page
48
+ if title.length < 20
49
+ page.errors << OpenStruct.new({ :severity => 'info', :category => 'SEO', :type => 'page title too short', :title => %Q{<title> too short (line #{elms.first.line})}, :message => %Q{<title> too short (< 20 characters)}, :snippet => elms.first.to_s, :icon => 'fa-header' })
50
+ end
51
+ if title.length > 55
52
+ page.errors << OpenStruct.new({ :severity => 'info', :category => 'SEO', :type => 'page title too long', :title => %Q{<title> too long (line #{elms.first.line})}, :message => %Q{<title> too long (> 55 characters)}, :snippet => elms.first.to_s, :icon => 'fa-header' })
53
+ end
54
+ end
55
+ end
56
+
57
+ def description page
58
+ elms = page.body.css('meta[name=description]')
59
+ if elms.length > 1
60
+ snippets = []
61
+ lines = []
62
+ elms.each do |elm|
63
+ lines << elm.line
64
+ snippets << elm.to_s
65
+ end
66
+ page.errors << OpenStruct.new({ :severity => 'info', :category => 'HTML Compatibility/Correctness', :type => '<meta name="description"> tag declared more than once', :title => %Q{<meta name="description"> tag declared more than once (lines #{lines.join(', ')})}, :message => %Q{<meta name="description"> tag declared more than once}, :snippet => snippets.join('\n'), :icon => 'fa-header' })
67
+ elsif elms.empty?
68
+ page.errors << OpenStruct.new({ :severity => 'info', :category => 'SEO', :type => '<meta name="description"> tag missing', :title => %Q{<meta name="description"> tag missing}, :message => %Q{<meta name="description"> tag missing}, :icon => 'fa-header' })
69
+ else
70
+ desc = elms.first['content']
71
+ @descriptions[desc] ||= {}
72
+ @descriptions[desc][page.response.effective_url] = page
73
+ if desc.length < 60
74
+ page.errors << OpenStruct.new({ :severity => 'info', :category => 'SEO', :type => '<meta name="description"> too short', :title => %Q{<meta name="description"> too short (lines #{elms.first.line})}, :message => %Q{<meta name="description"> too short (< 60 characters)}, :snippet => elms.first.to_s, :icon => 'fa-header' })
75
+ end
76
+ if desc.length > 115
77
+ page.errors << OpenStruct.new({ :severity => 'info', :category => 'SEO', :type => '<meta name="description"> too long', :title => %Q{<meta name="description"> too long (lines #{elms.first.line})}, :message => %Q{<meta name="description"> too long (> 115 characters)}, :snippet => elms.first.to_s, :icon => 'fa-header' })
78
+ end
79
+ end
80
+ end
81
+
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,39 @@
1
+ Dir[ File.join( File.dirname(__FILE__), '*.rb' ) ].each do |f|
2
+ begin
3
+ require f
4
+ rescue LoadError => e
5
+ $LOG.warn "Missing required dependency to activate optional built-in extension #{File.basename(f)}\n #{e}" if $LOG.debug?
6
+ rescue StandardError => e
7
+ $LOG.warn "Missing runtime configuration to activate optional built-in extension #{File.basename(f)}\n #{e}" if $LOG.debug?
8
+ end
9
+ end
10
+
11
+ module Blinkr
12
+ module Extensions
13
+ class Pipeline
14
+
15
+ attr_reader :extensions
16
+
17
+ def initialize(&block)
18
+ @extensions = []
19
+ @block = block
20
+ end
21
+
22
+ def load config
23
+ begin
24
+ instance_exec config, &@block if @block && @block.arity == 1
25
+ instance_exec &@block if @block && @block.arity == 0
26
+ self
27
+ rescue Exception => e
28
+ abort("Failed to initialize pipeline: #{e}")
29
+ end
30
+ end
31
+
32
+ def extension(ext)
33
+ @extensions << ext
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+
@@ -0,0 +1,20 @@
1
+ module Blinkr
2
+ module Extensions
3
+ class Resources
4
+
5
+ def initialize config
6
+ @config = config
7
+ end
8
+
9
+ def collect page
10
+ page.resource_errors.each do |error|
11
+ start = error['errorString'].rindex('server replied: ')
12
+ message = error['errorString'].slice(start.nil? ? 0 : start + 16, error['errorString'].length) unless error['errorString'].nil?
13
+ code = error['errorCode'].nil? ? nil : error['errorCode'].to_i
14
+ page.errors << OpenStruct.new({ :severity => 'danger', :category => 'Resources missing', :type => 'Resource loading error', :title => error['url'], :code => code, :message => message, :icon => 'fa-file-image-o' })
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ require 'uri'
2
+
3
+ module Blinkr
4
+ module HttpUtils
5
+
6
+ def sanitize dest, src
7
+ # src is the page that tried to load the URL
8
+ # URI fails to handle #! style fragments, so we chomp them
9
+ src = src[0, src.rindex('#!')] unless src.rindex('#!').nil?
10
+ src_uri = URI(src)
11
+
12
+ # Remove the query and fragment from the SRC URI, as we are going to use it to resolve the relative dest URIs
13
+ src_uri.query = nil
14
+ src_uri.fragment = nil
15
+
16
+ # base is the web root of the site
17
+ base_uri = URI(@config.base_url)
18
+
19
+ begin
20
+
21
+ # dest is the URL we are trying to load
22
+ dest_uri = URI(dest)
23
+ dest_uri.fragment = nil if @config.ignore_fragments
24
+
25
+ # If we have a relative URI, or just a fragment, join what we have to our base URL
26
+ dest_uri = URI.join(src_uri, dest) if ( empty?(dest_uri.path) && !empty?(dest_uri.fragment ) ) || dest_uri.relative?
27
+
28
+ # If we have an absolute path URI, join it to the base URL
29
+ dest_uri = URI.join(base_uri.scheme, base_uri.hostname, base_uri.port, dest_uri) if empty?(dest_uri.scheme) && empty?(dest_uri.hostname)
30
+
31
+ dest = dest_uri.to_s
32
+ rescue URI::InvalidURIError, URI::InvalidComponentError, URI::BadURIError
33
+ end
34
+ dest.chomp('#').chomp('index.html')
35
+ end
36
+
37
+ def retry? resp
38
+ resp.timed_out? || (resp.code == 0 && [ "Server returned nothing (no headers, no data)", "SSL connect error", "Failure when receiving data from the peer" ].include?(resp.return_message) )
39
+ end
40
+
41
+ private
42
+
43
+ def empty? str
44
+ str.nil? || str.empty?
45
+ end
46
+
47
+ end
48
+ end
49
+
@@ -0,0 +1,65 @@
1
+ require 'typhoeus'
2
+ require 'ostruct'
3
+ require 'tempfile'
4
+ require 'blinkr/http_utils'
5
+ require 'parallel'
6
+
7
+
8
+ module Blinkr
9
+ class PhantomJSWrapper
10
+ include HttpUtils
11
+
12
+ SNAP_JS = File.expand_path('snap.js', File.dirname(__FILE__))
13
+
14
+ attr_reader :count
15
+
16
+ def initialize config, context
17
+ @config = config.validate
18
+ @context = context
19
+ @count = 0
20
+ end
21
+
22
+ def process_all urls, limit, &block
23
+ Parallel.each(urls, :in_threads => @config.phantomjs_threads) do |url|
24
+ process url, limit, &block
25
+ end
26
+ end
27
+
28
+ def process url, limit, &block
29
+ _process url, limit, limit, &block
30
+ end
31
+
32
+ def name
33
+ 'phantomjs'
34
+ end
35
+
36
+ private
37
+
38
+ def _process url, limit, max, &block
39
+ unless @config.skipped? url
40
+ Tempfile.open('blinkr') do|f|
41
+ if system "phantomjs #{SNAP_JS} #{url} #{@config.viewport} #{f.path}"
42
+ json = JSON.load(File.read(f.path))
43
+ response = Typhoeus::Response.new(code: 200, body: json['content'], effective_url: json['url'], mock: true)
44
+ response.request = Typhoeus::Request.new(url)
45
+ Typhoeus.stub(url).and_return(response)
46
+ block.call response, json['resourceErrors'], json['javascriptErrors']
47
+ else
48
+ if limit > 1
49
+ puts "Loading #{url} via phantomjs (attempt #{max - limit + 2} of #{max})" if verbose
50
+ _process url, limit - 1, max, &block
51
+ else
52
+ puts "Loading #{url} via phantomjs failed" if @config.verbose
53
+ response = Typhoeus::Response.new(code: 0, status_message: "Server timed out after #{max} retries", mock: true)
54
+ response.request = Typhoeus::Request.new(url)
55
+ Typhoeus.stub(url).and_return(response)
56
+ block.call response, nil, nil
57
+ end
58
+ end
59
+ end
60
+ @count += 1
61
+ end
62
+ end
63
+
64
+ end
65
+ end
@@ -9,141 +9,230 @@ head
9
9
  body
10
10
 
11
11
  .container
12
- h1 Blinkr
13
- ol
14
- li
15
- a href='#links'
16
- p class="#{errors.links.empty? ? 'text-success' : 'text-danger'}"
17
- |Broken links
18
- - unless errors.javascript.nil?
19
- li
20
- a href='#javascript'
21
- p class="#{errors.javascript.empty? ? 'text-success' : 'text-danger'}"
22
- | Page load JavaScript errors
23
- - unless errors.resources.nil?
24
- li
25
- a href='#resources'
26
- p class="#{errors.resources.empty? ? 'text-success' : 'text-danger'}"
27
- | Resource loading errors
12
+ .row
13
+ h1 Blinkr
14
+ p.lead Blackbox testing for websites
28
15
 
29
- a id='links'
30
- h2
31
- |Broken links (
32
- code
33
- |a[href]
34
- |)
35
- - errors.links.each do |url, err|
36
- .panel.panel-danger
16
+ h2
17
+ | Pages
18
+ .label.label-primary.pull-right = blinkr.error_count
19
+ .panel.panel-default
37
20
  .panel-heading
38
- a data-toggle="collapse" href="#a-#{err.uid}" id="control-a-#{err.uid}"
39
- i.fa.fa-caret-square-o-right
40
- '
41
- a href="#{url}" target='_blank'
42
- | #{url}
43
- i.fa.fa-external-link
44
- .pull-right.badge
45
- - if err.code > 0 || !err.status_message.nil?
46
- span data-toggle="tooltip" title="Status message"
47
- ' #{err.code if err.code > 0} #{err.status_message}
48
- - elsif !err.return_message.nil? && err.return_message != 'No error'
49
- span data-toggle="tooltip" title="Return message"
50
- ' #{err.return_message}
51
- .panel-body.collapse id="a-#{err.uid}"
52
- - unless err.return_message.nil? || err.return_message == 'No error' || (err.code == 0 && err.status_message.nil?)
53
- |Message: #{err.return_message}
54
- ul.list-group
55
- - err.refs.each do |ref|
56
- li.list-group-item
57
- '#{ref.src}
58
- a href="#{ref.src}" target='_blank'
21
+ h4
22
+ a> data-toggle="collapse" href="#filters" id="control-filters"
23
+ i.fa.fa-caret-square-o-down
24
+ | Filters
25
+ .panel-collapse.collapse.in#filters
26
+ .panel-body
27
+ form.row
28
+ .col-md-4#severities
29
+ .panel.panel-default
30
+ .panel-heading
31
+ .checkbox
32
+ label
33
+ input.severities type="checkbox" value="severities" onchange="severities()" checked="true"
34
+ | Severity
35
+ .label.label-primary< = blinkr.error_count
36
+ .panel-body
37
+ - blinkr.severities.each do |severity, metadata|
38
+ .checkbox
39
+ label
40
+ input.severity type="checkbox" value="severity-#{metadata.id}" onchange="updateSeverities(); filter()" checked="true"
41
+ | #{severity.capitalize}
42
+ div< class="label label-#{severity}" = metadata.count
43
+ .col-md-8#types style="padding-left:20px; border-left: 1px solid #ccc;"
44
+ .row
45
+ - blinkr.categories.each do |category, metadata|
46
+ .col-md-6
47
+ .panel.panel-default
48
+ .panel-heading
49
+ .checkbox
50
+ label
51
+ input.category type="checkbox" value="category-#{metadata.id}" onchange="category()" checked="true"
52
+ | #{category}
53
+ - metadata.severities.each do |severity, count|
54
+ div< class="label label-#{severity}" = count
55
+ .panel-body
56
+ - metadata.types.each do |type, type_metadata|
57
+ .checkbox
58
+ label
59
+ input.type type="checkbox" value="type-#{metadata.id}-#{type_metadata.id}" onchange="updateCategory(); filter();" checked="true" data-category="category-#{metadata.id}"
60
+ | #{type}
61
+ - type_metadata.severities.each do |severity, count|
62
+ div< class="label label-#{severity}" = count
63
+ javascript:
64
+ $('#filters').on('show.bs.collapse', function () {
65
+ $('a#control-filters').html('<i class="fa fa-caret-square-o-down"></i>');
66
+ });
67
+ $('#filters').on('hide.bs.collapse', function () {
68
+ $('a#control-filters').html('<i class="fa fa-caret-square-o-right"></i>');
69
+ });
70
+ .spinner.center-block style="padding-top: 2em"
71
+ img src="http://static.jboss.org/www/images/_images_icons_ajax-loader-8.gif"
72
+ javascript:
73
+ $( '.spinner' ).hide();
74
+ #pages
75
+ - blinkr.pages.each_with_index do |(url, page), index|
76
+
77
+ div class="page panel panel-#{page.max_severity}"
78
+ .panel-heading
79
+ a> data-toggle="collapse" href="#p-#{index}" id="control-p-#{index}"
80
+ i.fa.fa-caret-square-o-right>
81
+ ' #{url}
82
+ a> href="#{url}" target='_blank'
59
83
  i.fa.fa-external-link
60
- '
61
- a href="view-source:#{ref.src}" target='_blank'
84
+ a href="view-source:#{url}" target='_blank'
62
85
  i.fa.fa-file-code-o
63
- .pull-right
64
- |(#{ref.src_location})
65
- pre #{ref.snippet}
66
- javascript:
67
- $('#a-#{err.uid}').on('show.bs.collapse', function () {
68
- $('a#control-a-#{err.uid}').html('<i class="fa fa-caret-square-o-down"></i>');
69
- });
70
- $('#a-#{err.uid}').on('hide.bs.collapse', function () {
71
- $('a#control-a-#{err.uid}').html('<i class="fa fa-caret-square-o-right"></i>');
72
- });
73
- - if errors.links.empty?
74
- .panel.panel-success
75
- .panel-heading
76
- a id='resources'
77
- | No broken links detected
78
- - unless errors.javascript.nil?
79
- a id='javascript'
80
- h2 Page load JavaScript errors
81
- - errors.javascript.each do |url, err|
82
- .panel.panel-danger
83
- .panel-heading
84
- a data-toggle="collapse" href="#j-#{err.uid}" id="control-j-#{err.uid}"
85
- i.fa.fa-caret-square-o-right
86
- '
87
- a href="#{url}" target='_blank'
88
- | #{url}
89
- i.fa.fa-external-link
90
- .panel-body.collapse id="j-#{err.uid}"
91
- ul.list-group
92
- - err.messages.each do |message|
93
- li.list-group-item
94
- pre= message.msg
95
- pre= message.trace
96
- javascript:
97
- $('#j-#{err.uid}').on('show.bs.collapse', function () {
98
- $('a#control-j-#{err.uid}').html('<i class="fa fa-caret-square-o-down"></i>');
99
- });
100
- $('#j-#{err.uid}').on('hide.bs.collapse', function () {
101
- $('a#control-j-#{err.uid}').html('<i class="fa fa-caret-square-o-right"></i>');
102
- });
103
- - if errors.javascript.empty?
104
- .panel.panel-success
105
- .panel-heading
106
- a id='javascript'
107
- | No JavaScript errors detected
108
-
109
- - unless errors.resources.nil?
110
- a id='resources'
111
- h2 Resource loading errors
112
- - errors.resources.each do |url, err|
113
- .panel.panel-danger
114
- .panel-heading
115
- a data-toggle="collapse" href="#r-#{err.uid}" id="control-r-#{err.uid}"
116
- i.fa.fa-caret-square-o-right
117
- '
118
- a href="#{url}" target='_blank'
119
- | #{url}
120
- i.fa.fa-external-link
121
- .panel-body.collapse id="r-#{err.uid}"
122
- ul.list-group
123
- - err.messages.each do |message|
124
- li.list-group-item
125
- '#{message.url}
126
- a href="#{message.url}" target='_blank'
127
- i.fa.fa-external-link
128
- .pull-right.badge.alert-danger
129
- - if message.errorCode > 0
130
- ' #{message.errorCode}
131
- - unless message.errorString.nil?
132
- | #{message.errorString}
86
+ .panel-body.collapse id="p-#{index}"
87
+ ul.list-group
88
+ - page.errors.each do |error|
89
+ == engine.transform page, error do
90
+ li.list-group-item.error data-ids="type-#{blinkr.categories[error.category].id}-#{blinkr.categories[error.category].types[error.type].id} severity-#{error.severity} category-#{blinkr.categories[error.category].id}"
91
+ - unless error.icon.nil?
92
+ i class="fa fa-2x #{error.icon} pull-left"
93
+ - unless error.code.nil? && error.message.nil?
94
+ div class="pull-right label label-#{error.severity}"
95
+ - unless error.code.nil?
96
+ ' #{error.code}
97
+ - unless error.message.nil?
98
+ | #{error.message}
99
+ div
100
+ ' #{error.title}
101
+ - if error.url
102
+ a href="#{error.url}" target='_blank'
103
+ i.fa.fa-external-link
104
+ - if error.detail
105
+ p= error.detail
106
+ - if error.snippet
107
+ pre #{error.snippet}
108
+ javascript:
109
+ $('#p-#{index}').on('show.bs.collapse', function () {
110
+ $('a#control-p-#{index}').html('<i class="fa fa-caret-square-o-down"></i>');
111
+ });
112
+ $('#p-#{index}').on('hide.bs.collapse', function () {
113
+ $('a#control-p-#{index}').html('<i class="fa fa-caret-square-o-right"></i>');
114
+ });
133
115
 
134
- javascript:
135
- $('#r-#{err.uid}').on('show.bs.collapse', function () {
136
- $('a#control-r-#{err.uid}').html('<i class="fa fa-caret-square-o-down"></i>');
137
- });
138
- $('#r-#{err.uid}').on('hide.bs.collapse', function () {
139
- $('a#control-r-#{err.uid}').html('<i class="fa fa-caret-square-o-right"></i>');
140
- });
141
- - if errors.resources.empty?
116
+ - if blinkr.pages.length == 0
142
117
  .panel.panel-success
143
118
  .panel-heading
144
119
  a id='resources'
145
- | No resource loading errors detected
146
-
120
+ | No errors detected
121
+
122
+ - engine.append(blinkr).each do |result|
123
+ == result
124
+
147
125
  javascript:
148
126
  $('[data-toggle="tooltip"]').tooltip({'placement': 'top'});
149
127
 
128
+ function category() {
129
+ $( "#filters #types input.category[type=checkbox]" ).each( function() {
130
+ var category = $( this ).val();
131
+ var checked = $( this ).prop("checked");
132
+ $( "#filters #types input.type[type=checkbox][data-category=" + category + "]" ).each( function() {
133
+ $( this ).prop( "checked", checked );
134
+ });
135
+ });
136
+ filter();
137
+ }
138
+
139
+ function severities() {
140
+ $( "#filters #severities input.severities[type=checkbox]" ).each( function() {
141
+ var checked = $( this ).prop("checked");
142
+ $( "#filters #severities input.severity[type=checkbox]" ).each( function() {
143
+ $( this ).prop( "checked", checked );
144
+ });
145
+ });
146
+ filter();
147
+ }
148
+
149
+ function updateCategory() {
150
+ $( "#filters #types input.category[type=checkbox]" ).each( function() {
151
+ var category = $( this ).val();
152
+ var checked = 0;
153
+ var unchecked = 0;
154
+ $( "#filters #types input.type[type=checkbox][data-category=" + category + "]" ).each( function() {
155
+ if ( $( this ).prop( "checked" ) ) {
156
+ checked++;
157
+ } else {
158
+ unchecked++;
159
+ }
160
+ });
161
+ if (unchecked == 0) {
162
+ $( this ).prop( "checked", true );
163
+ } else {
164
+ $( this ).prop( "checked", false );
165
+ }
166
+ });
167
+ }
168
+
169
+ function updateSeverities() {
170
+ $( "#filters #severities input.severities[type=checkbox]" ).each( function() {
171
+ var checked = 0;
172
+ var unchecked = 0;
173
+ $( "#filters #severities input.severity[type=checkbox]" ).each( function() {
174
+ if ( $( this ).prop( "checked" ) ) {
175
+ checked++;
176
+ } else {
177
+ unchecked++;
178
+ }
179
+ });
180
+ if (unchecked == 0) {
181
+ $( this ).prop( "checked", true );
182
+ } else {
183
+ $( this ).prop( "checked", false );
184
+ }
185
+ });
186
+ }
187
+
188
+ function filter() {
189
+ $( ".pages" ).hide();
190
+ $( ".spinner" ).show();
191
+ var selectedTypes = [];
192
+ var selectedSeverities = [];
193
+ $( "#filters #severities input[type=checkbox]" ).each( function() {
194
+ if ( $( this ).prop( "checked" ) ) {
195
+ selectedSeverities.push( $( this ).val() );
196
+ }
197
+ });
198
+ $( "#filters #types input.type[type=checkbox]" ).each( function() {
199
+ if ( $( this ).prop( "checked" ) ) {
200
+ selectedTypes.push( $( this ).val() );
201
+ }
202
+ });
203
+ $( "#pages .page" ).each( function() {
204
+ var count = 0;
205
+ $( "li.error[data-ids]", this ).each( function() {
206
+ var showType = false;
207
+ var showSeverity = false;
208
+ var ids = $( this ).attr("data-ids");
209
+ for (i = 0; i < selectedTypes.length; i++) {
210
+ if (ids.indexOf(selectedTypes[i]) >= 0) {
211
+ showType = true;
212
+ continue;
213
+ }
214
+ }
215
+ for (i = 0; i < selectedSeverities.length; i++) {
216
+ if (ids.indexOf(selectedSeverities[i]) >= 0) {
217
+ showSeverity = true;
218
+ continue;
219
+ }
220
+ }
221
+ if (showSeverity && showType) {
222
+ $( this ).show();
223
+ count++;
224
+ } else {
225
+ $( this ).hide();
226
+ }
227
+ });
228
+ if (count == 0) {
229
+ $( this ).hide();
230
+ } else {
231
+ $( this ).show();
232
+ }
233
+ });
234
+ $( ".pages" ).show();
235
+ $( ".spinner" ).hide();
236
+ }
237
+
238
+