2011-01-13 Mikhail Naganov <mnaganov@chromium.org>
[WebKit-https.git] / Source / WebCore / inspector / front-end / Resource.js
1 /*
2  * Copyright (C) 2007, 2008 Apple 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
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28 WebInspector.Resource = function(identifier, url)
29 {
30     this.identifier = identifier;
31     this.url = url;
32     this._startTime = -1;
33     this._endTime = -1;
34     this._requestMethod = "";
35     this._category = WebInspector.resourceCategories.other;
36     this._pendingContentCallbacks = [];
37 }
38
39 // Keep these in sync with WebCore::InspectorResource::Type
40 WebInspector.Resource.Type = {
41     Document:   0,
42     Stylesheet: 1,
43     Image:      2,
44     Font:       3,
45     Script:     4,
46     XHR:        5,
47     Media:      6,
48     WebSocket:  7,
49     Other:      8,
50
51     isTextType: function(type)
52     {
53         return (type === this.Document) || (type === this.Stylesheet) || (type === this.Script) || (type === this.XHR);
54     },
55
56     toUIString: function(type)
57     {
58         switch (type) {
59             case this.Document:
60                 return WebInspector.UIString("Document");
61             case this.Stylesheet:
62                 return WebInspector.UIString("Stylesheet");
63             case this.Image:
64                 return WebInspector.UIString("Image");
65             case this.Font:
66                 return WebInspector.UIString("Font");
67             case this.Script:
68                 return WebInspector.UIString("Script");
69             case this.XHR:
70                 return WebInspector.UIString("XHR");
71             case this.Media:
72                 return WebInspector.UIString("Media");
73             case this.WebSocket:
74                 return WebInspector.UIString("WebSocket");
75             case this.Other:
76             default:
77                 return WebInspector.UIString("Other");
78         }
79     },
80
81     // Returns locale-independent string identifier of resource type (primarily for use in extension API).
82     // The IDs need to be kept in sync with webInspector.resoureces.Types object in ExtensionAPI.js.
83     toString: function(type)
84     {
85         switch (type) {
86             case this.Document:
87                 return "document";
88             case this.Stylesheet:
89                 return "stylesheet";
90             case this.Image:
91                 return "image";
92             case this.Font:
93                 return "font";
94             case this.Script:
95                 return "script";
96             case this.XHR:
97                 return "xhr";
98             case this.Media:
99                 return "media";
100             case this.WebSocket:
101                 return "websocket";
102             case this.Other:
103             default:
104                 return "other";
105         }
106     }
107 }
108
109 WebInspector.Resource.prototype = {
110     get url()
111     {
112         return this._url;
113     },
114
115     set url(x)
116     {
117         if (this._url === x)
118             return;
119
120         this._url = x;
121         delete this._parsedQueryParameters;
122
123         var parsedURL = x.asParsedURL();
124         this.domain = parsedURL ? parsedURL.host : "";
125         this.path = parsedURL ? parsedURL.path : "";
126         this.lastPathComponent = "";
127         if (parsedURL && parsedURL.path) {
128             // First cut the query params.
129             var path = parsedURL.path;
130             var indexOfQuery = path.indexOf("?");
131             if (indexOfQuery !== -1)
132                 path = path.substring(0, indexOfQuery);
133
134             // Then take last path component.
135             var lastSlashIndex = path.lastIndexOf("/");
136             if (lastSlashIndex !== -1)
137                 this.lastPathComponent = path.substring(lastSlashIndex + 1);
138         }
139         this.lastPathComponentLowerCase = this.lastPathComponent.toLowerCase();
140     },
141
142     get documentURL()
143     {
144         return this._documentURL;
145     },
146
147     set documentURL(x)
148     {
149         this._documentURL = x;
150     },
151
152     get displayName()
153     {
154         if (this._displayName)
155             return this._displayName;
156         this._displayName = this.lastPathComponent;
157         if (!this._displayName)
158             this._displayName = this.displayDomain;
159         if (!this._displayName && this.url)
160             this._displayName = this.url.trimURL(WebInspector.mainResource ? WebInspector.mainResource.domain : "");
161         if (this._displayName === "/")
162             this._displayName = this.url;
163         return this._displayName;
164     },
165
166     get displayDomain()
167     {
168         // WebInspector.Database calls this, so don't access more than this.domain.
169         if (this.domain && (!WebInspector.mainResource || (WebInspector.mainResource && this.domain !== WebInspector.mainResource.domain)))
170             return this.domain;
171         return "";
172     },
173
174     get startTime()
175     {
176         return this._startTime || -1;
177     },
178
179     set startTime(x)
180     {
181         this._startTime = x;
182     },
183
184     get responseReceivedTime()
185     {
186         return this._responseReceivedTime || -1;
187     },
188
189     set responseReceivedTime(x)
190     {
191         this._responseReceivedTime = x;
192     },
193
194     get endTime()
195     {
196         return this._endTime || -1;
197     },
198
199     set endTime(x)
200     {
201         if (this.timing && this.timing.requestTime) {
202             // Check against accurate responseReceivedTime.
203             this._endTime = Math.max(x, this.responseReceivedTime);
204         } else {
205             // Prefer endTime since it might be from the network stack.
206             this._endTime = x;
207             if (this._responseReceivedTime > x)
208                 this._responseReceivedTime = x;
209         }
210     },
211
212     get duration()
213     {
214         if (this._endTime === -1 || this._startTime === -1)
215             return -1;
216         return this._endTime - this._startTime;
217     },
218
219     get latency()
220     {
221         if (this._responseReceivedTime === -1 || this._startTime === -1)
222             return -1;
223         return this._responseReceivedTime - this._startTime;
224     },
225
226     get receiveDuration()
227     {
228         if (this._endTime === -1 || this._responseReceivedTime === -1)
229             return -1;
230         return this._endTime - this._responseReceivedTime;
231     },
232
233     get resourceSize()
234     {
235         return this._resourceSize || 0;
236     },
237
238     set resourceSize(x)
239     {
240         this._resourceSize = x;
241     },
242
243     get transferSize()
244     {
245         // FIXME: this is wrong for chunked-encoding resources.
246         return this.cached ? 0 : Number(this.responseHeaders["Content-Length"] || this.resourceSize || 0);
247     },
248
249     get expectedContentLength()
250     {
251         return this._expectedContentLength || 0;
252     },
253
254     set expectedContentLength(x)
255     {
256         this._expectedContentLength = x;
257     },
258
259     get finished()
260     {
261         return this._finished;
262     },
263
264     set finished(x)
265     {
266         if (this._finished === x)
267             return;
268
269         this._finished = x;
270
271         if (x) {
272             this._checkWarnings();
273             this.dispatchEventToListeners("finished");
274             if (this._pendingContentCallbacks.length)
275                 this._innerRequestContent();
276         }
277     },
278
279     get failed()
280     {
281         return this._failed;
282     },
283
284     set failed(x)
285     {
286         this._failed = x;
287     },
288
289     get category()
290     {
291         return this._category;
292     },
293
294     set category(x)
295     {
296         this._category = x;
297     },
298
299     get cached()
300     {
301         return this._cached;
302     },
303
304     set cached(x)
305     {
306         this._cached = x;
307         if (x)
308             delete this._timing;
309     },
310
311
312     get timing()
313     {
314         return this._timing;
315     },
316
317     set timing(x)
318     {
319         if (x && !this._cached) {
320             // Take startTime and responseReceivedTime from timing data for better accuracy.
321             // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis.
322             this._startTime = x.requestTime;
323             this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0;
324
325             this._timing = x;
326             this.dispatchEventToListeners("timing changed");
327         }
328     },
329
330     get mimeType()
331     {
332         return this._mimeType;
333     },
334
335     set mimeType(x)
336     {
337         this._mimeType = x;
338     },
339
340     get type()
341     {
342         return this._type;
343     },
344
345     set type(x)
346     {
347         if (this._type === x)
348             return;
349
350         this._type = x;
351
352         switch (x) {
353             case WebInspector.Resource.Type.Document:
354                 this.category = WebInspector.resourceCategories.documents;
355                 break;
356             case WebInspector.Resource.Type.Stylesheet:
357                 this.category = WebInspector.resourceCategories.stylesheets;
358                 break;
359             case WebInspector.Resource.Type.Script:
360                 this.category = WebInspector.resourceCategories.scripts;
361                 break;
362             case WebInspector.Resource.Type.Image:
363                 this.category = WebInspector.resourceCategories.images;
364                 break;
365             case WebInspector.Resource.Type.Font:
366                 this.category = WebInspector.resourceCategories.fonts;
367                 break;
368             case WebInspector.Resource.Type.XHR:
369                 this.category = WebInspector.resourceCategories.xhr;
370                 break;
371             case WebInspector.Resource.Type.WebSocket:
372                 this.category = WebInspector.resourceCategories.websockets;
373                 break;
374             case WebInspector.Resource.Type.Other:
375             default:
376                 this.category = WebInspector.resourceCategories.other;
377                 break;
378         }
379     },
380
381     get requestHeaders()
382     {
383         return this._requestHeaders || {};
384     },
385
386     set requestHeaders(x)
387     {
388         this._requestHeaders = x;
389         delete this._sortedRequestHeaders;
390         delete this._requestCookies;
391
392         this.dispatchEventToListeners("requestHeaders changed");
393     },
394
395     get sortedRequestHeaders()
396     {
397         if (this._sortedRequestHeaders !== undefined)
398             return this._sortedRequestHeaders;
399
400         this._sortedRequestHeaders = [];
401         for (var key in this.requestHeaders)
402             this._sortedRequestHeaders.push({header: key, value: this.requestHeaders[key]});
403         this._sortedRequestHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
404
405         return this._sortedRequestHeaders;
406     },
407
408     requestHeaderValue: function(headerName)
409     {
410         return this._headerValue(this.requestHeaders, headerName);
411     },
412
413     get requestCookies()
414     {
415         if (!this._requestCookies)
416             this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie"));
417         return this._requestCookies;
418     },
419
420     get requestFormData()
421     {
422         return this._requestFormData;
423     },
424
425     set requestFormData(x)
426     {
427         this._requestFormData = x;
428         delete this._parsedFormParameters;
429     },
430
431     get responseHeaders()
432     {
433         return this._responseHeaders || {};
434     },
435
436     set responseHeaders(x)
437     {
438         this._responseHeaders = x;
439         delete this._sortedResponseHeaders;
440         delete this._responseCookies;
441
442         this.dispatchEventToListeners("responseHeaders changed");
443     },
444
445     get sortedResponseHeaders()
446     {
447         if (this._sortedResponseHeaders !== undefined)
448             return this._sortedResponseHeaders;
449
450         this._sortedResponseHeaders = [];
451         for (var key in this.responseHeaders)
452             this._sortedResponseHeaders.push({header: key, value: this.responseHeaders[key]});
453         this._sortedResponseHeaders.sort(function(a,b) { return a.header.localeCompare(b.header) });
454
455         return this._sortedResponseHeaders;
456     },
457
458     responseHeaderValue: function(headerName)
459     {
460         return this._headerValue(this.responseHeaders, headerName);
461     },
462
463     get responseCookies()
464     {
465         if (!this._responseCookies)
466             this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie"));
467         return this._responseCookies;
468     },
469
470     get queryParameters()
471     {
472         if (this._parsedQueryParameters)
473             return this._parsedQueryParameters;
474         var queryString = this.url.split("?", 2)[1];
475         if (!queryString)
476             return;
477         this._parsedQueryParameters = this._parseParameters(queryString);
478         return this._parsedQueryParameters;
479     },
480
481     get formParameters()
482     {
483         if (this._parsedFormParameters)
484             return this._parsedFormParameters;
485         if (!this.requestFormData)
486             return;
487         var requestContentType = this.requestHeaderValue("Content-Type");
488         if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
489             return;
490         this._parsedFormParameters = this._parseParameters(this.requestFormData);
491         return this._parsedFormParameters;
492     },
493
494     _parseParameters: function(queryString)
495     {
496         function parseNameValue(pair)
497         {
498             var parameter = {};
499             var splitPair = pair.split("=", 2);
500
501             parameter.name = splitPair[0];
502             if (splitPair.length === 1)
503                 parameter.value = "";
504             else
505                 parameter.value = splitPair[1];
506             return parameter;
507         }
508         return queryString.split("&").map(parseNameValue);
509     },
510
511     _headerValue: function(headers, headerName)
512     {
513         headerName = headerName.toLowerCase();
514         for (var header in headers) {
515             if (header.toLowerCase() === headerName)
516                 return headers[header];
517         }
518     },
519
520     get errors()
521     {
522         return this._errors || 0;
523     },
524
525     set errors(x)
526     {
527         this._errors = x;
528         this.dispatchEventToListeners("errors-warnings-updated");
529     },
530
531     get warnings()
532     {
533         return this._warnings || 0;
534     },
535
536     set warnings(x)
537     {
538         this._warnings = x;
539         this.dispatchEventToListeners("errors-warnings-updated");
540     },
541
542     clearErrorsAndWarnings: function()
543     {
544         this._warnings = 0;
545         this._errors = 0;
546         this.dispatchEventToListeners("errors-warnings-updated");
547     },
548
549     _mimeTypeIsConsistentWithType: function()
550     {
551         // If status is an error, content is likely to be of an inconsistent type,
552         // as it's going to be an error message. We do not want to emit a warning
553         // for this, though, as this will already be reported as resource loading failure.
554         if (this.statusCode >= 400)
555             return true;
556
557         if (typeof this.type === "undefined"
558          || this.type === WebInspector.Resource.Type.Other
559          || this.type === WebInspector.Resource.Type.XHR
560          || this.type === WebInspector.Resource.Type.WebSocket)
561             return true;
562
563         if (!this.mimeType)
564             return true; // Might be not known for cached resources with null responses.
565
566         if (this.mimeType in WebInspector.MIMETypes)
567             return this.type in WebInspector.MIMETypes[this.mimeType];
568
569         return false;
570     },
571
572     _checkWarnings: function()
573     {
574         for (var warning in WebInspector.Warnings)
575             this._checkWarning(WebInspector.Warnings[warning]);
576     },
577
578     _checkWarning: function(warning)
579     {
580         var msg;
581         switch (warning.id) {
582             case WebInspector.Warnings.IncorrectMIMEType.id:
583                 if (!this._mimeTypeIsConsistentWithType())
584                     msg = new WebInspector.ConsoleMessage(WebInspector.ConsoleMessage.MessageSource.Other,
585                         WebInspector.ConsoleMessage.MessageType.Log,
586                         WebInspector.ConsoleMessage.MessageLevel.Warning,
587                         -1,
588                         this.url,
589                         1,
590                         String.sprintf(WebInspector.Warnings.IncorrectMIMEType.message, WebInspector.Resource.Type.toUIString(this.type), this.mimeType),
591                         null,
592                         null);
593                 break;
594         }
595
596         if (msg)
597             WebInspector.console.addMessage(msg);
598     },
599
600     get content()
601     {
602         return this._content;
603     },
604
605     get contentTimestamp()
606     {
607         return this._contentTimestamp;
608     },
609
610     setInitialContent: function(content)
611     {
612         this._content = content;
613     },
614
615     isLocallyModified: function()
616     {
617         return !!this._baseRevision;
618     },
619
620     setContent: function(newContent, onRevert)
621     {
622         var revisionResource = new WebInspector.Resource(null, this.url);
623         revisionResource.type = this.type;
624         revisionResource.loader = this.loader;
625         revisionResource.timestamp = this.timestamp;
626         revisionResource._content = this._content;
627         revisionResource._actualResource = this;
628         revisionResource._fireOnRevert = onRevert;
629
630         if (this.finished)
631             revisionResource.finished = true;
632         else {
633             function finished()
634             {
635                 this.removeEventListener("finished", finished);
636                 revisionResource.finished = true;
637             }
638             this.addEventListener("finished", finished.bind(this));
639         }
640
641         if (!this._baseRevision)
642             this._baseRevision = revisionResource;
643         else
644             revisionResource._baseRevision = this._baseRevision;
645
646         var data = { revision: revisionResource };
647         this._content = newContent;
648         this.timestamp = new Date();
649         this.dispatchEventToListeners("content-changed", data);
650     },
651
652     revertToThis: function()
653     {
654         if (!this._actualResource || !this._fireOnRevert)
655             return;
656
657         function callback(content)
658         {
659             if (content)
660                 this._fireOnRevert(content);
661         }
662         this.requestContent(callback.bind(this));
663     },
664
665     get baseRevision()
666     {
667         return this._baseRevision;
668     },
669
670     requestContent: function(callback)
671     {
672         if (this._content) {
673             callback(this._content, this._contentEncoded);
674             return;
675         }
676         this._pendingContentCallbacks.push(callback);
677         if (this.finished)
678             this._innerRequestContent();
679     },
680
681     get contentURL()
682     {
683         const maxDataUrlSize = 1024 * 1024;
684         // If resource content is not available or won't fit a data URL, fall back to using original URL.
685         if (!this._content || this._content.length > maxDataUrlSize)
686             return this.url;
687
688         return "data:" + this.mimeType + (this._contentEncoded ? ";base64," : ",") + this._content;
689     },
690
691     _innerRequestContent: function()
692     {
693         if (this._contentRequested)
694             return;
695         this._contentRequested = true;
696         this._contentEncoded = !WebInspector.Resource.Type.isTextType(this.type);
697
698         function onResourceContent(data)
699         {
700             this._content = data;
701             var callbacks = this._pendingContentCallbacks.slice();
702             for (var i = 0; i < callbacks.length; ++i)
703                 callbacks[i](this._content, this._contentEncoded);
704             this._pendingContentCallbacks.length = 0;
705             delete this._contentRequested;
706         }
707         WebInspector.NetworkManager.requestContent(this, this._contentEncoded, onResourceContent.bind(this));
708     }
709 }
710
711 WebInspector.Resource.prototype.__proto__ = WebInspector.Object.prototype;