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