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 request is cacheable
43}
44
45/**
46 * @param {!Array.<!WebInspector.NetworkRequest>} requests
47 * @param {?Array.<!WebInspector.resourceTypes>} types
48 * @param {boolean} needFullResources
49 * @return {!Object.<string, !Array.<!WebInspector.NetworkRequest|string>>}
50 */
51WebInspector.AuditRules.getDomainToResourcesMap = function(requests, types, needFullResources)
52{
53    var domainToResourcesMap = {};
54    for (var i = 0, size = requests.length; i < size; ++i) {
55        var request = requests[i];
56        if (types && types.indexOf(request.type) === -1)
57            continue;
58        var parsedURL = request.url.asParsedURL();
59        if (!parsedURL)
60            continue;
61        var domain = parsedURL.host;
62        var domainResources = domainToResourcesMap[domain];
63        if (domainResources === undefined) {
64          domainResources = [];
65          domainToResourcesMap[domain] = domainResources;
66        }
67        domainResources.push(needFullResources ? request : request.url);
68    }
69    return domainToResourcesMap;
70}
71
72/**
73 * @constructor
74 * @extends {WebInspector.AuditRule}
75 */
76WebInspector.AuditRules.GzipRule = function()
77{
78    WebInspector.AuditRule.call(this, "network-gzip", "Enable gzip compression");
79}
80
81WebInspector.AuditRules.GzipRule.prototype = {
82    /**
83     * @param {!Array.<!WebInspector.NetworkRequest>} requests
84     * @param {!WebInspector.AuditRuleResult} result
85     * @param {function(?WebInspector.AuditRuleResult)} callback
86     * @param {!WebInspector.Progress} progress
87     */
88    doRun: function(requests, result, callback, progress)
89    {
90        var totalSavings = 0;
91        var compressedSize = 0;
92        var candidateSize = 0;
93        var summary = result.addChild("", true);
94        for (var i = 0, length = requests.length; i < length; ++i) {
95            var request = requests[i];
96            if (request.cached || request.statusCode === 304)
97                continue; // Do not test cached resources.
98            if (this._shouldCompress(request)) {
99                var size = request.resourceSize;
100                candidateSize += size;
101                if (this._isCompressed(request)) {
102                    compressedSize += size;
103                    continue;
104                }
105                var savings = 2 * size / 3;
106                totalSavings += savings;
107                summary.addFormatted("%r could save ~%s", request.url, Number.bytesToString(savings));
108                result.violationCount++;
109            }
110        }
111        if (!totalSavings)
112            return callback(null);
113        summary.value = String.sprintf("Compressing the following resources with gzip could reduce their transfer size by about two thirds (~%s):", Number.bytesToString(totalSavings));
114        callback(result);
115    },
116
117    _isCompressed: function(request)
118    {
119        var encodingHeader = request.responseHeaderValue("Content-Encoding");
120        if (!encodingHeader)
121            return false;
122
123        return /\b(?:gzip|deflate)\b/.test(encodingHeader);
124    },
125
126    _shouldCompress: function(request)
127    {
128        return request.type.isTextType() && request.parsedURL.host && request.resourceSize !== undefined && request.resourceSize > 150;
129    },
130
131    __proto__: WebInspector.AuditRule.prototype
132}
133
134/**
135 * @constructor
136 * @extends {WebInspector.AuditRule}
137 */
138WebInspector.AuditRules.CombineExternalResourcesRule = function(id, name, type, resourceTypeName, allowedPerDomain)
139{
140    WebInspector.AuditRule.call(this, id, name);
141    this._type = type;
142    this._resourceTypeName = resourceTypeName;
143    this._allowedPerDomain = allowedPerDomain;
144}
145
146WebInspector.AuditRules.CombineExternalResourcesRule.prototype = {
147    /**
148     * @param {!Array.<!WebInspector.NetworkRequest>} requests
149     * @param {!WebInspector.AuditRuleResult} result
150     * @param {function(?WebInspector.AuditRuleResult)} callback
151     * @param {!WebInspector.Progress} progress
152     */
153    doRun: function(requests, result, callback, progress)
154    {
155        var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, [this._type], false);
156        var penalizedResourceCount = 0;
157        // TODO: refactor according to the chosen i18n approach
158        var summary = result.addChild("", true);
159        for (var domain in domainToResourcesMap) {
160            var domainResources = domainToResourcesMap[domain];
161            var extraResourceCount = domainResources.length - this._allowedPerDomain;
162            if (extraResourceCount <= 0)
163                continue;
164            penalizedResourceCount += extraResourceCount - 1;
165            summary.addChild(String.sprintf("%d %s resources served from %s.", domainResources.length, this._resourceTypeName, WebInspector.AuditRuleResult.resourceDomain(domain)));
166            result.violationCount += domainResources.length;
167        }
168        if (!penalizedResourceCount)
169            return callback(null);
170
171        summary.value = "There are multiple resources served from same domain. Consider combining them into as few files as possible.";
172        callback(result);
173    },
174
175    __proto__: WebInspector.AuditRule.prototype
176}
177
178/**
179 * @constructor
180 * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
181 */
182WebInspector.AuditRules.CombineJsResourcesRule = function(allowedPerDomain) {
183    WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externaljs", "Combine external JavaScript", WebInspector.resourceTypes.Script, "JavaScript", allowedPerDomain);
184}
185
186WebInspector.AuditRules.CombineJsResourcesRule.prototype = {
187    __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
188}
189
190/**
191 * @constructor
192 * @extends {WebInspector.AuditRules.CombineExternalResourcesRule}
193 */
194WebInspector.AuditRules.CombineCssResourcesRule = function(allowedPerDomain) {
195    WebInspector.AuditRules.CombineExternalResourcesRule.call(this, "page-externalcss", "Combine external CSS", WebInspector.resourceTypes.Stylesheet, "CSS", allowedPerDomain);
196}
197
198WebInspector.AuditRules.CombineCssResourcesRule.prototype = {
199    __proto__: WebInspector.AuditRules.CombineExternalResourcesRule.prototype
200}
201
202/**
203 * @constructor
204 * @extends {WebInspector.AuditRule}
205 */
206WebInspector.AuditRules.MinimizeDnsLookupsRule = function(hostCountThreshold) {
207    WebInspector.AuditRule.call(this, "network-minimizelookups", "Minimize DNS lookups");
208    this._hostCountThreshold = hostCountThreshold;
209}
210
211WebInspector.AuditRules.MinimizeDnsLookupsRule.prototype = {
212    /**
213     * @param {!Array.<!WebInspector.NetworkRequest>} requests
214     * @param {!WebInspector.AuditRuleResult} result
215     * @param {function(?WebInspector.AuditRuleResult)} callback
216     * @param {!WebInspector.Progress} progress
217     */
218    doRun: function(requests, result, callback, progress)
219    {
220        var summary = result.addChild("");
221        var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests, null, false);
222        for (var domain in domainToResourcesMap) {
223            if (domainToResourcesMap[domain].length > 1)
224                continue;
225            var parsedURL = domain.asParsedURL();
226            if (!parsedURL)
227                continue;
228            if (!parsedURL.host.search(WebInspector.AuditRules.IPAddressRegexp))
229                continue; // an IP address
230            summary.addSnippet(domain);
231            result.violationCount++;
232        }
233        if (!summary.children || summary.children.length <= this._hostCountThreshold)
234            return callback(null);
235
236        summary.value = "The following domains only serve one resource each. If possible, avoid the extra DNS lookups by serving these resources from existing domains.";
237        callback(result);
238    },
239
240    __proto__: WebInspector.AuditRule.prototype
241}
242
243/**
244 * @constructor
245 * @extends {WebInspector.AuditRule}
246 */
247WebInspector.AuditRules.ParallelizeDownloadRule = function(optimalHostnameCount, minRequestThreshold, minBalanceThreshold)
248{
249    WebInspector.AuditRule.call(this, "network-parallelizehosts", "Parallelize downloads across hostnames");
250    this._optimalHostnameCount = optimalHostnameCount;
251    this._minRequestThreshold = minRequestThreshold;
252    this._minBalanceThreshold = minBalanceThreshold;
253}
254
255WebInspector.AuditRules.ParallelizeDownloadRule.prototype = {
256    /**
257     * @param {!Array.<!WebInspector.NetworkRequest>} requests
258     * @param {!WebInspector.AuditRuleResult} result
259     * @param {function(?WebInspector.AuditRuleResult)} callback
260     * @param {!WebInspector.Progress} progress
261     */
262    doRun: function(requests, result, callback, progress)
263    {
264        function hostSorter(a, b)
265        {
266            var aCount = domainToResourcesMap[a].length;
267            var bCount = domainToResourcesMap[b].length;
268            return (aCount < bCount) ? 1 : (aCount == bCount) ? 0 : -1;
269        }
270
271        var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(
272            requests,
273            [WebInspector.resourceTypes.Stylesheet, WebInspector.resourceTypes.Image],
274            true);
275
276        var hosts = [];
277        for (var url in domainToResourcesMap)
278            hosts.push(url);
279
280        if (!hosts.length)
281            return callback(null); // no hosts (local file or something)
282
283        hosts.sort(hostSorter);
284
285        var optimalHostnameCount = this._optimalHostnameCount;
286        if (hosts.length > optimalHostnameCount)
287            hosts.splice(optimalHostnameCount);
288
289        var busiestHostResourceCount = domainToResourcesMap[hosts[0]].length;
290        var requestCountAboveThreshold = busiestHostResourceCount - this._minRequestThreshold;
291        if (requestCountAboveThreshold <= 0)
292            return callback(null);
293
294        var avgResourcesPerHost = 0;
295        for (var i = 0, size = hosts.length; i < size; ++i)
296            avgResourcesPerHost += domainToResourcesMap[hosts[i]].length;
297
298        // Assume optimal parallelization.
299        avgResourcesPerHost /= optimalHostnameCount;
300        avgResourcesPerHost = Math.max(avgResourcesPerHost, 1);
301
302        var pctAboveAvg = (requestCountAboveThreshold / avgResourcesPerHost) - 1.0;
303        var minBalanceThreshold = this._minBalanceThreshold;
304        if (pctAboveAvg < minBalanceThreshold)
305            return callback(null);
306
307        var requestsOnBusiestHost = domainToResourcesMap[hosts[0]];
308        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);
309        for (var i = 0; i < requestsOnBusiestHost.length; ++i)
310            entry.addURL(requestsOnBusiestHost[i].url);
311
312        result.violationCount = requestsOnBusiestHost.length;
313        callback(result);
314    },
315
316    __proto__: WebInspector.AuditRule.prototype
317}
318
319/**
320 * The reported CSS rule size is incorrect (parsed != original in WebKit),
321 * so use percentages instead, which gives a better approximation.
322 * @constructor
323 * @extends {WebInspector.AuditRule}
324 */
325WebInspector.AuditRules.UnusedCssRule = function()
326{
327    WebInspector.AuditRule.call(this, "page-unusedcss", "Remove unused CSS rules");
328}
329
330WebInspector.AuditRules.UnusedCssRule.prototype = {
331    /**
332     * @param {!Array.<!WebInspector.NetworkRequest>} requests
333     * @param {!WebInspector.AuditRuleResult} result
334     * @param {function(?WebInspector.AuditRuleResult)} callback
335     * @param {!WebInspector.Progress} progress
336     */
337    doRun: function(requests, result, callback, progress)
338    {
339        var self = this;
340
341        /**
342         * @param {!Array.<!WebInspector.CSSStyleSheet>} styleSheets
343         */
344        function evalCallback(styleSheets) {
345            if (progress.isCanceled())
346                return;
347
348            if (!styleSheets.length)
349                return callback(null);
350
351            var selectors = [];
352            var testedSelectors = {};
353            for (var i = 0; i < styleSheets.length; ++i) {
354                var styleSheet = styleSheets[i];
355                for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
356                    var selectorText = styleSheet.rules[curRule].selectorText;
357                    if (testedSelectors[selectorText])
358                        continue;
359                    selectors.push(selectorText);
360                    testedSelectors[selectorText] = 1;
361                }
362            }
363
364            var foundSelectors = {};
365
366            /**
367             * @param {!Array.<!WebInspector.CSSStyleSheet>} styleSheets
368             */
369            function selectorsCallback(styleSheets)
370            {
371                if (progress.isCanceled())
372                    return;
373
374                var inlineBlockOrdinal = 0;
375                var totalStylesheetSize = 0;
376                var totalUnusedStylesheetSize = 0;
377                var summary;
378
379                for (var i = 0; i < styleSheets.length; ++i) {
380                    var styleSheet = styleSheets[i];
381                    var unusedRules = [];
382                    for (var curRule = 0; curRule < styleSheet.rules.length; ++curRule) {
383                        var rule = styleSheet.rules[curRule];
384                        if (!testedSelectors[rule.selectorText] || foundSelectors[rule.selectorText])
385                            continue;
386                        unusedRules.push(rule.selectorText);
387                    }
388                    totalStylesheetSize += styleSheet.rules.length;
389                    totalUnusedStylesheetSize += unusedRules.length;
390
391                    if (!unusedRules.length)
392                        continue;
393
394                    var resource = WebInspector.resourceForURL(styleSheet.sourceURL);
395                    var isInlineBlock = resource && resource.request && resource.request.type == WebInspector.resourceTypes.Document;
396                    var url = !isInlineBlock ? WebInspector.AuditRuleResult.linkifyDisplayName(styleSheet.sourceURL) : String.sprintf("Inline block #%d", ++inlineBlockOrdinal);
397                    var pctUnused = Math.round(100 * unusedRules.length / styleSheet.rules.length);
398                    if (!summary)
399                        summary = result.addChild("", true);
400                    var entry = summary.addFormatted("%s: %d% is not used by the current page.", url, pctUnused);
401
402                    for (var j = 0; j < unusedRules.length; ++j)
403                        entry.addSnippet(unusedRules[j]);
404
405                    result.violationCount += unusedRules.length;
406                }
407
408                if (!totalUnusedStylesheetSize)
409                    return callback(null);
410
411                var totalUnusedPercent = Math.round(100 * totalUnusedStylesheetSize / totalStylesheetSize);
412                summary.value = String.sprintf("%s rules (%d%) of CSS not used by the current page.", totalUnusedStylesheetSize, totalUnusedPercent);
413
414                callback(result);
415            }
416
417            /**
418             * @param {?function()} boundSelectorsCallback
419             * @param {string} selector
420             * @param {?DOMAgent.NodeId} nodeId
421             */
422            function queryCallback(boundSelectorsCallback, selector, nodeId)
423            {
424                if (nodeId)
425                    foundSelectors[selector] = true;
426                if (boundSelectorsCallback)
427                    boundSelectorsCallback();
428            }
429
430            /**
431             * @param {!Array.<string>} selectors
432             * @param {!WebInspector.DOMDocument} document
433             */
434            function documentLoaded(selectors, document) {
435                var pseudoSelectorRegexp = /::?(?:[\w-]+)(?:\(.*?\))?/g;
436                if (!selectors.length) {
437                    selectorsCallback([]);
438                    return;
439                }
440                for (var i = 0; i < selectors.length; ++i) {
441                    if (progress.isCanceled())
442                        return;
443                    var effectiveSelector = selectors[i].replace(pseudoSelectorRegexp, "");
444                    WebInspector.domAgent.querySelector(document.id, effectiveSelector, queryCallback.bind(null, i === selectors.length - 1 ? selectorsCallback.bind(null, styleSheets) : null, selectors[i]));
445                }
446            }
447
448            WebInspector.domAgent.requestDocument(documentLoaded.bind(null, selectors));
449        }
450
451        /**
452         * @param {!Array.<!WebInspector.CSSStyleSheet>} styleSheets
453         * @param {string} sourceURL
454         * @param {?function(!Array.<!WebInspector.CSSStyleSheet>)} continuation
455         * @param {?WebInspector.CSSStyleSheet} styleSheet
456         */
457        function styleSheetCallback(styleSheets, sourceURL, continuation, styleSheet)
458        {
459            if (progress.isCanceled())
460                return;
461
462            if (styleSheet) {
463                styleSheet.sourceURL = sourceURL;
464                styleSheets.push(styleSheet);
465            }
466            if (continuation)
467                continuation(styleSheets);
468        }
469
470        /**
471         * @param {?Protocol.Error} error
472         * @param {!Array.<!CSSAgent.CSSStyleSheetHeader>} styleSheetInfos
473         */
474        function allStylesCallback(error, styleSheetInfos)
475        {
476            if (progress.isCanceled())
477                return;
478
479            if (error || !styleSheetInfos || !styleSheetInfos.length)
480                return evalCallback([]);
481            var styleSheets = [];
482            for (var i = 0; i < styleSheetInfos.length; ++i) {
483                var info = styleSheetInfos[i];
484                WebInspector.CSSStyleSheet.createForId(info.styleSheetId, styleSheetCallback.bind(null, styleSheets, info.sourceURL, i == styleSheetInfos.length - 1 ? evalCallback : null));
485            }
486        }
487
488        CSSAgent.getAllStyleSheets(allStylesCallback);
489    },
490
491    __proto__: WebInspector.AuditRule.prototype
492}
493
494/**
495 * @constructor
496 * @extends {WebInspector.AuditRule}
497 */
498WebInspector.AuditRules.CacheControlRule = function(id, name)
499{
500    WebInspector.AuditRule.call(this, id, name);
501}
502
503WebInspector.AuditRules.CacheControlRule.MillisPerMonth = 1000 * 60 * 60 * 24 * 30;
504
505WebInspector.AuditRules.CacheControlRule.prototype = {
506    /**
507     * @param {!Array.<!WebInspector.NetworkRequest>} requests
508     * @param {!WebInspector.AuditRuleResult} result
509     * @param {function(!WebInspector.AuditRuleResult)} callback
510     * @param {!WebInspector.Progress} progress
511     */
512    doRun: function(requests, result, callback, progress)
513    {
514        var cacheableAndNonCacheableResources = this._cacheableAndNonCacheableResources(requests);
515        if (cacheableAndNonCacheableResources[0].length)
516            this.runChecks(cacheableAndNonCacheableResources[0], result);
517        this.handleNonCacheableResources(cacheableAndNonCacheableResources[1], result);
518
519        callback(result);
520    },
521
522    handleNonCacheableResources: function(requests, result)
523    {
524    },
525
526    _cacheableAndNonCacheableResources: function(requests)
527    {
528        var processedResources = [[], []];
529        for (var i = 0; i < requests.length; ++i) {
530            var request = requests[i];
531            if (!this.isCacheableResource(request))
532                continue;
533            if (this._isExplicitlyNonCacheable(request))
534                processedResources[1].push(request);
535            else
536                processedResources[0].push(request);
537        }
538        return processedResources;
539    },
540
541    execCheck: function(messageText, requestCheckFunction, requests, result)
542    {
543        var requestCount = requests.length;
544        var urls = [];
545        for (var i = 0; i < requestCount; ++i) {
546            if (requestCheckFunction.call(this, requests[i]))
547                urls.push(requests[i].url);
548        }
549        if (urls.length) {
550            var entry = result.addChild(messageText, true);
551            entry.addURLs(urls);
552            result.violationCount += urls.length;
553        }
554    },
555
556    freshnessLifetimeGreaterThan: function(request, timeMs)
557    {
558        var dateHeader = this.responseHeader(request, "Date");
559        if (!dateHeader)
560            return false;
561
562        var dateHeaderMs = Date.parse(dateHeader);
563        if (isNaN(dateHeaderMs))
564            return false;
565
566        var freshnessLifetimeMs;
567        var maxAgeMatch = this.responseHeaderMatch(request, "Cache-Control", "max-age=(\\d+)");
568
569        if (maxAgeMatch)
570            freshnessLifetimeMs = (maxAgeMatch[1]) ? 1000 * maxAgeMatch[1] : 0;
571        else {
572            var expiresHeader = this.responseHeader(request, "Expires");
573            if (expiresHeader) {
574                var expDate = Date.parse(expiresHeader);
575                if (!isNaN(expDate))
576                    freshnessLifetimeMs = expDate - dateHeaderMs;
577            }
578        }
579
580        return (isNaN(freshnessLifetimeMs)) ? false : freshnessLifetimeMs > timeMs;
581    },
582
583    responseHeader: function(request, header)
584    {
585        return request.responseHeaderValue(header);
586    },
587
588    hasResponseHeader: function(request, header)
589    {
590        return request.responseHeaderValue(header) !== undefined;
591    },
592
593    isCompressible: function(request)
594    {
595        return request.type.isTextType();
596    },
597
598    isPubliclyCacheable: function(request)
599    {
600        if (this._isExplicitlyNonCacheable(request))
601            return false;
602
603        if (this.responseHeaderMatch(request, "Cache-Control", "public"))
604            return true;
605
606        return request.url.indexOf("?") == -1 && !this.responseHeaderMatch(request, "Cache-Control", "private");
607    },
608
609    responseHeaderMatch: function(request, header, regexp)
610    {
611        return request.responseHeaderValue(header)
612            ? request.responseHeaderValue(header).match(new RegExp(regexp, "im"))
613            : undefined;
614    },
615
616    hasExplicitExpiration: function(request)
617    {
618        return this.hasResponseHeader(request, "Date") &&
619            (this.hasResponseHeader(request, "Expires") || this.responseHeaderMatch(request, "Cache-Control", "max-age"));
620    },
621
622    _isExplicitlyNonCacheable: function(request)
623    {
624        var hasExplicitExp = this.hasExplicitExpiration(request);
625        return this.responseHeaderMatch(request, "Cache-Control", "(no-cache|no-store|must-revalidate)") ||
626            this.responseHeaderMatch(request, "Pragma", "no-cache") ||
627            (hasExplicitExp && !this.freshnessLifetimeGreaterThan(request, 0)) ||
628            (!hasExplicitExp && request.url && request.url.indexOf("?") >= 0) ||
629            (!hasExplicitExp && !this.isCacheableResource(request));
630    },
631
632    isCacheableResource: function(request)
633    {
634        return request.statusCode !== undefined && WebInspector.AuditRules.CacheableResponseCodes[request.statusCode];
635    },
636
637    __proto__: WebInspector.AuditRule.prototype
638}
639
640/**
641 * @constructor
642 * @extends {WebInspector.AuditRules.CacheControlRule}
643 */
644WebInspector.AuditRules.BrowserCacheControlRule = function()
645{
646    WebInspector.AuditRules.CacheControlRule.call(this, "http-browsercache", "Leverage browser caching");
647}
648
649WebInspector.AuditRules.BrowserCacheControlRule.prototype = {
650    handleNonCacheableResources: function(requests, result)
651    {
652        if (requests.length) {
653            var entry = result.addChild("The following resources are explicitly non-cacheable. Consider making them cacheable if possible:", true);
654            result.violationCount += requests.length;
655            for (var i = 0; i < requests.length; ++i)
656                entry.addURL(requests[i].url);
657        }
658    },
659
660    runChecks: function(requests, result, callback)
661    {
662        this.execCheck("The following resources are missing a cache expiration. Resources that do not specify an expiration may not be cached by browsers:",
663            this._missingExpirationCheck, requests, result);
664        this.execCheck("The following resources specify a \"Vary\" header that disables caching in most versions of Internet Explorer:",
665            this._varyCheck, requests, result);
666        this.execCheck("The following cacheable resources have a short freshness lifetime:",
667            this._oneMonthExpirationCheck, requests, result);
668
669        // Unable to implement the favicon check due to the WebKit limitations.
670        this.execCheck("To further improve cache hit rate, specify an expiration one year in the future for the following cacheable resources:",
671            this._oneYearExpirationCheck, requests, result);
672    },
673
674    _missingExpirationCheck: function(request)
675    {
676        return this.isCacheableResource(request) && !this.hasResponseHeader(request, "Set-Cookie") && !this.hasExplicitExpiration(request);
677    },
678
679    _varyCheck: function(request)
680    {
681        var varyHeader = this.responseHeader(request, "Vary");
682        if (varyHeader) {
683            varyHeader = varyHeader.replace(/User-Agent/gi, "");
684            varyHeader = varyHeader.replace(/Accept-Encoding/gi, "");
685            varyHeader = varyHeader.replace(/[, ]*/g, "");
686        }
687        return varyHeader && varyHeader.length && this.isCacheableResource(request) && this.freshnessLifetimeGreaterThan(request, 0);
688    },
689
690    _oneMonthExpirationCheck: function(request)
691    {
692        return this.isCacheableResource(request) &&
693            !this.hasResponseHeader(request, "Set-Cookie") &&
694            !this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
695            this.freshnessLifetimeGreaterThan(request, 0);
696    },
697
698    _oneYearExpirationCheck: function(request)
699    {
700        return this.isCacheableResource(request) &&
701            !this.hasResponseHeader(request, "Set-Cookie") &&
702            !this.freshnessLifetimeGreaterThan(request, 11 * WebInspector.AuditRules.CacheControlRule.MillisPerMonth) &&
703            this.freshnessLifetimeGreaterThan(request, WebInspector.AuditRules.CacheControlRule.MillisPerMonth);
704    },
705
706    __proto__: WebInspector.AuditRules.CacheControlRule.prototype
707}
708
709/**
710 * @constructor
711 * @extends {WebInspector.AuditRules.CacheControlRule}
712 */
713WebInspector.AuditRules.ProxyCacheControlRule = function() {
714    WebInspector.AuditRules.CacheControlRule.call(this, "http-proxycache", "Leverage proxy caching");
715}
716
717WebInspector.AuditRules.ProxyCacheControlRule.prototype = {
718    runChecks: function(requests, result, callback)
719    {
720        this.execCheck("Resources with a \"?\" in the URL are not cached by most proxy caching servers:",
721            this._questionMarkCheck, requests, result);
722        this.execCheck("Consider adding a \"Cache-Control: public\" header to the following resources:",
723            this._publicCachingCheck, requests, result);
724        this.execCheck("The following publicly cacheable resources contain a Set-Cookie header. This security vulnerability can cause cookies to be shared by multiple users.",
725            this._setCookieCacheableCheck, requests, result);
726    },
727
728    _questionMarkCheck: function(request)
729    {
730        return request.url.indexOf("?") >= 0 && !this.hasResponseHeader(request, "Set-Cookie") && this.isPubliclyCacheable(request);
731    },
732
733    _publicCachingCheck: function(request)
734    {
735        return this.isCacheableResource(request) &&
736            !this.isCompressible(request) &&
737            !this.responseHeaderMatch(request, "Cache-Control", "public") &&
738            !this.hasResponseHeader(request, "Set-Cookie");
739    },
740
741    _setCookieCacheableCheck: function(request)
742    {
743        return this.hasResponseHeader(request, "Set-Cookie") && this.isPubliclyCacheable(request);
744    },
745
746    __proto__: WebInspector.AuditRules.CacheControlRule.prototype
747}
748
749/**
750 * @constructor
751 * @extends {WebInspector.AuditRule}
752 */
753WebInspector.AuditRules.ImageDimensionsRule = function()
754{
755    WebInspector.AuditRule.call(this, "page-imagedims", "Specify image dimensions");
756}
757
758WebInspector.AuditRules.ImageDimensionsRule.prototype = {
759    /**
760     * @param {!Array.<!WebInspector.NetworkRequest>} requests
761     * @param {!WebInspector.AuditRuleResult} result
762     * @param {function(?WebInspector.AuditRuleResult)} callback
763     * @param {!WebInspector.Progress} progress
764     */
765    doRun: function(requests, result, callback, progress)
766    {
767        var urlToNoDimensionCount = {};
768
769        function doneCallback()
770        {
771            for (var url in urlToNoDimensionCount) {
772                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);
773                var format = "%r";
774                if (urlToNoDimensionCount[url] > 1)
775                    format += " (%d uses)";
776                entry.addFormatted(format, url, urlToNoDimensionCount[url]);
777                result.violationCount++;
778            }
779            callback(entry ? result : null);
780        }
781
782        function imageStylesReady(imageId, styles, isLastStyle, computedStyle)
783        {
784            if (progress.isCanceled())
785                return;
786
787            const node = WebInspector.domAgent.nodeForId(imageId);
788            var src = node.getAttribute("src");
789            if (!src.asParsedURL()) {
790                for (var frameOwnerCandidate = node; frameOwnerCandidate; frameOwnerCandidate = frameOwnerCandidate.parentNode) {
791                    if (frameOwnerCandidate.baseURL) {
792                        var completeSrc = WebInspector.ParsedURL.completeURL(frameOwnerCandidate.baseURL, src);
793                        break;
794                    }
795                }
796            }
797            if (completeSrc)
798                src = completeSrc;
799
800            if (computedStyle.getPropertyValue("position") === "absolute") {
801                if (isLastStyle)
802                    doneCallback();
803                return;
804            }
805
806            if (styles.attributesStyle) {
807                var widthFound = !!styles.attributesStyle.getLiveProperty("width");
808                var heightFound = !!styles.attributesStyle.getLiveProperty("height");
809            }
810
811            var inlineStyle = styles.inlineStyle;
812            if (inlineStyle) {
813                if (inlineStyle.getPropertyValue("width") !== "")
814                    widthFound = true;
815                if (inlineStyle.getPropertyValue("height") !== "")
816                    heightFound = true;
817            }
818
819            for (var i = styles.matchedCSSRules.length - 1; i >= 0 && !(widthFound && heightFound); --i) {
820                var style = styles.matchedCSSRules[i].style;
821                if (style.getPropertyValue("width") !== "")
822                    widthFound = true;
823                if (style.getPropertyValue("height") !== "")
824                    heightFound = true;
825            }
826
827            if (!widthFound || !heightFound) {
828                if (src in urlToNoDimensionCount)
829                    ++urlToNoDimensionCount[src];
830                else
831                    urlToNoDimensionCount[src] = 1;
832            }
833
834            if (isLastStyle)
835                doneCallback();
836        }
837
838        /**
839         * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
840         */
841        function getStyles(nodeIds)
842        {
843            if (progress.isCanceled())
844                return;
845            var targetResult = {};
846
847            function inlineCallback(inlineStyle, attributesStyle)
848            {
849                targetResult.inlineStyle = inlineStyle;
850                targetResult.attributesStyle = attributesStyle;
851            }
852
853            function matchedCallback(result)
854            {
855                if (result)
856                    targetResult.matchedCSSRules = result.matchedCSSRules;
857            }
858
859            if (!nodeIds || !nodeIds.length)
860                doneCallback();
861
862            for (var i = 0; nodeIds && i < nodeIds.length; ++i) {
863                WebInspector.cssModel.getMatchedStylesAsync(nodeIds[i], false, false, matchedCallback);
864                WebInspector.cssModel.getInlineStylesAsync(nodeIds[i], inlineCallback);
865                WebInspector.cssModel.getComputedStyleAsync(nodeIds[i], imageStylesReady.bind(null, nodeIds[i], targetResult, i === nodeIds.length - 1));
866            }
867        }
868
869        function onDocumentAvailable(root)
870        {
871            if (progress.isCanceled())
872                return;
873            WebInspector.domAgent.querySelectorAll(root.id, "img[src]", getStyles);
874        }
875
876        if (progress.isCanceled())
877            return;
878        WebInspector.domAgent.requestDocument(onDocumentAvailable);
879    },
880
881    __proto__: WebInspector.AuditRule.prototype
882}
883
884/**
885 * @constructor
886 * @extends {WebInspector.AuditRule}
887 */
888WebInspector.AuditRules.CssInHeadRule = function()
889{
890    WebInspector.AuditRule.call(this, "page-cssinhead", "Put CSS in the document head");
891}
892
893WebInspector.AuditRules.CssInHeadRule.prototype = {
894    /**
895     * @param {!Array.<!WebInspector.NetworkRequest>} requests
896     * @param {!WebInspector.AuditRuleResult} result
897     * @param {function(?WebInspector.AuditRuleResult)} callback
898     * @param {!WebInspector.Progress} progress
899     */
900    doRun: function(requests, result, callback, progress)
901    {
902        function evalCallback(evalResult)
903        {
904            if (progress.isCanceled())
905                return;
906
907            if (!evalResult)
908                return callback(null);
909
910            var summary = result.addChild("");
911
912            var outputMessages = [];
913            for (var url in evalResult) {
914                var urlViolations = evalResult[url];
915                if (urlViolations[0]) {
916                    result.addFormatted("%s style block(s) in the %r body should be moved to the document head.", urlViolations[0], url);
917                    result.violationCount += urlViolations[0];
918                }
919                for (var i = 0; i < urlViolations[1].length; ++i)
920                    result.addFormatted("Link node %r should be moved to the document head in %r", urlViolations[1][i], url);
921                result.violationCount += urlViolations[1].length;
922            }
923            summary.value = String.sprintf("CSS in the document body adversely impacts rendering performance.");
924            callback(result);
925        }
926
927        /**
928         * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
929         */
930        function externalStylesheetsReceived(root, inlineStyleNodeIds, nodeIds)
931        {
932            if (progress.isCanceled())
933                return;
934
935            if (!nodeIds)
936                return;
937            var externalStylesheetNodeIds = nodeIds;
938            var result = null;
939            if (inlineStyleNodeIds.length || externalStylesheetNodeIds.length) {
940                var urlToViolationsArray = {};
941                var externalStylesheetHrefs = [];
942                for (var j = 0; j < externalStylesheetNodeIds.length; ++j) {
943                    var linkNode = WebInspector.domAgent.nodeForId(externalStylesheetNodeIds[j]);
944                    var completeHref = WebInspector.ParsedURL.completeURL(linkNode.ownerDocument.baseURL, linkNode.getAttribute("href"));
945                    externalStylesheetHrefs.push(completeHref || "<empty>");
946                }
947                urlToViolationsArray[root.documentURL] = [inlineStyleNodeIds.length, externalStylesheetHrefs];
948                result = urlToViolationsArray;
949            }
950            evalCallback(result);
951        }
952
953        /**
954         * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
955         */
956        function inlineStylesReceived(root, nodeIds)
957        {
958            if (progress.isCanceled())
959                return;
960
961            if (!nodeIds)
962                return;
963            WebInspector.domAgent.querySelectorAll(root.id, "body link[rel~='stylesheet'][href]", externalStylesheetsReceived.bind(null, root, nodeIds));
964        }
965
966        function onDocumentAvailable(root)
967        {
968            if (progress.isCanceled())
969                return;
970
971            WebInspector.domAgent.querySelectorAll(root.id, "body style", inlineStylesReceived.bind(null, root));
972        }
973
974        WebInspector.domAgent.requestDocument(onDocumentAvailable);
975    },
976
977    __proto__: WebInspector.AuditRule.prototype
978}
979
980/**
981 * @constructor
982 * @extends {WebInspector.AuditRule}
983 */
984WebInspector.AuditRules.StylesScriptsOrderRule = function()
985{
986    WebInspector.AuditRule.call(this, "page-stylescriptorder", "Optimize the order of styles and scripts");
987}
988
989WebInspector.AuditRules.StylesScriptsOrderRule.prototype = {
990    /**
991     * @param {!Array.<!WebInspector.NetworkRequest>} requests
992     * @param {!WebInspector.AuditRuleResult} result
993     * @param {function(?WebInspector.AuditRuleResult)} callback
994     * @param {!WebInspector.Progress} progress
995     */
996    doRun: function(requests, result, callback, progress)
997    {
998        function evalCallback(resultValue)
999        {
1000            if (progress.isCanceled())
1001                return;
1002
1003            if (!resultValue)
1004                return callback(null);
1005
1006            var lateCssUrls = resultValue[0];
1007            var cssBeforeInlineCount = resultValue[1];
1008
1009            if (lateCssUrls.length) {
1010                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);
1011                entry.addURLs(lateCssUrls);
1012                result.violationCount += lateCssUrls.length;
1013            }
1014
1015            if (cssBeforeInlineCount) {
1016                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"));
1017                result.violationCount += cssBeforeInlineCount;
1018            }
1019            callback(result);
1020        }
1021
1022        /**
1023         * @param {!Array.<!DOMAgent.NodeId>} lateStyleIds
1024         * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
1025         */
1026        function cssBeforeInlineReceived(lateStyleIds, nodeIds)
1027        {
1028            if (progress.isCanceled())
1029                return;
1030
1031            if (!nodeIds)
1032                return;
1033
1034            var cssBeforeInlineCount = nodeIds.length;
1035            var result = null;
1036            if (lateStyleIds.length || cssBeforeInlineCount) {
1037                var lateStyleUrls = [];
1038                for (var i = 0; i < lateStyleIds.length; ++i) {
1039                    var lateStyleNode = WebInspector.domAgent.nodeForId(lateStyleIds[i]);
1040                    var completeHref = WebInspector.ParsedURL.completeURL(lateStyleNode.ownerDocument.baseURL, lateStyleNode.getAttribute("href"));
1041                    lateStyleUrls.push(completeHref || "<empty>");
1042                }
1043                result = [ lateStyleUrls, cssBeforeInlineCount ];
1044            }
1045
1046            evalCallback(result);
1047        }
1048
1049        /**
1050         * @param {!WebInspector.DOMDocument} root
1051         * @param {!Array.<!DOMAgent.NodeId>=} nodeIds
1052         */
1053        function lateStylesReceived(root, nodeIds)
1054        {
1055            if (progress.isCanceled())
1056                return;
1057
1058            if (!nodeIds)
1059                return;
1060
1061            WebInspector.domAgent.querySelectorAll(root.id, "head link[rel~='stylesheet'][href] ~ script:not([src])", cssBeforeInlineReceived.bind(null, nodeIds));
1062        }
1063
1064        /**
1065         * @param {!WebInspector.DOMDocument} root
1066         */
1067        function onDocumentAvailable(root)
1068        {
1069            if (progress.isCanceled())
1070                return;
1071
1072            WebInspector.domAgent.querySelectorAll(root.id, "head script[src] ~ link[rel~='stylesheet'][href]", lateStylesReceived.bind(null, root));
1073        }
1074
1075        WebInspector.domAgent.requestDocument(onDocumentAvailable);
1076    },
1077
1078    __proto__: WebInspector.AuditRule.prototype
1079}
1080
1081/**
1082 * @constructor
1083 * @extends {WebInspector.AuditRule}
1084 */
1085WebInspector.AuditRules.CSSRuleBase = function(id, name)
1086{
1087    WebInspector.AuditRule.call(this, id, name);
1088}
1089
1090WebInspector.AuditRules.CSSRuleBase.prototype = {
1091    /**
1092     * @param {!Array.<!WebInspector.NetworkRequest>} requests
1093     * @param {!WebInspector.AuditRuleResult} result
1094     * @param {function(?WebInspector.AuditRuleResult)} callback
1095     * @param {!WebInspector.Progress} progress
1096     */
1097    doRun: function(requests, result, callback, progress)
1098    {
1099        CSSAgent.getAllStyleSheets(sheetsCallback.bind(this));
1100
1101        /**
1102         * @param {?Protocol.Error} error
1103         * @param {!Array.<!CSSAgent.CSSStyleSheetHeader>} headers
1104         * @this {WebInspector.AuditRules.CSSRuleBase}
1105         */
1106        function sheetsCallback(error, headers)
1107        {
1108            if (error)
1109                return callback(null);
1110
1111            if (!headers.length)
1112                return callback(null);
1113            for (var i = 0; i < headers.length; ++i) {
1114                var header = headers[i];
1115                if (header.disabled)
1116                    continue; // Do not check disabled stylesheets.
1117
1118                this._visitStyleSheet(header.styleSheetId, i === headers.length - 1 ? finishedCallback : null, result, progress);
1119            }
1120        }
1121
1122        function finishedCallback()
1123        {
1124            callback(result);
1125        }
1126    },
1127
1128    _visitStyleSheet: function(styleSheetId, callback, result, progress)
1129    {
1130        WebInspector.CSSStyleSheet.createForId(styleSheetId, sheetCallback.bind(this));
1131
1132        /**
1133         * @param {?WebInspector.CSSStyleSheet} styleSheet
1134         * @this {WebInspector.AuditRules.CSSRuleBase}
1135         */
1136        function sheetCallback(styleSheet)
1137        {
1138            if (progress.isCanceled())
1139                return;
1140
1141            if (!styleSheet) {
1142                if (callback)
1143                    callback();
1144                return;
1145            }
1146
1147            this.visitStyleSheet(styleSheet, result);
1148
1149            for (var i = 0; i < styleSheet.rules.length; ++i)
1150                this._visitRule(styleSheet, styleSheet.rules[i], result);
1151
1152            this.didVisitStyleSheet(styleSheet, result);
1153
1154            if (callback)
1155                callback();
1156        }
1157    },
1158
1159    _visitRule: function(styleSheet, rule, result)
1160    {
1161        this.visitRule(styleSheet, rule, result);
1162        var allProperties = rule.style.allProperties;
1163        for (var i = 0; i < allProperties.length; ++i)
1164            this.visitProperty(styleSheet, allProperties[i], result);
1165        this.didVisitRule(styleSheet, rule, result);
1166    },
1167
1168    visitStyleSheet: function(styleSheet, result)
1169    {
1170        // Subclasses can implement.
1171    },
1172
1173    didVisitStyleSheet: function(styleSheet, result)
1174    {
1175        // Subclasses can implement.
1176    },
1177
1178    visitRule: function(styleSheet, rule, result)
1179    {
1180        // Subclasses can implement.
1181    },
1182
1183    didVisitRule: function(styleSheet, rule, result)
1184    {
1185        // Subclasses can implement.
1186    },
1187
1188    visitProperty: function(styleSheet, property, result)
1189    {
1190        // Subclasses can implement.
1191    },
1192
1193    __proto__: WebInspector.AuditRule.prototype
1194}
1195
1196/**
1197 * @constructor
1198 * @extends {WebInspector.AuditRules.CSSRuleBase}
1199 */
1200WebInspector.AuditRules.VendorPrefixedCSSProperties = function()
1201{
1202    WebInspector.AuditRules.CSSRuleBase.call(this, "page-vendorprefixedcss", "Use normal CSS property names instead of vendor-prefixed ones");
1203    this._webkitPrefix = "-webkit-";
1204}
1205
1206WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties = [
1207    "background-clip", "background-origin", "background-size",
1208    "border-radius", "border-bottom-left-radius", "border-bottom-right-radius", "border-top-left-radius", "border-top-right-radius",
1209    "box-shadow", "box-sizing", "opacity", "text-shadow"
1210].keySet();
1211
1212WebInspector.AuditRules.VendorPrefixedCSSProperties.prototype = {
1213    didVisitStyleSheet: function(styleSheet)
1214    {
1215        delete this._styleSheetResult;
1216    },
1217
1218    visitRule: function(rule)
1219    {
1220        this._mentionedProperties = {};
1221    },
1222
1223    didVisitRule: function()
1224    {
1225        delete this._ruleResult;
1226        delete this._mentionedProperties;
1227    },
1228
1229    visitProperty: function(styleSheet, property, result)
1230    {
1231        if (!property.name.startsWith(this._webkitPrefix))
1232            return;
1233
1234        var normalPropertyName = property.name.substring(this._webkitPrefix.length).toLowerCase(); // Start just after the "-webkit-" prefix.
1235        if (WebInspector.AuditRules.VendorPrefixedCSSProperties.supportedProperties[normalPropertyName] && !this._mentionedProperties[normalPropertyName]) {
1236            var style = property.ownerStyle;
1237            var liveProperty = style.getLiveProperty(normalPropertyName);
1238            if (liveProperty && !liveProperty.styleBased)
1239                return; // WebCore can provide normal versions of prefixed properties automatically, so be careful to skip only normal source-based properties.
1240
1241            var rule = style.parentRule;
1242            this._mentionedProperties[normalPropertyName] = true;
1243            if (!this._styleSheetResult)
1244                this._styleSheetResult = result.addChild(rule.sourceURL ? WebInspector.linkifyResourceAsNode(rule.sourceURL) : "<unknown>");
1245            if (!this._ruleResult) {
1246                var anchor = WebInspector.linkifyURLAsNode(rule.sourceURL, rule.selectorText);
1247                anchor.preferredPanel = "resources";
1248                anchor.lineNumber = rule.lineNumberInSource();
1249                this._ruleResult = this._styleSheetResult.addChild(anchor);
1250            }
1251            ++result.violationCount;
1252            this._ruleResult.addSnippet(String.sprintf("\"" + this._webkitPrefix + "%s\" is used, but \"%s\" is supported.", normalPropertyName, normalPropertyName));
1253        }
1254    },
1255
1256    __proto__: WebInspector.AuditRules.CSSRuleBase.prototype
1257}
1258
1259/**
1260 * @constructor
1261 * @extends {WebInspector.AuditRule}
1262 */
1263WebInspector.AuditRules.CookieRuleBase = function(id, name)
1264{
1265    WebInspector.AuditRule.call(this, id, name);
1266}
1267
1268WebInspector.AuditRules.CookieRuleBase.prototype = {
1269    /**
1270     * @param {!Array.<!WebInspector.NetworkRequest>} requests
1271     * @param {!WebInspector.AuditRuleResult} result
1272     * @param {function(!WebInspector.AuditRuleResult)} callback
1273     * @param {!WebInspector.Progress} progress
1274     */
1275    doRun: function(requests, result, callback, progress)
1276    {
1277        var self = this;
1278        function resultCallback(receivedCookies) {
1279            if (progress.isCanceled())
1280                return;
1281
1282            self.processCookies(receivedCookies, requests, result);
1283            callback(result);
1284        }
1285
1286        WebInspector.Cookies.getCookiesAsync(resultCallback);
1287    },
1288
1289    mapResourceCookies: function(requestsByDomain, allCookies, callback)
1290    {
1291        for (var i = 0; i < allCookies.length; ++i) {
1292            for (var requestDomain in requestsByDomain) {
1293                if (WebInspector.Cookies.cookieDomainMatchesResourceDomain(allCookies[i].domain(), requestDomain))
1294                    this._callbackForResourceCookiePairs(requestsByDomain[requestDomain], allCookies[i], callback);
1295            }
1296        }
1297    },
1298
1299    _callbackForResourceCookiePairs: function(requests, cookie, callback)
1300    {
1301        if (!requests)
1302            return;
1303        for (var i = 0; i < requests.length; ++i) {
1304            if (WebInspector.Cookies.cookieMatchesResourceURL(cookie, requests[i].url))
1305                callback(requests[i], cookie);
1306        }
1307    },
1308
1309    __proto__: WebInspector.AuditRule.prototype
1310}
1311
1312/**
1313 * @constructor
1314 * @extends {WebInspector.AuditRules.CookieRuleBase}
1315 */
1316WebInspector.AuditRules.CookieSizeRule = function(avgBytesThreshold)
1317{
1318    WebInspector.AuditRules.CookieRuleBase.call(this, "http-cookiesize", "Minimize cookie size");
1319    this._avgBytesThreshold = avgBytesThreshold;
1320    this._maxBytesThreshold = 1000;
1321}
1322
1323WebInspector.AuditRules.CookieSizeRule.prototype = {
1324    _average: function(cookieArray)
1325    {
1326        var total = 0;
1327        for (var i = 0; i < cookieArray.length; ++i)
1328            total += cookieArray[i].size();
1329        return cookieArray.length ? Math.round(total / cookieArray.length) : 0;
1330    },
1331
1332    _max: function(cookieArray)
1333    {
1334        var result = 0;
1335        for (var i = 0; i < cookieArray.length; ++i)
1336            result = Math.max(cookieArray[i].size(), result);
1337        return result;
1338    },
1339
1340    processCookies: function(allCookies, requests, result)
1341    {
1342        function maxSizeSorter(a, b)
1343        {
1344            return b.maxCookieSize - a.maxCookieSize;
1345        }
1346
1347        function avgSizeSorter(a, b)
1348        {
1349            return b.avgCookieSize - a.avgCookieSize;
1350        }
1351
1352        var cookiesPerResourceDomain = {};
1353
1354        function collectorCallback(request, cookie)
1355        {
1356            var cookies = cookiesPerResourceDomain[request.parsedURL.host];
1357            if (!cookies) {
1358                cookies = [];
1359                cookiesPerResourceDomain[request.parsedURL.host] = cookies;
1360            }
1361            cookies.push(cookie);
1362        }
1363
1364        if (!allCookies.length)
1365            return;
1366
1367        var sortedCookieSizes = [];
1368
1369        var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests,
1370                null,
1371                true);
1372        var matchingResourceData = {};
1373        this.mapResourceCookies(domainToResourcesMap, allCookies, collectorCallback.bind(this));
1374
1375        for (var requestDomain in cookiesPerResourceDomain) {
1376            var cookies = cookiesPerResourceDomain[requestDomain];
1377            sortedCookieSizes.push({
1378                domain: requestDomain,
1379                avgCookieSize: this._average(cookies),
1380                maxCookieSize: this._max(cookies)
1381            });
1382        }
1383        var avgAllCookiesSize = this._average(allCookies);
1384
1385        var hugeCookieDomains = [];
1386        sortedCookieSizes.sort(maxSizeSorter);
1387
1388        for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1389            var maxCookieSize = sortedCookieSizes[i].maxCookieSize;
1390            if (maxCookieSize > this._maxBytesThreshold)
1391                hugeCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(sortedCookieSizes[i].domain) + ": " + Number.bytesToString(maxCookieSize));
1392        }
1393
1394        var bigAvgCookieDomains = [];
1395        sortedCookieSizes.sort(avgSizeSorter);
1396        for (var i = 0, len = sortedCookieSizes.length; i < len; ++i) {
1397            var domain = sortedCookieSizes[i].domain;
1398            var avgCookieSize = sortedCookieSizes[i].avgCookieSize;
1399            if (avgCookieSize > this._avgBytesThreshold && avgCookieSize < this._maxBytesThreshold)
1400                bigAvgCookieDomains.push(WebInspector.AuditRuleResult.resourceDomain(domain) + ": " + Number.bytesToString(avgCookieSize));
1401        }
1402        result.addChild(String.sprintf("The average cookie size for all requests on this page is %s", Number.bytesToString(avgAllCookiesSize)));
1403
1404        var message;
1405        if (hugeCookieDomains.length) {
1406            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);
1407            entry.addURLs(hugeCookieDomains);
1408            result.violationCount += hugeCookieDomains.length;
1409        }
1410
1411        if (bigAvgCookieDomains.length) {
1412            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);
1413            entry.addURLs(bigAvgCookieDomains);
1414            result.violationCount += bigAvgCookieDomains.length;
1415        }
1416    },
1417
1418    __proto__: WebInspector.AuditRules.CookieRuleBase.prototype
1419}
1420
1421/**
1422 * @constructor
1423 * @extends {WebInspector.AuditRules.CookieRuleBase}
1424 */
1425WebInspector.AuditRules.StaticCookielessRule = function(minResources)
1426{
1427    WebInspector.AuditRules.CookieRuleBase.call(this, "http-staticcookieless", "Serve static content from a cookieless domain");
1428    this._minResources = minResources;
1429}
1430
1431WebInspector.AuditRules.StaticCookielessRule.prototype = {
1432    processCookies: function(allCookies, requests, result)
1433    {
1434        var domainToResourcesMap = WebInspector.AuditRules.getDomainToResourcesMap(requests,
1435                [WebInspector.resourceTypes.Stylesheet,
1436                 WebInspector.resourceTypes.Image],
1437                true);
1438        var totalStaticResources = 0;
1439        for (var domain in domainToResourcesMap)
1440            totalStaticResources += domainToResourcesMap[domain].length;
1441        if (totalStaticResources < this._minResources)
1442            return;
1443        var matchingResourceData = {};
1444        this.mapResourceCookies(domainToResourcesMap, allCookies, this._collectorCallback.bind(this, matchingResourceData));
1445
1446        var badUrls = [];
1447        var cookieBytes = 0;
1448        for (var url in matchingResourceData) {
1449            badUrls.push(url);
1450            cookieBytes += matchingResourceData[url]
1451        }
1452        if (badUrls.length < this._minResources)
1453            return;
1454
1455        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);
1456        entry.addURLs(badUrls);
1457        result.violationCount = badUrls.length;
1458    },
1459
1460    _collectorCallback: function(matchingResourceData, request, cookie)
1461    {
1462        matchingResourceData[request.url] = (matchingResourceData[request.url] || 0) + cookie.size();
1463    },
1464
1465    __proto__: WebInspector.AuditRules.CookieRuleBase.prototype
1466}
1467