Web Inspector: Show Resource Initiator in Network Tab detail views
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / ResourceHeadersContentView.js
1 /*
2  * Copyright (C) 2017 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  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.ResourceHeadersContentView = class ResourceHeadersContentView extends WI.ContentView
27 {
28     constructor(resource, delegate)
29     {
30         super(null);
31
32         console.assert(resource instanceof WI.Resource);
33         console.assert(delegate);
34
35         this._resource = resource;
36         this._resource.addEventListener(WI.Resource.Event.MetricsDidChange, this._resourceMetricsDidChange, this);
37         this._resource.addEventListener(WI.Resource.Event.RequestHeadersDidChange, this._resourceRequestHeadersDidChange, this);
38         this._resource.addEventListener(WI.Resource.Event.ResponseReceived, this._resourceResponseReceived, this);
39
40         this._delegate = delegate;
41
42         this._searchQuery = null;
43         this._searchResults = null;
44         this._searchDOMChanges = [];
45         this._searchIndex = -1;
46         this._automaticallyRevealFirstSearchResult = false;
47         this._bouncyHighlightElement = null;
48         this._popover = null;
49         this._popoverCallStackIconElement = null;
50
51         this._redirectDetailsSections = [];
52
53         this.element.classList.add("resource-details", "resource-headers");
54         this.element.tabIndex = 0;
55
56         this._needsSummaryRefresh = false;
57         this._needsRedirectHeadersRefresh = false;
58         this._needsRequestHeadersRefresh = false;
59         this._needsResponseHeadersRefresh = false;
60     }
61
62     // Protected
63
64     initialLayout()
65     {
66         super.initialLayout();
67
68         this._summarySection = new WI.ResourceDetailsSection(WI.UIString("Summary"), "summary");
69         this.element.appendChild(this._summarySection.element);
70         this._refreshSummarySection();
71
72         this._refreshRedirectHeadersSections();
73
74         this._requestHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Request"), "headers");
75         this.element.appendChild(this._requestHeadersSection.element);
76         this._refreshRequestHeadersSection();
77
78         this._responseHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Response"), "headers");
79         this.element.appendChild(this._responseHeadersSection.element);
80         this._refreshResponseHeadersSection();
81
82         if (this._resource.urlComponents.queryString) {
83             this._queryStringSection = new WI.ResourceDetailsSection(WI.UIString("Query String Parameters"));
84             this.element.appendChild(this._queryStringSection.element);
85             this._refreshQueryStringSection();
86         }
87
88         if (this._resource.requestData) {
89             this._requestDataSection = new WI.ResourceDetailsSection(WI.UIString("Request Data"));
90             this.element.appendChild(this._requestDataSection.element);
91             this._refreshRequestDataSection();
92         }
93
94         this._needsSummaryRefresh = false;
95         this._needsRedirectHeadersRefresh = false;
96         this._needsRequestHeadersRefresh = false;
97         this._needsResponseHeadersRefresh = false;
98     }
99
100     layout()
101     {
102         super.layout();
103
104         if (this._needsSummaryRefresh) {
105             this._refreshSummarySection();
106             this._needsSummaryRefresh = false;
107         }
108
109         if (this._needsRedirectHeadersRefresh) {
110             this._refreshRedirectHeadersSections();
111             this._needsRedirectHeadersRefresh = false;
112         }
113
114         if (this._needsRequestHeadersRefresh) {
115             this._refreshRequestHeadersSection();
116             this._needsRequestHeadersRefresh = false;
117         }
118
119         if (this._needsResponseHeadersRefresh) {
120             this._refreshResponseHeadersSection();
121             this._needsResponseHeadersRefresh = false;
122         }
123     }
124
125     hidden()
126     {
127         super.hidden();
128
129         if (this._popover)
130             this._popover.dismiss();
131     }
132
133     closed()
134     {
135         this._resource.removeEventListener(null, null, this);
136
137         super.closed();
138     }
139
140     get supportsSearch()
141     {
142         return true;
143     }
144
145     get numberOfSearchResults()
146     {
147         return this._searchResults ? this._searchResults.length : null;
148     }
149
150     get hasPerformedSearch()
151     {
152         return this._searchResults !== null;
153     }
154
155     set automaticallyRevealFirstSearchResult(reveal)
156     {
157         this._automaticallyRevealFirstSearchResult = reveal;
158
159         // If we haven't shown a search result yet, reveal one now.
160         if (this._automaticallyRevealFirstSearchResult && this.numberOfSearchResults > 0) {
161             if (this._searchIndex === -1)
162                 this.revealNextSearchResult();
163         }
164     }
165
166     performSearch(query)
167     {
168         if (query === this._searchQuery)
169             return;
170
171         WI.revertDOMChanges(this._searchDOMChanges);
172
173         this._searchQuery = query;
174         this._searchResults = [];
175         this._searchDOMChanges = [];
176         this._searchIndex = -1;
177
178         this._perfomSearchOnKeyValuePairs();
179
180         this.dispatchEventToListeners(WI.ContentView.Event.NumberOfSearchResultsDidChange);
181
182         if (this._automaticallyRevealFirstSearchResult && this._searchResults.length > 0)
183             this.revealNextSearchResult();
184     }
185
186     searchCleared()
187     {
188         WI.revertDOMChanges(this._searchDOMChanges);
189
190         this._searchQuery = null;
191         this._searchResults = null;
192         this._searchDOMChanges = [];
193         this._searchIndex = -1;
194     }
195
196     revealPreviousSearchResult(changeFocus)
197     {
198         if (!this.numberOfSearchResults)
199             return;
200
201         if (this._searchIndex > 0)
202             --this._searchIndex;
203         else
204             this._searchIndex = this._searchResults.length - 1;
205
206         this._revealSearchResult(this._searchIndex, changeFocus);
207     }
208
209     revealNextSearchResult(changeFocus)
210     {
211         if (!this.numberOfSearchResults)
212             return;
213
214         if (this._searchIndex + 1 < this._searchResults.length)
215             ++this._searchIndex;
216         else
217             this._searchIndex = 0;
218
219         this._revealSearchResult(this._searchIndex, changeFocus);
220     }
221
222     // Private
223
224     _responseSourceDisplayString(responseSource)
225     {
226         switch (responseSource) {
227         case WI.Resource.ResponseSource.Network:
228             return WI.UIString("Network");
229         case WI.Resource.ResponseSource.MemoryCache:
230             return WI.UIString("Memory Cache");
231         case WI.Resource.ResponseSource.DiskCache:
232             return WI.UIString("Disk Cache");
233         case WI.Resource.ResponseSource.ServiceWorker:
234             return WI.UIString("Service Worker");
235         case WI.Resource.ResponseSource.Unknown:
236         default:
237             return null;
238         }
239     }
240
241     _refreshSummarySection()
242     {
243         let detailsElement = this._summarySection.detailsElement;
244         detailsElement.removeChildren();
245
246         this._summarySection.toggleError(this._resource.hadLoadingError());
247
248         for (let redirect of this._resource.redirects)
249             this._summarySection.appendKeyValuePair(WI.UIString("URL"), redirect.url.insertWordBreakCharacters(), "url");
250         this._summarySection.appendKeyValuePair(WI.UIString("URL"), this._resource.url.insertWordBreakCharacters(), "url");
251
252         let status = emDash;
253         if (!isNaN(this._resource.statusCode))
254             status = this._resource.statusCode + (this._resource.statusText ? " " + this._resource.statusText : "");
255         this._summarySection.appendKeyValuePair(WI.UIString("Status"), status);
256
257         // FIXME: <https://webkit.org/b/178827> Web Inspector: Should be able to link directly to the ServiceWorker that handled a particular load
258
259         let source = this._responseSourceDisplayString(this._resource.responseSource) || emDash;
260         this._summarySection.appendKeyValuePair(WI.UIString("Source"), source);
261
262         if (this._resource.remoteAddress)
263             this._summarySection.appendKeyValuePair(WI.UIString("Address"), this._resource.remoteAddress);
264
265         let initiatorLocation = this._resource.initiatorSourceCodeLocation;
266         if (initiatorLocation) {
267
268             let fragment = document.createDocumentFragment();
269
270             const options = {
271                 dontFloat: true,
272                 ignoreSearchTab: true,
273             };
274             let link = WI.createSourceCodeLocationLink(initiatorLocation, options);
275             fragment.appendChild(link);
276
277             let callFrames = this._resource.initiatorCallFrames;
278             if (callFrames) {
279                 this._popoverCallStackIconElement = document.createElement("img");
280                 this._popoverCallStackIconElement.className = "call-stack";
281                 fragment.appendChild(this._popoverCallStackIconElement);
282
283                 this._popoverCallStackIconElement.addEventListener("click", (event) => {
284                     if (!this._popover) {
285                         this._popover = new WI.Popover(this);
286                         this._popover.windowResizeHandler = () => { this._presentPopoverBelowCallStackElement() };
287                     }
288
289                     const selectable = false;
290                     let callFramesTreeOutline = new WI.TreeOutline(selectable);
291                     callFramesTreeOutline.disclosureButtons = false;
292                     let callFrameTreeController = new WI.CallFrameTreeController(callFramesTreeOutline);
293                     callFrameTreeController.callFrames = callFrames;
294
295                     let popoverContent = document.createElement("div");
296                     popoverContent.appendChild(callFrameTreeController.treeOutline.element);
297                     this._popover.content = popoverContent;
298
299                     this._presentPopoverBelowCallStackElement();
300                 });
301             }
302
303             let pair = this._summarySection.appendKeyValuePair(WI.UIString("Initiator"), fragment);
304             pair.classList.add("initiator");
305
306             if (this._popover && this._popover.visible)
307                 this._presentPopoverBelowCallStackElement();
308         }
309     }
310
311     _refreshRedirectHeadersSections()
312     {
313         let referenceElement = this._redirectDetailsSections.length ? this._redirectDetailsSections.lastValue.element : this._summarySection.element;
314
315         for (let i = this._redirectDetailsSections.length; i < this._resource.redirects.length; ++i) {
316             let redirect = this._resource.redirects[i];
317
318             let redirectRequestSection = new WI.ResourceDetailsSection(WI.UIString("Request"), "redirect");
319
320             // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests
321             redirectRequestSection.appendKeyValuePair(`${redirect.requestMethod} ${redirect.urlComponents.path}`, null, "h1-status");
322
323             for (let key in redirect.requestHeaders)
324                 redirectRequestSection.appendKeyValuePair(key, redirect.requestHeaders[key], "header");
325
326             referenceElement = this.element.insertBefore(redirectRequestSection.element, referenceElement.nextElementSibling);
327             this._redirectDetailsSections.push(redirectRequestSection);
328
329             let redirectResponseSection = new WI.ResourceDetailsSection(WI.UIString("Redirect Response"), "redirect");
330
331             // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests
332             redirectResponseSection.appendKeyValuePair(`${redirect.responseStatusCode} ${redirect.responseStatusText}`, null, "h1-status");
333
334             for (let key in redirect.responseHeaders)
335                 redirectResponseSection.appendKeyValuePair(key, redirect.responseHeaders[key], "header");
336
337             referenceElement = this.element.insertBefore(redirectResponseSection.element, referenceElement.nextElementSibling);
338             this._redirectDetailsSections.push(redirectResponseSection);
339         }
340     }
341
342     _refreshRequestHeadersSection()
343     {
344         let detailsElement = this._requestHeadersSection.detailsElement;
345         detailsElement.removeChildren();
346
347         // A revalidation request still sends a request even though we served from cache, so show the request.
348         if (this._resource.statusCode !== 304) {
349             if (this._resource.responseSource === WI.Resource.ResponseSource.MemoryCache) {
350                 this._requestHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No request, served from the memory cache."));
351                 return;
352             }
353             if (this._resource.responseSource === WI.Resource.ResponseSource.DiskCache) {
354                 this._requestHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No request, served from the disk cache."));
355                 return;
356             }
357         }
358
359         let protocol = this._resource.protocol || "";
360         let urlComponents = this._resource.urlComponents;
361         if (protocol.startsWith("http/1")) {
362             // HTTP/1.1 request line:
363             // https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
364             let requestLine = `${this._resource.requestMethod} ${urlComponents.path} ${protocol.toUpperCase()}`;
365             this._requestHeadersSection.appendKeyValuePair(requestLine, null, "h1-status");
366         } else if (protocol === "h2") {
367             // HTTP/2 Request pseudo headers:
368             // https://tools.ietf.org/html/rfc7540#section-8.1.2.3
369             this._requestHeadersSection.appendKeyValuePair(":method", this._resource.requestMethod, "h2-pseudo-header");
370             this._requestHeadersSection.appendKeyValuePair(":scheme", urlComponents.scheme, "h2-pseudo-header");
371             this._requestHeadersSection.appendKeyValuePair(":authority", WI.h2Authority(urlComponents), "h2-pseudo-header");
372             this._requestHeadersSection.appendKeyValuePair(":path", WI.h2Path(urlComponents), "h2-pseudo-header");
373         }
374
375         let requestHeaders = this._resource.requestHeaders;
376         for (let key in requestHeaders)
377             this._requestHeadersSection.appendKeyValuePair(key, requestHeaders[key], "header");
378
379         if (!detailsElement.firstChild)
380             this._requestHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No request headers"));
381     }
382
383     _refreshResponseHeadersSection()
384     {
385         let detailsElement = this._responseHeadersSection.detailsElement;
386         detailsElement.removeChildren();
387
388         if (!this._resource.hasResponse()) {
389             this._responseHeadersSection.markIncompleteSectionWithLoadingIndicator();
390             return;
391         }
392
393         this._responseHeadersSection.toggleIncomplete(false);
394
395         let protocol = this._resource.protocol || "";
396         if (protocol.startsWith("http/1")) {
397             // HTTP/1.1 response status line:
398             // https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
399             let responseLine = `${protocol.toUpperCase()} ${this._resource.statusCode} ${this._resource.statusText}`;
400             this._responseHeadersSection.appendKeyValuePair(responseLine, null, "h1-status");
401         } else if (protocol === "h2") {
402             // HTTP/2 Response pseudo headers:
403             // https://tools.ietf.org/html/rfc7540#section-8.1.2.4
404             this._responseHeadersSection.appendKeyValuePair(":status", this._resource.statusCode, "h2-pseudo-header");
405         }
406
407         let responseHeaders = this._resource.responseHeaders;
408         for (let key in responseHeaders) {
409             // Split multiple Set-Cookie response headers out into their multiple headers instead of as a combined value.
410             if (key.toLowerCase() === "set-cookie") {
411                 let responseCookies = this._resource.responseCookies;
412                 console.assert(responseCookies.length > 0);
413                 for (let cookie of responseCookies)
414                     this._responseHeadersSection.appendKeyValuePair(key, cookie.header, "header");
415                 continue;
416             }
417
418             this._responseHeadersSection.appendKeyValuePair(key, responseHeaders[key], "header");
419         }
420
421         if (!detailsElement.firstChild)
422             this._responseHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No response headers"));
423     }
424
425     _refreshQueryStringSection()
426     {
427         if (!this._queryStringSection)
428             return;
429
430         let detailsElement = this._queryStringSection.detailsElement;
431         detailsElement.removeChildren();
432
433         let queryString = this._resource.urlComponents.queryString;
434         let queryStringPairs = parseQueryString(queryString, true);
435         for (let {name, value} of queryStringPairs)
436             this._queryStringSection.appendKeyValuePair(name, value);
437     }
438
439     _refreshRequestDataSection()
440     {
441         if (!this._requestDataSection)
442             return;
443
444         let detailsElement = this._requestDataSection.detailsElement;
445         detailsElement.removeChildren();
446
447         let requestData = this._resource.requestData;
448         let requestDataContentType = this._resource.requestDataContentType || "";
449
450         if (requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) {
451             // Simple form data that should be parsable like a query string.
452             this._requestDataSection.appendKeyValuePair(WI.UIString("MIME Type"), requestDataContentType);
453             let queryStringPairs = parseQueryString(requestData, true);
454             for (let {name, value} of queryStringPairs)
455                 this._requestDataSection.appendKeyValuePair(name, value);
456             return;
457         }
458
459         let mimeTypeComponents = parseMIMEType(requestDataContentType);
460         let mimeType = mimeTypeComponents.type;
461         let boundary = mimeTypeComponents.boundary;
462         let encoding = mimeTypeComponents.encoding;
463
464         this._requestDataSection.appendKeyValuePair(WI.UIString("MIME Type"), mimeType);
465         if (boundary)
466             this._requestDataSection.appendKeyValuePair(WI.UIString("Boundary"), boundary);
467         if (encoding)
468             this._requestDataSection.appendKeyValuePair(WI.UIString("Encoding"), encoding);
469
470         let goToButton = detailsElement.appendChild(WI.createGoToArrowButton());
471         goToButton.addEventListener("click", () => { this._delegate.headersContentViewGoToRequestData(this); });
472         this._requestDataSection.appendKeyValuePair(WI.UIString("Request Data"), goToButton);
473     }
474
475     _perfomSearchOnKeyValuePairs()
476     {
477         let searchRegex = WI.SearchUtilities.regExpForString(this._searchQuery, WI.SearchUtilities.defaultSettings);
478
479         let elements = this.element.querySelectorAll(".key, .value");
480         for (let element of elements) {
481             let matchRanges = [];
482             let text = element.textContent;
483             let match;
484             while (match = searchRegex.exec(text))
485                 matchRanges.push({offset: match.index, length: match[0].length});
486
487             if (matchRanges.length) {
488                 let highlightedNodes = WI.highlightRangesWithStyleClass(element, matchRanges, "search-highlight", this._searchDOMChanges);
489                 this._searchResults = this._searchResults.concat(highlightedNodes);
490             }
491         }
492     }
493
494     _revealSearchResult(index, changeFocus)
495     {
496         let highlightElement = this._searchResults[index];
497         if (!highlightElement)
498             return;
499
500         highlightElement.scrollIntoViewIfNeeded();
501
502         if (!this._bouncyHighlightElement) {
503             this._bouncyHighlightElement = document.createElement("div");
504             this._bouncyHighlightElement.className = "bouncy-highlight";
505             this._bouncyHighlightElement.addEventListener("animationend", (event) => {
506                 this._bouncyHighlightElement.remove();
507             });
508         }
509
510         this._bouncyHighlightElement.remove();
511
512         let computedStyles = window.getComputedStyle(highlightElement);
513         let highlightElementRect = highlightElement.getBoundingClientRect();
514         let contentViewRect = this.element.getBoundingClientRect();
515         let contentViewScrollTop = this.element.scrollTop;
516         let contentViewScrollLeft = this.element.scrollLeft;
517
518         this._bouncyHighlightElement.textContent = highlightElement.textContent;
519         this._bouncyHighlightElement.style.top = (highlightElementRect.top - contentViewRect.top + contentViewScrollTop) + "px";
520         this._bouncyHighlightElement.style.left = (highlightElementRect.left - contentViewRect.left + contentViewScrollLeft) + "px";
521         this._bouncyHighlightElement.style.fontWeight = computedStyles.fontWeight;
522
523         this.element.appendChild(this._bouncyHighlightElement);
524     }
525
526     _presentPopoverBelowCallStackElement()
527     {
528         let bounds = WI.Rect.rectFromClientRect(this._popoverCallStackIconElement.getBoundingClientRect());
529         this._popover.present(bounds.pad(2), [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]);
530     }
531
532     _resourceMetricsDidChange(event)
533     {
534         this._needsSummaryRefresh = true;
535         this._needsRequestHeadersRefresh = true;
536         this._needsResponseHeadersRefresh = true;
537         this.needsLayout();
538     }
539
540     _resourceRequestHeadersDidChange(event)
541     {
542         this._needsSummaryRefresh = true;
543         this._needsRedirectHeadersRefresh = true;
544         this._needsRequestHeadersRefresh = true;
545         this.needsLayout();
546     }
547
548     _resourceResponseReceived(event)
549     {
550         this._needsSummaryRefresh = true;
551         this._needsResponseHeadersRefresh = true;
552         this.needsLayout();
553     }
554 };