[Web Inspector] Fix Sort by Initiator functionality of Network Panel.
[WebKit-https.git] / Source / WebCore / inspector / front-end / NetworkRequest.js
1 /*
2  * Copyright (C) 2012 Google Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 /**
32  * @constructor
33  * @extends {WebInspector.Object}
34  * @implements {WebInspector.ContentProvider}
35  * @param {NetworkAgent.RequestId} requestId
36  * @param {string} url
37  * @param {string} documentURL
38  * @param {NetworkAgent.FrameId} frameId
39  * @param {NetworkAgent.LoaderId} loaderId
40  */
41 WebInspector.NetworkRequest = function(requestId, url, documentURL, frameId, loaderId)
42 {
43     this._requestId = requestId;
44     this.url = url;
45     this._documentURL = documentURL;
46     this._frameId = frameId;
47     this._loaderId = loaderId;
48     this._startTime = -1;
49     this._endTime = -1;
50
51     this.statusCode = 0;
52     this.statusText = "";
53     this.requestMethod = "";
54     this.requestTime = 0;
55     this.receiveHeadersEnd = 0;
56
57     this._type = WebInspector.resourceTypes.Other;
58     this._contentEncoded = false;
59     this._pendingContentCallbacks = [];
60     this._frames = [];
61 }
62
63 WebInspector.NetworkRequest.Events = {
64     FinishedLoading: "FinishedLoading",
65     TimingChanged: "TimingChanged",
66     RequestHeadersChanged: "RequestHeadersChanged",
67     ResponseHeadersChanged: "ResponseHeadersChanged",
68 }
69
70 WebInspector.NetworkRequest.InitiatorType = {
71     Parser: "parser",
72     Script: "script",
73     Other: "other",
74 }
75
76 WebInspector.NetworkRequest.prototype = {
77     /**
78      * @return {NetworkAgent.RequestId}
79      */
80     get requestId()
81     {
82         return this._requestId;
83     },
84
85     set requestId(requestId)
86     {
87         this._requestId = requestId;
88     },
89
90     /**
91      * @return {string}
92      */
93     get url()
94     {
95         return this._url;
96     },
97
98     set url(x)
99     {
100         if (this._url === x)
101             return;
102
103         this._url = x;
104         this._parsedURL = new WebInspector.ParsedURL(x);
105         delete this._parsedQueryParameters;
106         delete this._name;
107         delete this._path;
108     },
109
110     /**
111      * @return {string}
112      */
113     get documentURL()
114     {
115         return this._documentURL;
116     },
117
118     get parsedURL()
119     {
120         return this._parsedURL;
121     },
122
123     /**
124      * @return {NetworkAgent.FrameId}
125      */
126     get frameId()
127     {
128         return this._frameId;
129     },
130
131     /**
132      * @return {NetworkAgent.LoaderId}
133      */
134     get loaderId()
135     {
136         return this._loaderId;
137     },
138
139     /**
140      * @return {number}
141      */
142     get startTime()
143     {
144         return this._startTime || -1;
145     },
146
147     set startTime(x)
148     {
149         this._startTime = x;
150     },
151
152     /**
153      * @return {number}
154      */
155     get responseReceivedTime()
156     {
157         return this._responseReceivedTime || -1;
158     },
159
160     set responseReceivedTime(x)
161     {
162         this._responseReceivedTime = x;
163     },
164
165     /**
166      * @return {number}
167      */
168     get endTime()
169     {
170         return this._endTime || -1;
171     },
172
173     set endTime(x)
174     {
175         if (this.timing && this.timing.requestTime) {
176             // Check against accurate responseReceivedTime.
177             this._endTime = Math.max(x, this.responseReceivedTime);
178         } else {
179             // Prefer endTime since it might be from the network stack.
180             this._endTime = x;
181             if (this._responseReceivedTime > x)
182                 this._responseReceivedTime = x;
183         }
184     },
185
186     /**
187      * @return {number}
188      */
189     get duration()
190     {
191         if (this._endTime === -1 || this._startTime === -1)
192             return -1;
193         return this._endTime - this._startTime;
194     },
195
196     /**
197      * @return {number}
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     /**
207      * @return {number}
208      */
209     get receiveDuration()
210     {
211         if (this._endTime === -1 || this._responseReceivedTime === -1)
212             return -1;
213         return this._endTime - this._responseReceivedTime;
214     },
215
216     /**
217      * @return {number}
218      */
219     get resourceSize()
220     {
221         return this._resourceSize || 0;
222     },
223
224     set resourceSize(x)
225     {
226         this._resourceSize = x;
227     },
228
229     /**
230      * @return {number}
231      */
232     get transferSize()
233     {
234         if (this.cached)
235             return 0;
236         if (this.statusCode === 304) // Not modified
237             return this.responseHeadersSize;
238         if (this._transferSize !== undefined)
239             return this._transferSize;
240         // If we did not receive actual transfer size from network
241         // stack, we prefer using Content-Length over resourceSize as
242         // resourceSize may differ from actual transfer size if platform's
243         // network stack performed decoding (e.g. gzip decompression).
244         // The Content-Length, though, is expected to come from raw
245         // response headers and will reflect actual transfer length.
246         // This won't work for chunked content encoding, so fall back to
247         // resourceSize when we don't have Content-Length. This still won't
248         // work for chunks with non-trivial encodings. We need a way to
249         // get actual transfer size from the network stack.
250         var bodySize = Number(this.responseHeaderValue("Content-Length") || this.resourceSize);
251         return this.responseHeadersSize + bodySize;
252     },
253
254     /**
255      * @param {number} x
256      */
257     increaseTransferSize: function(x)
258     {
259         this._transferSize = (this._transferSize || 0) + x;
260     },
261
262     /**
263      * @return {boolean}
264      */
265     get finished()
266     {
267         return this._finished;
268     },
269
270     set finished(x)
271     {
272         if (this._finished === x)
273             return;
274
275         this._finished = x;
276
277         if (x) {
278             this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.FinishedLoading, this);
279             if (this._pendingContentCallbacks.length)
280                 this._innerRequestContent();
281         }
282     },
283
284     /**
285      * @return {boolean}
286      */
287     get failed()
288     {
289         return this._failed;
290     },
291
292     set failed(x)
293     {
294         this._failed = x;
295     },
296
297     /**
298      * @return {boolean}
299      */
300     get canceled()
301     {
302         return this._canceled;
303     },
304
305     set canceled(x)
306     {
307         this._canceled = x;
308     },
309
310     /**
311      * @return {boolean}
312      */
313     get cached()
314     {
315         return this._cached;
316     },
317
318     set cached(x)
319     {
320         this._cached = x;
321         if (x)
322             delete this._timing;
323     },
324
325     /**
326      * @return {NetworkAgent.ResourceTiming|undefined}
327      */
328     get timing()
329     {
330         return this._timing;
331     },
332
333     set timing(x)
334     {
335         if (x && !this._cached) {
336             // Take startTime and responseReceivedTime from timing data for better accuracy.
337             // Timing's requestTime is a baseline in seconds, rest of the numbers there are ticks in millis.
338             this._startTime = x.requestTime;
339             this._responseReceivedTime = x.requestTime + x.receiveHeadersEnd / 1000.0;
340
341             this._timing = x;
342             this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.TimingChanged, this);
343         }
344     },
345
346     /**
347      * @return {string}
348      */
349     get mimeType()
350     {
351         return this._mimeType;
352     },
353
354     set mimeType(x)
355     {
356         this._mimeType = x;
357     },
358
359     /**
360      * @return {string}
361      */
362     get displayName()
363     {
364         return this._parsedURL.displayName;
365     },
366
367     name: function()
368     {
369         if (this._name)
370             return this._name;
371         this._parseNameAndPathFromURL();
372         return this._name;
373     },
374
375     path: function()
376     {
377         if (this._path)
378             return this._path;
379         this._parseNameAndPathFromURL();
380         return this._path;
381     },
382
383     _parseNameAndPathFromURL: function()
384     {
385         if (this._parsedURL.isDataURL()) {
386             this._name = this._parsedURL.dataURLDisplayName();
387             this._path = "";
388         } else if (this._parsedURL.isAboutBlank()) {
389             this._name = this._parsedURL.url;
390             this._path = "";
391         } else {
392             this._path = this._parsedURL.host + this._parsedURL.folderPathComponents;
393             this._path = this._path.trimURL(WebInspector.inspectedPageDomain ? WebInspector.inspectedPageDomain : "");
394             if (this._parsedURL.lastPathComponent || this._parsedURL.queryParams)
395                 this._name = this._parsedURL.lastPathComponent + (this._parsedURL.queryParams ? "?" + this._parsedURL.queryParams : "");
396             else if (this._parsedURL.folderPathComponents) {
397                 this._name = this._parsedURL.folderPathComponents.substring(this._parsedURL.folderPathComponents.lastIndexOf("/") + 1) + "/";
398                 this._path = this._path.substring(0, this._path.lastIndexOf("/"));
399             } else {
400                 this._name = this._parsedURL.host;
401                 this._path = "";
402             }
403         }
404     },
405
406     /**
407      * @return {string}
408      */
409     get folder()
410     {
411         var path = this._parsedURL.path;
412         var indexOfQuery = path.indexOf("?");
413         if (indexOfQuery !== -1)
414             path = path.substring(0, indexOfQuery);
415         var lastSlashIndex = path.lastIndexOf("/");
416         return lastSlashIndex !== -1 ? path.substring(0, lastSlashIndex) : "";
417     },
418
419     /**
420      * @return {WebInspector.ResourceType}
421      */
422     get type()
423     {
424         return this._type;
425     },
426
427     set type(x)
428     {
429         this._type = x;
430     },
431
432     /**
433      * @return {string}
434      */
435     get domain()
436     {
437         return this._parsedURL.host;
438     },
439
440     /**
441      * @return {?WebInspector.NetworkRequest}
442      */
443     get redirectSource()
444     {
445         if (this.redirects && this.redirects.length > 0)
446             return this.redirects[this.redirects.length - 1];
447         return this._redirectSource;
448     },
449
450     set redirectSource(x)
451     {
452         this._redirectSource = x;
453     },
454
455     /**
456      * @return {Array.<Object>}
457      */
458     get requestHeaders()
459     {
460         return this._requestHeaders || [];
461     },
462
463     set requestHeaders(x)
464     {
465         this._requestHeaders = x;
466         delete this._sortedRequestHeaders;
467         delete this._requestCookies;
468
469         this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.RequestHeadersChanged);
470     },
471
472     /**
473      * @return {string}
474      */
475     get requestHeadersText()
476     {
477         if (typeof this._requestHeadersText === "undefined") {
478             this._requestHeadersText = this.requestMethod + " " + this.url + " HTTP/1.1\r\n";
479             for (var i = 0; i < this.requestHeaders.length; ++i)
480                 this._requestHeadersText += this.requestHeaders[i].name + ": " + this.requestHeaders[i].value + "\r\n";
481         }
482         return this._requestHeadersText;
483     },
484
485     set requestHeadersText(x)
486     {
487         this._requestHeadersText = x;
488
489         this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.RequestHeadersChanged);
490     },
491
492     /**
493      * @return {number}
494      */
495     get requestHeadersSize()
496     {
497         return this.requestHeadersText.length;
498     },
499
500     /**
501      * @return {Array.<Object>}
502      */
503     get sortedRequestHeaders()
504     {
505         if (this._sortedRequestHeaders !== undefined)
506             return this._sortedRequestHeaders;
507
508         this._sortedRequestHeaders = [];
509         this._sortedRequestHeaders = this.requestHeaders.slice();
510         this._sortedRequestHeaders.sort(function(a,b) { return a.name.toLowerCase().compareTo(b.name.toLowerCase()) });
511         return this._sortedRequestHeaders;
512     },
513
514     /**
515      * @param {string} headerName
516      * @return {string|undefined}
517      */
518     requestHeaderValue: function(headerName)
519     {
520         return this._headerValue(this.requestHeaders, headerName);
521     },
522
523     /**
524      * @return {Array.<WebInspector.Cookie>}
525      */
526     get requestCookies()
527     {
528         if (!this._requestCookies)
529             this._requestCookies = WebInspector.CookieParser.parseCookie(this.requestHeaderValue("Cookie"));
530         return this._requestCookies;
531     },
532
533     /**
534      * @return {string|undefined}
535      */
536     get requestFormData()
537     {
538         return this._requestFormData;
539     },
540
541     set requestFormData(x)
542     {
543         this._requestFormData = x;
544         delete this._parsedFormParameters;
545     },
546
547     /**
548      * @return {string|undefined}
549      */
550     get requestHttpVersion()
551     {
552         var firstLine = this.requestHeadersText.split(/\r\n/)[0];
553         var match = firstLine.match(/(HTTP\/\d+\.\d+)$/);
554         return match ? match[1] : undefined;
555     },
556
557     /**
558      * @return {Array.<Object>}
559      */
560     get responseHeaders()
561     {
562         return this._responseHeaders || [];
563     },
564
565     set responseHeaders(x)
566     {
567         this._responseHeaders = x;
568         delete this._sortedResponseHeaders;
569         delete this._responseCookies;
570
571         this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.ResponseHeadersChanged);
572     },
573
574     /**
575      * @return {string}
576      */
577     get responseHeadersText()
578     {
579         if (typeof this._responseHeadersText === "undefined") {
580             this._responseHeadersText = "HTTP/1.1 " + this.statusCode + " " + this.statusText + "\r\n";
581             for (var i = 0; i < this.responseHeaders.length; ++i)
582                 this._responseHeadersText += this.responseHeaders[i].name + ": " + this.responseHeaders[i].value + "\r\n";
583         }
584         return this._responseHeadersText;
585     },
586
587     set responseHeadersText(x)
588     {
589         this._responseHeadersText = x;
590
591         this.dispatchEventToListeners(WebInspector.NetworkRequest.Events.ResponseHeadersChanged);
592     },
593
594     /**
595      * @return {number}
596      */
597     get responseHeadersSize()
598     {
599         return this.responseHeadersText.length;
600     },
601
602     /**
603      * @return {Array.<Object>}
604      */
605     get sortedResponseHeaders()
606     {
607         if (this._sortedResponseHeaders !== undefined)
608             return this._sortedResponseHeaders;
609         
610         this._sortedResponseHeaders = [];
611         this._sortedResponseHeaders = this.responseHeaders.slice();
612         this._sortedResponseHeaders.sort(function(a,b) { return a.name.toLowerCase().compareTo(b.name.toLowerCase()) });
613         return this._sortedResponseHeaders;
614     },
615
616     /**
617      * @param {string} headerName
618      * @return {string|undefined}
619      */
620     responseHeaderValue: function(headerName)
621     {
622         return this._headerValue(this.responseHeaders, headerName);
623     },
624
625     /**
626      * @return {Array.<WebInspector.Cookie>}
627      */
628     get responseCookies()
629     {
630         if (!this._responseCookies)
631             this._responseCookies = WebInspector.CookieParser.parseSetCookie(this.responseHeaderValue("Set-Cookie"));
632         return this._responseCookies;
633     },
634
635     /**
636      * @return {?string}
637      */
638     queryString: function()
639     {
640         if (this._queryString)
641             return this._queryString;
642         var queryString = this.url.split("?", 2)[1];
643         if (!queryString)
644             return null;
645         this._queryString = queryString.split("#", 2)[0];
646         return this._queryString;
647     },
648
649     /**
650      * @return {?Array.<Object>}
651      */
652     get queryParameters()
653     {
654         if (this._parsedQueryParameters)
655             return this._parsedQueryParameters;
656         var queryString = this.queryString();
657         if (!queryString)
658             return null;
659         this._parsedQueryParameters = this._parseParameters(queryString);
660         return this._parsedQueryParameters;
661     },
662
663     /**
664      * @return {?Array.<Object>}
665      */
666     get formParameters()
667     {
668         if (this._parsedFormParameters)
669             return this._parsedFormParameters;
670         if (!this.requestFormData)
671             return null;
672         var requestContentType = this.requestContentType();
673         if (!requestContentType || !requestContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
674             return null;
675         this._parsedFormParameters = this._parseParameters(this.requestFormData);
676         return this._parsedFormParameters;
677     },
678
679     /**
680      * @return {string|undefined}
681      */
682     get responseHttpVersion()
683     {
684         var match = this.responseHeadersText.match(/^(HTTP\/\d+\.\d+)/);
685         return match ? match[1] : undefined;
686     },
687
688     /**
689      * @param {string} queryString
690      * @return {Array.<Object>}
691      */
692     _parseParameters: function(queryString)
693     {
694         function parseNameValue(pair)
695         {
696             var parameter = {};
697             var splitPair = pair.split("=", 2);
698
699             parameter.name = splitPair[0];
700             if (splitPair.length === 1)
701                 parameter.value = "";
702             else
703                 parameter.value = splitPair[1];
704             return parameter;
705         }
706         return queryString.split("&").map(parseNameValue);
707     },
708
709     /**
710      * @param {Object} headers
711      * @param {string} headerName
712      * @return {string|undefined}
713      */
714     _headerValue: function(headers, headerName)
715     {
716         headerName = headerName.toLowerCase();
717         
718         var values = [];
719         for (var i = 0; i < headers.length; ++i) {
720             if (headers[i].name.toLowerCase() === headerName)
721                 values.push(headers[i].value);
722         }
723         if (!values.length)
724             return undefined;
725         // Set-Cookie values should be separated by '\n', not comma, otherwise cookies could not be parsed.
726         if (headerName === "set-cookie")
727             return values.join("\n");
728         return values.join(", ");
729     },
730
731     /**
732      * @return {?string|undefined}
733      */
734     get content()
735     {
736         return this._content;
737     },
738
739     /**
740      * @return {boolean}
741      */
742     get contentEncoded()
743     {
744         return this._contentEncoded;
745     },
746
747     /**
748      * @return {string}
749      */
750     contentURL: function()
751     {
752         return this._url;
753     },
754
755     /**
756      * @return {WebInspector.ResourceType}
757      */
758     contentType: function()
759     {
760         return this._type;
761     },
762
763     /**
764      * @param {function(?string, boolean, string)} callback
765      */
766     requestContent: function(callback)
767     {
768         // We do not support content retrieval for WebSockets at the moment.
769         // Since WebSockets are potentially long-living, fail requests immediately
770         // to prevent caller blocking until resource is marked as finished.
771         if (this.type === WebInspector.resourceTypes.WebSocket) {
772             callback(null, false, this._mimeType);
773             return;
774         }
775         if (typeof this._content !== "undefined") {
776             callback(this.content || null, this._contentEncoded, this._mimeType);
777             return;
778         }
779         this._pendingContentCallbacks.push(callback);
780         if (this.finished)
781             this._innerRequestContent();
782     },
783
784     /**
785      * @param {string} query
786      * @param {boolean} caseSensitive
787      * @param {boolean} isRegex
788      * @param {function(Array.<WebInspector.ContentProvider.SearchMatch>)} callback
789      */
790     searchInContent: function(query, caseSensitive, isRegex, callback)
791     {
792         callback([]);
793     },
794
795     /**
796      * @return {boolean}
797      */
798     isHttpFamily: function()
799     {
800         return !!this.url.match(/^https?:/i);
801     },
802
803     /**
804      * @return {string|undefined}
805      */
806     requestContentType: function()
807     {
808         return this.requestHeaderValue("Content-Type");
809     },
810
811     /**
812      * @return {boolean}
813      */
814     isPingRequest: function()
815     {
816         return "text/ping" === this.requestContentType();
817     },
818
819     /**
820      * @return {boolean}
821      */
822     hasErrorStatusCode: function()
823     {
824         return this.statusCode >= 400;
825     },
826
827     /**
828      * @param {Element} image
829      */
830     populateImageSource: function(image)
831     {
832         /**
833          * @this {WebInspector.NetworkRequest}
834          * @param {?string} content
835          * @param {boolean} contentEncoded
836          * @param {string} mimeType
837          */
838         function onResourceContent(content, contentEncoded, mimeType)
839         {
840             var imageSrc = this.asDataURL();
841             if (imageSrc === null)
842                 imageSrc = this.url;
843             image.src = imageSrc;
844         }
845
846         this.requestContent(onResourceContent.bind(this));
847     },
848
849     /**
850      * @return {?string}
851      */
852     asDataURL: function()
853     {
854         return WebInspector.contentAsDataURL(this._content, this.mimeType, this._contentEncoded);
855     },
856
857     _innerRequestContent: function()
858     {
859         if (this._contentRequested)
860             return;
861         this._contentRequested = true;
862
863         /**
864          * @param {?Protocol.Error} error
865          * @param {string} content
866          * @param {boolean} contentEncoded
867          */
868         function onResourceContent(error, content, contentEncoded)
869         {
870             this._content = error ? null : content;
871             this._contentEncoded = contentEncoded;
872             var callbacks = this._pendingContentCallbacks.slice();
873             for (var i = 0; i < callbacks.length; ++i)
874                 callbacks[i](this._content, this._contentEncoded, this._mimeType);
875             this._pendingContentCallbacks.length = 0;
876             delete this._contentRequested;
877         }
878         NetworkAgent.getResponseBody(this._requestId, onResourceContent.bind(this));
879     },
880
881     /**
882      * @return {Object}
883      */
884     frames: function()
885     {
886         return this._frames;
887     },
888
889     /**
890      * @param {number} position
891      * @return {Object}
892      */
893     frame: function(position)
894     {
895         return this._frames[position];
896     },
897
898     /**
899      * @param {string} errorMessage
900      * @param {number} time
901      */
902     addFrameError: function(errorMessage, time)
903     {
904         var errorObject = {};
905         errorObject.errorMessage = errorMessage;
906         errorObject.time = time;
907         this._pushFrame(errorObject);
908     },
909
910     /**
911      * @param {Object} response
912      * @param {number} time
913      * @param {boolean} sent
914      */
915     addFrame: function(response, time, sent)
916     {
917         response.time = time;
918         if (sent)
919             response.sent = true;
920         this._pushFrame(response);
921     },
922
923     _pushFrame: function(object)
924     {
925         if (this._frames.length >= 100) {
926             this._frames.splice(0, 10);
927         }
928         this._frames.push(object);
929     },
930
931     __proto__: WebInspector.Object.prototype
932 }