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