tizen beta release
[profile/ivi/webkit-efl.git] / debian / libwebkit-engine / usr / share / ewebkit-0 / webinspector / AuditRules.js
1 /*
2  * Copyright (C) 2010 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 WebInspector.AuditRules.IPAddressRegexp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
32
33 WebInspector.AuditRules.CacheableResponseCodes =
34 {
35     200: true,
36     203: true,
37     206: true,
38     300: true,
39     301: true,
40     410: true,
41
42     304: true // Underlying resource is cacheable
43 }
44
45 WebInspector.AuditRules.getDomainToResourcesMap = function(resources, types, needFullResources)
46 {
47     var domainToResourcesMap = {};
48     for (var i = 0, size = resources.length; i < size; ++i) {
49         var resource = resources[i];
50         if (types && types.indexOf(resource.type) === -1)
51             continue;
52         var parsedURL = resource.url.asParsedURL();
53         if (!parsedURL)
54             continue;
55         var domain = parsedURL.host;
56         var domainResources = domainToResourcesMap[domain];
57         if (domainResources === undefined) {
58           domainResources = [];
59           domainToResourcesMap[domain] = domainResources;
60         }
61         domainResources.push(needFullResources ? resource : resource.url);
62     }
63     return domainToResourcesMap;
64 }
65
66 /**
67  * @constructor
68  * @extends {WebInspector.AuditRule}
69  */
70 WebInspector.AuditRules.GzipRule = function()
71 {
72     WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
73 }
74
75 WebInspector.AuditRules.GzipRule.prototype = {
76     doRun: function(resources, result, callback)
77     {
78         var totalSavings = 0;
79         var compressedSize = 0;
80         var candidateSize = 0;
81         var summary = result.addChild("", true);
82         for (var i = 0, length = resources.length; i < length; ++i) {
83             var resource = resources[i];
84             if (resource.statusCode === 304)
85                 continue; // Do not test 304 Not Modified resources as their contents are always empty.
86             if (this._shouldCompress(resource)) {
87                 var size = resource.resourceSize;
88                 candidateSize += size;
89                 if (this._isCompressed(resource)) {
90                     compressedSize += size;
91                     continue;
92                 }
93                 var savings = 2 * size / 3;
94                 totalSavings += savings;
95                 summary.addFormatted("%r could save ~%s", resource.url, Number.bytesToString(savings));
96                 result.violationCount++;
97             }
98         }
99         if (!totalSavings)
100             return callback(null);
101         summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
102         callback(result);
103     },
104
105     _isCompressed: function(resource)
106     {
107         var encodingHeader = resource.responseHeaders["Content-Encoding"];
108         if (!encodingHeader)
109             return false;
110
111         return /\b(?:gzip|deflate)\b/.test(encodingHeader);
112     },
113
114     _shouldCompress: function(resource)
115     {
116         return WebInspector.Resource.Type.isTextType(resource.type) && resource.domain && resource.resourceSize !== undefined && resource.resourceSize > 150;
117     }
118 }
119
120 WebInspector.AuditRules.GzipRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
121
122 /**
123  * @constructor
124  * @extends {WebInspector.AuditRule}
125  */
126 WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
127 {
128     WebInspector.AuditRule.call(this, id, name);
129     this._type = type;
130     this._resourceTypeName = resourceTypeName;
131     this._allowedPerDomain = allowedPerDomain;
132 }
133
134 WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
135     doRun: function(resources, result, callback)
136     {
137         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, [this._type], false);
138         var penalizedResourceCount = 0;
139         // TODO: refactor according to the chosen i18n approach
140         var summary = result.addChild("", true);
141         for (var domain in domainToResourcesMap) {
142             var domainResources = domainToResourcesMap[domain];
143             var extraResourceCount = domainResources.length - this._allowedPerDomain;
144             if (extraResourceCount <= 0)
145                 continue;
146             penalizedResourceCount += extraResourceCount - 1;
147             summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
148             result.violationCount += domainResources.length;
149         }
150         if (!penalizedResourceCount)
151             return callback(null);
152
153         summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible.";
154         callback(result);
155     }
156 }
157
158 WebInspector.AuditRules.CombineExternalResourcesRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
159
160 /**
161  * @constructor
162  * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
163  */
164 WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
165     WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.Resource.Type.Script, "JavaScript", allowedPerDomain);
166 }
167
168 WebInspector.AuditRules.CombineJsResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
169
170 /**
171  * @constructor
172  * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
173  */
174 WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
175     WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.Resource.Type.Stylesheet, "CSS", allowedPerDomain);
176 }
177
178 WebInspector.AuditRules.CombineCssResourcesRule.prototype.__proto__ = WebInspector.AuditRules.CombineExternalResourcesRule.prototype;
179
180 /**
181  * @constructor
182  * @extends {WebInspector.AuditRule}
183  */
184 WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
185     WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups");
186     this._hostCountThreshold = hostCountThreshold;
187 }
188
189 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
190     doRun: function(resources, result, callback)
191     {
192         var summary = result.addChild("");
193         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources, undefined, false);
194         for (var domain in domainToResourcesMap) {
195             if (domainToResourcesMap[domain].length > 1)
196                 continue;
197             var parsedURL = domain.asParsedURL();
198             if (!parsedURL)
199                 continue;
200             if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
201                 continue; // an IP address
202             summary.addSnippet(domain);
203             result.violationCount++;
204         }
205         if (!summary.children || summary.children.length <= this._hostCountThreshold)
206             return callback(null);
207
208         summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.";
209         callback(result);
210     }
211 }
212
213 WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
214
215 /**
216  * @constructor
217  * @extends {WebInspector.AuditRule}
218  */
219 WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
220 {
221     WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames");
222     this._optimalHostnameCount = optimalHostnameCount;
223     this._minRequestThreshold = minRequestThreshold;
224     this._minBalanceThreshold = minBalanceThreshold;
225 }
226
227 WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
228     doRun: function(resources, result, callback)
229     {
230         function hostSorter(a, b)
231         {
232             var aCount = domainToResourcesMap[a].length;
233             var bCount = domainToResourcesMap[b].length;
234             return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
235         }
236
237         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
238             resources,
239             [WebInspector.Resource.Type.Stylesheet, WebInspector.Resource.Type.Image],
240             true);
241
242         var hosts = [];
243         for (var url in domainToResourcesMap)
244             hosts.push(url);
245
246         if (!hosts.length)
247             return callback(null); // no hosts (local file or something)
248
249         hosts.sort(hostSorter);
250
251         var optimalHostnameCount = this._optimalHostnameCount;
252         if (hosts.length > optimalHostnameCount)
253             hosts.splice(optimalHostnameCount);
254
255         var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
256         var resourceCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
257         if (resourceCountAboveThreshold <= 0)
258             return callback(null);
259
260         var avgResourcesPerHost = 0;
261         for (var i = 0, size = hosts.length; i < size; ++i)
262             avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
263
264         // Assume optimal parallelization.
265         avgResourcesPerHost /= optimalHostnameCount;
266         avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
267
268         var pctAboveAvg = (resourceCountAboveThreshold / avgResourcesPerHost) - 1.0;
269         var minBalanceThreshold = this._minBalanceThreshold;
270         if (pctAboveAvg < minBalanceThreshold)
271             return callback(null);
272
273         var resourcesOnBusiestHost = domainToResourcesMap[hosts[0]];
274         var entry = result.addChild(String.sprintf("This page makes %d parallelizable requests to %s. Increase download parallelization by distributing the following requests across multiple hostnames.", busiestHostResourceCount, hosts[0]), true);
275         for (var i = 0; i < resourcesOnBusiestHost.length; ++i)
276             entry.addURL(resourcesOnBusiestHost[i].url);
277
278         result.violationCount = resourcesOnBusiestHost.length;
279         callback(result);
280     }
281 }
282
283 WebInspector.AuditRules.ParallelizeDownloadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
284
285 /**
286  * The reported CSS rule size is incorrect (parsed != original in WebKit),
287  * so use percentages instead, which gives a better approximation.
288  * @constructor
289  * @extends {WebInspector.AuditRule}
290  */
291 WebInspector.AuditRules.UnusedCssRule = function()
292 {
293     WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules");
294 }
295
296 WebInspector.AuditRules.UnusedCssRule.prototype = {
297     doRun: function(resources, result, callback)
298     {
299         var self = this;
300
301         function evalCallback(styleSheets) {
302             if (!styleSheets.length)
303                 return callback(null);
304
305             var pseudoSelectorRegexp = /:hover|:link|:active|:visited|:focus|:before|:after/;
306             var selectors = [];
307             var testedSelectors = {};
308             for (var i = 0; i < styleSheets.length; ++i) {
309                 var styleSheet = styleSheets[i];
310                 for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
311                     var selectorText = styleSheet.rules[curRule].selectorText;
312                     if (selectorText.match(pseudoSelectorRegexp) || testedSelectors[selectorText])
313                         continue;
314                     selectors.push(selectorText);
315                     testedSelectors[selectorText] = 1;
316                 }
317             }
318
319             function selectorsCallback(callback, styleSheets, testedSelectors, foundSelectors)
320             {
321                 var inlineBlockOrdinal = 0;
322                 var totalStylesheetSize = 0;
323                 var totalUnusedStylesheetSize = 0;
324                 var summary;
325
326                 for (var i = 0; i < styleSheets.length; ++i) {
327                     var styleSheet = styleSheets[i];
328                     var stylesheetSize = 0;
329                     var unusedStylesheetSize = 0;
330                     var unusedRules = [];
331                     for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
332                         var rule = styleSheet.rules[curRule];
333                         // Exact computation whenever source ranges are available.
334                         var textLength = (rule.selectorRange && rule.style.range && rule.style.range.end) ? rule.style.range.end - rule.selectorRange.start + 1 : 0;
335                         if (!textLength && rule.style.cssText)
336                             textLength = rule.style.cssText.length + rule.selectorText.length;
337                         stylesheetSize += textLength;
338                         if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
339                             continue;
340                         unusedStylesheetSize += textLength;
341                         unusedRules.push(rule.selectorText);
342                     }
343                     totalStylesheetSize += stylesheetSize;
344                     totalUnusedStylesheetSize += unusedStylesheetSize;
345
346                     if (!unusedRules.length)
347                         continue;
348
349                     var resource = WebInspector.resourceForURL(styleSheet.sourceURL);
350                     var isInlineBlock = resource && resource.type == WebInspector.Resource.Type.Document;
351                     var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal);
352                     var pctUnused = Math.round(100 * unusedStylesheetSize / stylesheetSize);
353                     if (!summary)
354                         summary = result.addChild("", true);
355                     var entry = summary.addFormatted("%s: %s (%d%%) is not used by the current page.", url, Number.bytesToString(unusedStylesheetSize), pctUnused);
356
357                     for (var j = 0; j < unusedRules.length; ++j)
358                         entry.addSnippet(unusedRules[j]);
359
360                     result.violationCount += unusedRules.length;
361                 }
362
363                 if (!totalUnusedStylesheetSize)
364                     return callback(null);
365
366                 var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
367                 summary.value = String.sprintf("%s (%d%%) of CSS is not used by the current page.", Number.bytesToString(totalUnusedStylesheetSize), totalUnusedPercent);
368
369                 callback(result);
370             }
371
372             var foundSelectors = {};
373             function queryCallback(boundSelectorsCallback, selector, styleSheets, testedSelectors, nodeId)
374             {
375                 if (nodeId)
376                     foundSelectors[selector] = true;
377                 if (boundSelectorsCallback)
378                     boundSelectorsCallback(foundSelectors);
379             }
380
381             function documentLoaded(selectors, document) {
382                 for (var i = 0; i < selectors.length; ++i)
383                     WebInspector.domAgent.querySelector(document.id, selectors[i], queryCallback.bind(null, i === selectors.length - 1 ? selectorsCallback.bind(null, callback, styleSheets, testedSelectors) : null, selectors[i], styleSheets, testedSelectors));
384             }
385
386             WebInspector.domAgent.requestDocument(documentLoaded.bind(null, selectors));
387         }
388
389         function styleSheetCallback(styleSheets, sourceURL, continuation, styleSheet)
390         {
391             if (styleSheet) {
392                 styleSheet.sourceURL = sourceURL;
393                 styleSheets.push(styleSheet);
394             }
395             if (continuation)
396                 continuation(styleSheets);
397         }
398
399         function allStylesCallback(error, styleSheetInfos)
400         {
401             if (error || !styleSheetInfos || !styleSheetInfos.length)
402                 return evalCallback([]);
403             var styleSheets = [];
404             for (var i = 0; i < styleSheetInfos.length; ++i) {
405                 var info = styleSheetInfos[i];
406                 WebInspector.CSSStyleSheet.createForId(info.styleSheetId, styleSheetCallback.bind(null, styleSheets, info.sourceURL, i == styleSheetInfos.length - 1 ? evalCallback : null));
407             }
408         }
409
410         CSSAgent.getAllStyleSheets(allStylesCallback);
411     }
412 }
413
414 WebInspector.AuditRules.UnusedCssRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
415
416 /**
417  * @constructor
418  * @extends {WebInspector.AuditRule}
419  */
420 WebInspector.AuditRules.CacheControlRule = function(id, name)
421 {
422     WebInspector.AuditRule.call(this, id, name);
423 }
424
425 WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
426
427 WebInspector.AuditRules.CacheControlRule.prototype = {
428
429     doRun: function(resources, result, callback)
430     {
431         var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(resources);
432         if (cacheableAndNonCacheableResources[0].length)
433             this.runChecks(cacheableAndNonCacheableResources[0], result);
434         this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
435
436         callback(result);
437     },
438
439     handleNonCacheableResources: function(resources, result)
440     {
441     },
442
443     _cacheableAndNonCacheableResources: function(resources)
444     {
445         var processedResources = [[], []];
446         for (var i = 0; i < resources.length; ++i) {
447             var resource = resources[i];
448             if (!this.isCacheableResource(resource))
449                 continue;
450             if (this._isExplicitlyNonCacheable(resource))
451                 processedResources[1].push(resource);
452             else
453                 processedResources[0].push(resource);
454         }
455         return processedResources;
456     },
457
458     execCheck: function(messageText, resourceCheckFunction, resources, result)
459     {
460         var resourceCount = resources.length;
461         var urls = [];
462         for (var i = 0; i < resourceCount; ++i) {
463             if (resourceCheckFunction.call(this, resources[i]))
464                 urls.push(resources[i].url);
465         }
466         if (urls.length) {
467             var entry = result.addChild(messageText, true);
468             entry.addURLs(urls);
469             result.violationCount += urls.length;
470         }
471     },
472
473     freshnessLifetimeGreaterThan: function(resource, timeMs)
474     {
475         var dateHeader = this.responseHeader(resource, "Date");
476         if (!dateHeader)
477             return false;
478
479         var dateHeaderMs = Date.parse(dateHeader);
480         if (isNaN(dateHeaderMs))
481             return false;
482
483         var freshnessLifetimeMs;
484         var maxAgeMatch = this.responseHeaderMatch(resource, "Cache-Control", "max-age=(\\d+)");
485
486         if (maxAgeMatch)
487             freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
488         else {
489             var expiresHeader = this.responseHeader(resource, "Expires");
490             if (expiresHeader) {
491                 var expDate = Date.parse(expiresHeader);
492                 if (!isNaN(expDate))
493                     freshnessLifetimeMs = expDate - dateHeaderMs;
494             }
495         }
496
497         return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
498     },
499
500     responseHeader: function(resource, header)
501     {
502         return resource.responseHeaders[header];
503     },
504
505     hasResponseHeader: function(resource, header)
506     {
507         return resource.responseHeaders[header] !== undefined;
508     },
509
510     isCompressible: function(resource)
511     {
512         return WebInspector.Resource.Type.isTextType(resource.type);
513     },
514
515     isPubliclyCacheable: function(resource)
516     {
517         if (this._isExplicitlyNonCacheable(resource))
518             return false;
519
520         if (this.responseHeaderMatch(resource, "Cache-Control", "public"))
521             return true;
522
523         return resource.url.indexOf("?") == -1 && !this.responseHeaderMatch(resource, "Cache-Control", "private");
524     },
525
526     responseHeaderMatch: function(resource, header, regexp)
527     {
528         return resource.responseHeaders[header]
529             ? resource.responseHeaders[header].match(new RegExp(regexp, "im"))
530             : undefined;
531     },
532
533     hasExplicitExpiration: function(resource)
534     {
535         return this.hasResponseHeader(resource, "Date") &&
536             (this.hasResponseHeader(resource, "Expires") || this.responseHeaderMatch(resource, "Cache-Control", "max-age"));
537     },
538
539     _isExplicitlyNonCacheable: function(resource)
540     {
541         var hasExplicitExp = this.hasExplicitExpiration(resource);
542         return this.responseHeaderMatch(resource, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
543             this.responseHeaderMatch(resource, "Pragma", "no-cache") ||
544             (hasExplicitExp && !this.freshnessLifetimeGreaterThan(resource, 0)) ||
545             (!hasExplicitExp && resource.url && resource.url.indexOf("?") >= 0) ||
546             (!hasExplicitExp && !this.isCacheableResource(resource));
547     },
548
549     isCacheableResource: function(resource)
550     {
551         return resource.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[resource.statusCode];
552     }
553 }
554
555 WebInspector.AuditRules.CacheControlRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
556
557 /**
558  * @constructor
559  * @extends {WebInspector.AuditRules.CacheControlRule}
560  */
561 WebInspector.AuditRules.BrowserCacheControlRule = function()
562 {
563     WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching");
564 }
565
566 WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
567     handleNonCacheableResources: function(resources, result)
568     {
569         if (resources.length) {
570             var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true);
571             result.violationCount += resources.length;
572             for (var i = 0; i < resources.length; ++i)
573                 entry.addURL(resources[i].url);
574         }
575     },
576
577     runChecks: function(resources, result, callback)
578     {
579         this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:",
580             this._missingExpirationCheck, resources, result);
581         this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:",
582             this._varyCheck, resources, result);
583         this.execCheck("The following cacheable resources have a short freshness lifetime:",
584             this._oneMonthExpirationCheck, resources, result);
585
586         // Unable to implement the favicon check due to the WebKit limitations.
587         this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:",
588             this._oneYearExpirationCheck, resources, result);
589     },
590
591     _missingExpirationCheck: function(resource)
592     {
593         return this.isCacheableResource(resource) && !this.hasResponseHeader(resource, "Set-Cookie") && !this.hasExplicitExpiration(resource);
594     },
595
596     _varyCheck: function(resource)
597     {
598         var varyHeader = this.responseHeader(resource, "Vary");
599         if (varyHeader) {
600             varyHeader = varyHeader.replace(/User-Agent/gi, "");
601             varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
602             varyHeader = varyHeader.replace(/[, ]*/g, "");
603         }
604         return varyHeader && varyHeader.length && this.isCacheableResource(resource) && this.freshnessLifetimeGreaterThan(resource, 0);
605     },
606
607     _oneMonthExpirationCheck: function(resource)
608     {
609         return this.isCacheableResource(resource) &&
610             !this.hasResponseHeader(resource, "Set-Cookie") &&
611             !this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
612             this.freshnessLifetimeGreaterThan(resource, 0);
613     },
614
615     _oneYearExpirationCheck: function(resource)
616     {
617         return this.isCacheableResource(resource) &&
618             !this.hasResponseHeader(resource, "Set-Cookie") &&
619             !this.freshnessLifetimeGreaterThan(resource, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
620             this.freshnessLifetimeGreaterThan(resource, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
621     }
622 }
623
624 WebInspector.AuditRules.BrowserCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
625
626 /**
627  * @constructor
628  * @extends {WebInspector.AuditRules.CacheControlRule}
629  */
630 WebInspector.AuditRules.ProxyCacheControlRule = function() {
631     WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching");
632 }
633
634 WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
635     runChecks: function(resources, result, callback)
636     {
637         this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:",
638             this._questionMarkCheck, resources, result);
639         this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:",
640             this._publicCachingCheck, resources, result);
641         this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.",
642             this._setCookieCacheableCheck, resources, result);
643     },
644
645     _questionMarkCheck: function(resource)
646     {
647         return resource.url.indexOf("?") >= 0 && !this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
648     },
649
650     _publicCachingCheck: function(resource)
651     {
652         return this.isCacheableResource(resource) &&
653             !this.isCompressible(resource) &&
654             !this.responseHeaderMatch(resource, "Cache-Control", "public") &&
655             !this.hasResponseHeader(resource, "Set-Cookie");
656     },
657
658     _setCookieCacheableCheck: function(resource)
659     {
660         return this.hasResponseHeader(resource, "Set-Cookie") && this.isPubliclyCacheable(resource);
661     }
662 }
663
664 WebInspector.AuditRules.ProxyCacheControlRule.prototype.__proto__ = WebInspector.AuditRules.CacheControlRule.prototype;
665
666 /**
667  * @constructor
668  * @extends {WebInspector.AuditRule}
669  */
670 WebInspector.AuditRules.ImageDimensionsRule = function()
671 {
672     WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions");
673 }
674
675 WebInspector.AuditRules.ImageDimensionsRule.prototype = {
676     doRun: function(resources, result, callback)
677     {
678         var urlToNoDimensionCount = {};
679
680         function doneCallback()
681         {
682             for (var url in urlToNoDimensionCount) {
683                 var entry = entry || result.addChild("A width and height should be specified for all images in order to speed up page display. The following image(s) are missing a width and/or height:", true);
684                 var format = "%r";
685                 if (urlToNoDimensionCount[url] > 1)
686                     format += " (%d uses)";
687                 entry.addFormatted(format, url, urlToNoDimensionCount[url]);
688                 result.violationCount++;
689             }
690             callback(entry ? result : null);
691         }
692
693         function imageStylesReady(imageId, styles, isLastStyle, computedStyle)
694         {
695             const node = WebInspector.domAgent.nodeForId(imageId);
696             var src = node.getAttribute("src");
697             if (!src.asParsedURL()) {
698                 for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
699                     if (frameOwnerCandidate.documentURL) {
700                         var completeSrc = WebInspector.completeURL(frameOwnerCandidate.documentURL, src);
701                         break;
702                     }
703                 }
704             }
705             if (completeSrc)
706                 src = completeSrc;
707
708             if (computedStyle.getPropertyValue("position") === "absolute") {
709                 if (isLastStyle)
710                     doneCallback();
711                 return;
712             }
713
714             var widthFound = "width" in styles.styleAttributes;
715             var heightFound = "height" in styles.styleAttributes;
716
717             var inlineStyle = styles.inlineStyle;
718             if (inlineStyle) {
719                 if (inlineStyle.getPropertyValue("width") !== "")
720                     widthFound = true;
721                 if (inlineStyle.getPropertyValue("height") !== "")
722                     heightFound = true;
723             }
724
725             for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
726                 var style = styles.matchedCSSRules[i].style;
727                 if (style.getPropertyValue("width") !== "")
728                     widthFound = true;
729                 if (style.getPropertyValue("height") !== "")
730                     heightFound = true;
731             }
732
733             if (!widthFound || !heightFound) {
734                 if (src in urlToNoDimensionCount)
735                     ++urlToNoDimensionCount[src];
736                 else
737                     urlToNoDimensionCount[src] = 1;
738             }
739
740             if (isLastStyle)
741                 doneCallback();
742         }
743
744         function getStyles(nodeIds)
745         {
746             var targetResult = {};
747
748             function inlineCallback(inlineStyle, styleAttributes)
749             {
750                 targetResult.inlineStyle = inlineStyle;
751                 targetResult.styleAttributes = styleAttributes;
752             }
753
754             function matchedCallback(result)
755             {
756                 if (result)
757                     targetResult.matchedCSSRules = result.matchedCSSRules;
758             }
759
760             if (!nodeIds || !nodeIds.length)
761                 doneCallback();
762
763             for (var i = 0; nodeIds && i < nodeIds.length; ++i) {
764                 WebInspector.cssModel.getMatchedStylesAsync(nodeIds[i], undefined, false, false, matchedCallback);
765                 WebInspector.cssModel.getInlineStylesAsync(nodeIds[i], inlineCallback);
766                 WebInspector.cssModel.getComputedStyleAsync(nodeIds[i], undefined, imageStylesReady.bind(null, nodeIds[i], targetResult, i === nodeIds.length - 1));
767             }
768         }
769
770         function onDocumentAvailable(root)
771         {
772             WebInspector.domAgent.querySelectorAll(root.id, "img[src]", getStyles);
773         }
774
775         WebInspector.domAgent.requestDocument(onDocumentAvailable);
776     }
777 }
778
779 WebInspector.AuditRules.ImageDimensionsRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
780
781 /**
782  * @constructor
783  * @extends {WebInspector.AuditRule}
784  */
785 WebInspector.AuditRules.CssInHeadRule = function()
786 {
787     WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head");
788 }
789
790 WebInspector.AuditRules.CssInHeadRule.prototype = {
791     doRun: function(resources, result, callback)
792     {
793         function evalCallback(evalResult)
794         {
795             if (!evalResult)
796                 return callback(null);
797
798             var summary = result.addChild("");
799
800             var outputMessages = [];
801             for (var url in evalResult) {
802                 var urlViolations = evalResult[url];
803                 if (urlViolations[0]) {
804                     result.addFormatted("%s style block(s) in the %r body should be moved to the document head.", urlViolations[0], url);
805                     result.violationCount += urlViolations[0];
806                 }
807                 for (var i = 0; i < urlViolations[1].length; ++i)
808                     result.addFormatted("Link node %r should be moved to the document head in %r", urlViolations[1][i], url);
809                 result.violationCount += urlViolations[1].length;
810             }
811             summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance.");
812             callback(result);
813         }
814
815         function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds)
816         {
817             if (!nodeIds)
818                 return;
819             var externalStylesheetNodeIds = nodeIds;
820             var result = null;
821             if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) {
822                 var urlToViolationsArray = {};
823                 var externalStylesheetHrefs = [];
824                 for (var j = 0; j < externalStylesheetNodeIds.length; ++j) {
825                     var linkNode = WebInspector.domAgent.nodeForId(externalStylesheetNodeIds[j]);
826                     var completeHref = WebInspector.completeURL(linkNode.ownerDocument.documentURL, linkNode.getAttribute("href"));
827                     externalStylesheetHrefs.push(completeHref || "<empty>");
828                 }
829                 urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs];
830                 result = urlToViolationsArray;
831             }
832             evalCallback(result);
833         }
834
835         function inlineStylesReceived(root, nodeIds)
836         {
837             if (!nodeIds)
838                 return;
839             WebInspector.domAgent.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds));
840         }
841
842         function onDocumentAvailable(root)
843         {
844             WebInspector.domAgent.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root));
845         }
846
847         WebInspector.domAgent.requestDocument(onDocumentAvailable);
848     }
849 }
850
851 WebInspector.AuditRules.CssInHeadRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
852
853 /**
854  * @constructor
855  * @extends {WebInspector.AuditRule}
856  */
857 WebInspector.AuditRules.StylesScriptsOrderRule = function()
858 {
859     WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts");
860 }
861
862 WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
863     doRun: function(resources, result, callback)
864     {
865         function evalCallback(resultValue)
866         {
867             if (!resultValue)
868                 return callback(null);
869
870             var lateCssUrls = resultValue[0];
871             var cssBeforeInlineCount = resultValue[1];
872
873             var entry = result.addChild("The following external CSS files were included after an external JavaScript file in the document head. To ensure CSS files are downloaded in parallel, always include external CSS before external JavaScript.", true);
874             entry.addURLs(lateCssUrls);
875             result.violationCount += lateCssUrls.length;
876
877             if (cssBeforeInlineCount) {
878                 result.addChild(String.sprintf(" %d inline script block%s found in the head between an external CSS file and another resource. To allow parallel downloading, move the inline script before the external CSS file, or after the next resource.", cssBeforeInlineCount, cssBeforeInlineCount > 1 ? "s were" : " was"));
879                 result.violationCount += cssBeforeInlineCount;
880             }
881             callback(result);
882         }
883
884         function cssBeforeInlineReceived(lateStyleIds, nodeIds)
885         {
886             if (!nodeIds)
887                 return;
888
889             var cssBeforeInlineCount = nodeIds.length;
890             var result = null;
891             if (lateStyleIds.length || cssBeforeInlineCount) {
892                 var lateStyleUrls = [];
893                 for (var i = 0; i < lateStyleIds.length; ++i) {
894                     var lateStyleNode = WebInspector.domAgent.nodeForId(lateStyleIds[i]);
895                     var completeHref = WebInspector.completeURL(lateStyleNode.ownerDocument.documentURL, lateStyleNode.getAttribute("href"));
896                     lateStyleUrls.push(completeHref || "<empty>");
897                 }
898                 result = [ lateStyleUrls, cssBeforeInlineCount ];
899             }
900
901             evalCallback(result);
902         }
903
904         function lateStylesReceived(root, nodeIds)
905         {
906             if (!nodeIds)
907                 return;
908
909             WebInspector.domAgent.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds));
910         }
911
912         function onDocumentAvailable(root)
913         {
914             WebInspector.domAgent.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root));
915         }
916
917         WebInspector.domAgent.requestDocument(onDocumentAvailable);
918     }
919 }
920
921 WebInspector.AuditRules.StylesScriptsOrderRule.prototype.__proto__ = WebInspector.AuditRule.prototype;
922
923 /**
924  * @constructor
925  * @extends {WebInspector.AuditRule}
926  */
927 WebInspector.AuditRules.CookieRuleBase = function(id, name)
928 {
929     WebInspector.AuditRule.call(this, id, name);
930 }
931
932 WebInspector.AuditRules.CookieRuleBase.prototype = {
933     doRun: function(resources, result, callback)
934     {
935         var self = this;
936         function resultCallback(receivedCookies, isAdvanced) {
937             self.processCookies(isAdvanced ? receivedCookies : [], resources, result);
938             callback(result);
939         }
940         WebInspector.Cookies.getCookiesAsync(resultCallback);
941     },
942
943     mapResourceCookies: function(resourcesByDomain, allCookies, callback)
944     {
945         for (var i = 0; i < allCookies.length; ++i) {
946             for (var resourceDomain in resourcesByDomain) {
947                 if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain, resourceDomain))
948                     this._callbackForResourceCookiePairs(resourcesByDomain[resourceDomain], allCookies[i], callback);
949             }
950         }
951     },
952
953     _callbackForResourceCookiePairs: function(resources, cookie, callback)
954     {
955         if (!resources)
956             return;
957         for (var i = 0; i < resources.length; ++i) {
958             if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, resources[i].url))
959                 callback(resources[i], cookie);
960         }
961     }
962 }
963
964 WebInspector.AuditRules.CookieRuleBase.prototype.__proto__ = WebInspector.AuditRule.prototype;
965
966 /**
967  * @constructor
968  * @extends {WebInspector.AuditRules.CookieRuleBase}
969  */
970 WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
971 {
972     WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size");
973     this._avgBytesThreshold = avgBytesThreshold;
974     this._maxBytesThreshold = 1000;
975 }
976
977 WebInspector.AuditRules.CookieSizeRule.prototype = {
978     _average: function(cookieArray)
979     {
980         var total = 0;
981         for (var i = 0; i < cookieArray.length; ++i)
982             total += cookieArray[i].size;
983         return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
984     },
985
986     _max: function(cookieArray)
987     {
988         var result = 0;
989         for (var i = 0; i < cookieArray.length; ++i)
990             result = Math.max(cookieArray[i].size, result);
991         return result;
992     },
993
994     processCookies: function(allCookies, resources, result)
995     {
996         function maxSizeSorter(a, b)
997         {
998             return b.maxCookieSize - a.maxCookieSize;
999         }
1000
1001         function avgSizeSorter(a, b)
1002         {
1003             return b.avgCookieSize - a.avgCookieSize;
1004         }
1005
1006         var cookiesPerResourceDomain = {};
1007
1008         function collectorCallback(resource, cookie)
1009         {
1010             var cookies = cookiesPerResourceDomain[resource.domain];
1011             if (!cookies) {
1012                 cookies = [];
1013                 cookiesPerResourceDomain[resource.domain] = cookies;
1014             }
1015             cookies.push(cookie);
1016         }
1017
1018         if (!allCookies.length)
1019             return;
1020
1021         var sortedCookieSizes = [];
1022
1023         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
1024                 null,
1025                 true);
1026         var matchingResourceData = {};
1027         this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));
1028
1029         for (var resourceDomain in cookiesPerResourceDomain) {
1030             var cookies = cookiesPerResourceDomain[resourceDomain];
1031             sortedCookieSizes.push({
1032                 domain: resourceDomain,
1033                 avgCookieSize: this._average(cookies),
1034                 maxCookieSize: this._max(cookies)
1035             });
1036         }
1037         var avgAllCookiesSize = this._average(allCookies);
1038
1039         var hugeCookieDomains = [];
1040         sortedCookieSizes.sort(maxSizeSorter);
1041
1042         for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1043             var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
1044             if (maxCookieSize > this._maxBytesThreshold)
1045                 hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
1046         }
1047
1048         var bigAvgCookieDomains = [];
1049         sortedCookieSizes.sort(avgSizeSorter);
1050         for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1051             var domain = sortedCookieSizes[i].domain;
1052             var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
1053             if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
1054                 bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
1055         }
1056         result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
1057
1058         var message;
1059         if (hugeCookieDomains.length) {
1060             var entry = result.addChild("The following domains have a cookie size in excess of 1KB. This is harmful because requests with cookies larger than 1KB typically cannot fit into a single network packet.", true);
1061             entry.addURLs(hugeCookieDomains);
1062             result.violationCount += hugeCookieDomains.length;
1063         }
1064
1065         if (bigAvgCookieDomains.length) {
1066             var entry = result.addChild(String.sprintf("The following domains have an average cookie size in excess of %d bytes. Reducing the size of cookies for these domains can reduce the time it takes to send requests.", this._avgBytesThreshold), true);
1067             entry.addURLs(bigAvgCookieDomains);
1068             result.violationCount += bigAvgCookieDomains.length;
1069         }
1070     }
1071 }
1072
1073 WebInspector.AuditRules.CookieSizeRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;
1074
1075 /**
1076  * @constructor
1077  * @extends {WebInspector.AuditRules.CookieRuleBase}
1078  */
1079 WebInspector.AuditRules.StaticCookielessRule = function(minResources)
1080 {
1081     WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain");
1082     this._minResources = minResources;
1083 }
1084
1085 WebInspector.AuditRules.StaticCookielessRule.prototype = {
1086     processCookies: function(allCookies, resources, result)
1087     {
1088         var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(resources,
1089                 [WebInspector.Resource.Type.Stylesheet,
1090                  WebInspector.Resource.Type.Image],
1091                 true);
1092         var totalStaticResources = 0;
1093         for (var domain in domainToResourcesMap)
1094             totalStaticResources += domainToResourcesMap[domain].length;
1095         if (totalStaticResources < this._minResources)
1096             return;
1097         var matchingResourceData = {};
1098         this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
1099
1100         var badUrls = [];
1101         var cookieBytes = 0;
1102         for (var url in matchingResourceData) {
1103             badUrls.push(url);
1104             cookieBytes += matchingResourceData[url]
1105         }
1106         if (badUrls.length < this._minResources)
1107             return;
1108
1109         var entry = result.addChild(String.sprintf("%s of cookies were sent with the following static resources. Serve these static resources from a domain that does not set cookies:", Number.bytesToString(cookieBytes)), true);
1110         entry.addURLs(badUrls);
1111         result.violationCount = badUrls.length;
1112     },
1113
1114     _collectorCallback: function(matchingResourceData, resource, cookie)
1115     {
1116         matchingResourceData[resource.url] = (matchingResourceData[resource.url] || 0) + cookie.size;
1117     }
1118 }
1119
1120 WebInspector.AuditRules.StaticCookielessRule.prototype.__proto__ = WebInspector.AuditRules.CookieRuleBase.prototype;