| /* |
| * Copyright (C) 2017 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.ResourceHeadersContentView = class ResourceHeadersContentView extends WI.ContentView |
| { |
| constructor(resource, delegate) |
| { |
| super(null); |
| |
| console.assert(resource instanceof WI.Resource); |
| console.assert(delegate); |
| |
| this._resource = resource; |
| this._resource.addEventListener(WI.Resource.Event.MetricsDidChange, this._resourceMetricsDidChange, this); |
| this._resource.addEventListener(WI.Resource.Event.RequestHeadersDidChange, this._resourceRequestHeadersDidChange, this); |
| this._resource.addEventListener(WI.Resource.Event.ResponseReceived, this._resourceResponseReceived, this); |
| |
| this._delegate = delegate; |
| |
| this._searchQuery = null; |
| this._searchResults = null; |
| this._searchDOMChanges = []; |
| this._searchIndex = -1; |
| this._automaticallyRevealFirstSearchResult = false; |
| this._bouncyHighlightElement = null; |
| this._popover = null; |
| this._popoverCallStackIconElement = null; |
| |
| this._redirectDetailsSections = []; |
| |
| this.element.classList.add("resource-details", "resource-headers"); |
| this.element.tabIndex = 0; |
| |
| this._needsSummaryRefresh = false; |
| this._needsRedirectHeadersRefresh = false; |
| this._needsRequestHeadersRefresh = false; |
| this._needsResponseHeadersRefresh = false; |
| } |
| |
| // Protected |
| |
| initialLayout() |
| { |
| super.initialLayout(); |
| |
| this._summarySection = new WI.ResourceDetailsSection(WI.UIString("Summary"), "summary"); |
| this.element.appendChild(this._summarySection.element); |
| this._refreshSummarySection(); |
| |
| this._refreshRedirectHeadersSections(); |
| |
| this._requestHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Request"), "headers"); |
| this.element.appendChild(this._requestHeadersSection.element); |
| this._refreshRequestHeadersSection(); |
| |
| this._responseHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Response"), "headers"); |
| this.element.appendChild(this._responseHeadersSection.element); |
| this._refreshResponseHeadersSection(); |
| |
| if (this._resource.urlComponents.queryString) { |
| this._queryStringSection = new WI.ResourceDetailsSection(WI.UIString("Query String Parameters")); |
| this.element.appendChild(this._queryStringSection.element); |
| this._refreshQueryStringSection(); |
| } |
| |
| if (this._resource.requestData) { |
| this._requestDataSection = new WI.ResourceDetailsSection(WI.UIString("Request Data")); |
| this.element.appendChild(this._requestDataSection.element); |
| this._refreshRequestDataSection(); |
| } |
| |
| this._needsSummaryRefresh = false; |
| this._needsRedirectHeadersRefresh = false; |
| this._needsRequestHeadersRefresh = false; |
| this._needsResponseHeadersRefresh = false; |
| } |
| |
| layout() |
| { |
| super.layout(); |
| |
| if (this._needsSummaryRefresh) { |
| this._refreshSummarySection(); |
| this._needsSummaryRefresh = false; |
| } |
| |
| if (this._needsRedirectHeadersRefresh) { |
| this._refreshRedirectHeadersSections(); |
| this._needsRedirectHeadersRefresh = false; |
| } |
| |
| if (this._needsRequestHeadersRefresh) { |
| this._refreshRequestHeadersSection(); |
| this._needsRequestHeadersRefresh = false; |
| } |
| |
| if (this._needsResponseHeadersRefresh) { |
| this._refreshResponseHeadersSection(); |
| this._needsResponseHeadersRefresh = false; |
| } |
| } |
| |
| detached() |
| { |
| if (this._popover) |
| this._popover.dismiss(); |
| |
| super.detached(); |
| } |
| |
| closed() |
| { |
| this._resource.removeEventListener(WI.Resource.Event.MetricsDidChange, this._resourceMetricsDidChange, this); |
| this._resource.removeEventListener(WI.Resource.Event.RequestHeadersDidChange, this._resourceRequestHeadersDidChange, this); |
| this._resource.removeEventListener(WI.Resource.Event.ResponseReceived, this._resourceResponseReceived, this); |
| |
| super.closed(); |
| } |
| |
| get supportsSearch() |
| { |
| return true; |
| } |
| |
| get numberOfSearchResults() |
| { |
| return this._searchResults ? this._searchResults.length : null; |
| } |
| |
| get hasPerformedSearch() |
| { |
| return this._searchResults !== null; |
| } |
| |
| set automaticallyRevealFirstSearchResult(reveal) |
| { |
| this._automaticallyRevealFirstSearchResult = reveal; |
| |
| // If we haven't shown a search result yet, reveal one now. |
| if (this._automaticallyRevealFirstSearchResult && this.numberOfSearchResults > 0) { |
| if (this._searchIndex === -1) |
| this.revealNextSearchResult(); |
| } |
| } |
| |
| performSearch(query) |
| { |
| if (query === this._searchQuery) |
| return; |
| |
| WI.revertDOMChanges(this._searchDOMChanges); |
| |
| this._searchQuery = query; |
| this._searchResults = []; |
| this._searchDOMChanges = []; |
| this._searchIndex = -1; |
| |
| this._perfomSearchOnKeyValuePairs(); |
| |
| this.dispatchEventToListeners(WI.ContentView.Event.NumberOfSearchResultsDidChange); |
| |
| if (this._automaticallyRevealFirstSearchResult && this._searchResults.length > 0) |
| this.revealNextSearchResult(); |
| } |
| |
| searchCleared() |
| { |
| WI.revertDOMChanges(this._searchDOMChanges); |
| |
| this._searchQuery = null; |
| this._searchResults = null; |
| this._searchDOMChanges = []; |
| this._searchIndex = -1; |
| } |
| |
| revealPreviousSearchResult(changeFocus) |
| { |
| if (!this.numberOfSearchResults) |
| return; |
| |
| if (this._searchIndex > 0) |
| --this._searchIndex; |
| else |
| this._searchIndex = this._searchResults.length - 1; |
| |
| this._revealSearchResult(this._searchIndex, changeFocus); |
| } |
| |
| revealNextSearchResult(changeFocus) |
| { |
| if (!this.numberOfSearchResults) |
| return; |
| |
| if (this._searchIndex + 1 < this._searchResults.length) |
| ++this._searchIndex; |
| else |
| this._searchIndex = 0; |
| |
| this._revealSearchResult(this._searchIndex, changeFocus); |
| } |
| |
| // Private |
| |
| _responseSourceDisplayString(responseSource) |
| { |
| switch (responseSource) { |
| case WI.Resource.ResponseSource.Network: |
| return WI.UIString("Network"); |
| case WI.Resource.ResponseSource.MemoryCache: |
| return WI.UIString("Memory Cache"); |
| case WI.Resource.ResponseSource.DiskCache: |
| return WI.UIString("Disk Cache"); |
| case WI.Resource.ResponseSource.ServiceWorker: |
| return WI.UIString("Service Worker"); |
| case WI.Resource.ResponseSource.InspectorOverride: |
| return WI.UIString("Local Override"); |
| case WI.Resource.ResponseSource.Unknown: |
| default: |
| return null; |
| } |
| } |
| |
| _refreshSummarySection() |
| { |
| let detailsElement = this._summarySection.detailsElement; |
| detailsElement.removeChildren(); |
| |
| this._summarySection.toggleError(this._resource.hadLoadingError()); |
| |
| for (let redirect of this._resource.redirects) |
| this._summarySection.appendKeyValuePair(WI.UIString("URL"), redirect.url.insertWordBreakCharacters(), "url"); |
| this._summarySection.appendKeyValuePair(WI.UIString("URL"), this._resource.displayURL.insertWordBreakCharacters(), "url"); |
| |
| let status = emDash; |
| if (!isNaN(this._resource.statusCode)) |
| status = this._resource.statusCode + (this._resource.statusText ? " " + this._resource.statusText : ""); |
| this._summarySection.appendKeyValuePair(WI.UIString("Status"), status); |
| |
| // FIXME: <https://webkit.org/b/178827> Web Inspector: Should be able to link directly to the ServiceWorker that handled a particular load |
| |
| let source = this._responseSourceDisplayString(this._resource.responseSource) || emDash; |
| this._summarySection.appendKeyValuePair(WI.UIString("Source"), source); |
| |
| if (this._resource.remoteAddress) |
| this._summarySection.appendKeyValuePair(WI.UIString("Address"), this._resource.remoteAddress); |
| |
| let initiatorLocation = this._resource.initiatorSourceCodeLocation; |
| if (initiatorLocation) { |
| |
| let fragment = document.createDocumentFragment(); |
| |
| const options = { |
| dontFloat: true, |
| ignoreSearchTab: true, |
| ignoreNetworkTab: true, |
| }; |
| let link = WI.createSourceCodeLocationLink(initiatorLocation, options); |
| fragment.appendChild(link); |
| |
| let callFrames = this._resource.initiatorCallFrames; |
| if (callFrames) { |
| this._popoverCallStackIconElement = document.createElement("img"); |
| this._popoverCallStackIconElement.className = "call-stack"; |
| fragment.appendChild(this._popoverCallStackIconElement); |
| |
| this._popoverCallStackIconElement.addEventListener("click", (event) => { |
| if (!this._popover) { |
| this._popover = new WI.Popover(this); |
| this._popover.windowResizeHandler = () => { this._presentPopoverBelowCallStackElement(); }; |
| } |
| |
| const selectable = false; |
| let callFramesTreeOutline = new WI.TreeOutline(selectable); |
| callFramesTreeOutline.disclosureButtons = false; |
| let callFrameTreeController = new WI.CallFrameTreeController(callFramesTreeOutline); |
| callFrameTreeController.callFrames = callFrames; |
| |
| let popoverContent = document.createElement("div"); |
| popoverContent.appendChild(callFrameTreeController.treeOutline.element); |
| this._popover.content = popoverContent; |
| |
| this._presentPopoverBelowCallStackElement(); |
| }); |
| } |
| |
| let pair = this._summarySection.appendKeyValuePair(WI.UIString("Initiator"), fragment); |
| pair.classList.add("initiator"); |
| |
| if (this._popover && this._popover.visible) |
| this._presentPopoverBelowCallStackElement(); |
| } |
| } |
| |
| _refreshRedirectHeadersSections() |
| { |
| let referenceElement = this._redirectDetailsSections.length ? this._redirectDetailsSections.lastValue.element : this._summarySection.element; |
| |
| for (let i = this._redirectDetailsSections.length; i < this._resource.redirects.length; ++i) { |
| let redirect = this._resource.redirects[i]; |
| |
| let redirectRequestSection = new WI.ResourceDetailsSection(WI.UIString("Request"), "redirect"); |
| |
| // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests |
| redirectRequestSection.appendKeyValuePair(`${redirect.requestMethod} ${redirect.urlComponents.path}`, null, "h1-status"); |
| |
| for (let key in redirect.requestHeaders) |
| redirectRequestSection.appendKeyValuePair(key, redirect.requestHeaders[key], "header"); |
| |
| referenceElement = this.element.insertBefore(redirectRequestSection.element, referenceElement.nextElementSibling); |
| this._redirectDetailsSections.push(redirectRequestSection); |
| |
| let redirectResponseSection = new WI.ResourceDetailsSection(WI.UIString("Redirect Response"), "redirect"); |
| |
| // FIXME: <https://webkit.org/b/190214> Web Inspector: expose full load metrics for redirect requests |
| redirectResponseSection.appendKeyValuePair(`${redirect.responseStatusCode} ${redirect.responseStatusText}`, null, "h1-status"); |
| |
| for (let key in redirect.responseHeaders) |
| redirectResponseSection.appendKeyValuePair(key, redirect.responseHeaders[key], "header"); |
| |
| referenceElement = this.element.insertBefore(redirectResponseSection.element, referenceElement.nextElementSibling); |
| this._redirectDetailsSections.push(redirectResponseSection); |
| } |
| } |
| |
| _refreshRequestHeadersSection() |
| { |
| let detailsElement = this._requestHeadersSection.detailsElement; |
| detailsElement.removeChildren(); |
| |
| // A revalidation request still sends a request even though we served from cache, so show the request. |
| if (this._resource.statusCode !== 304) { |
| if (this._resource.responseSource === WI.Resource.ResponseSource.MemoryCache) { |
| this._requestHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No request, served from the memory cache.")); |
| return; |
| } |
| if (this._resource.responseSource === WI.Resource.ResponseSource.DiskCache) { |
| this._requestHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No request, served from the disk cache.")); |
| return; |
| } |
| } |
| |
| let protocol = this._resource.protocol || ""; |
| let urlComponents = this._resource.urlComponents; |
| if (protocol.startsWith("http/1")) { |
| // HTTP/1.1 request line: |
| // https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1 |
| let requestLine = `${this._resource.requestMethod} ${urlComponents.path} ${protocol.toUpperCase()}`; |
| this._requestHeadersSection.appendKeyValuePair(requestLine, null, "h1-status"); |
| } else if (protocol === "h2") { |
| // HTTP/2 Request pseudo headers: |
| // https://tools.ietf.org/html/rfc7540#section-8.1.2.3 |
| this._requestHeadersSection.appendKeyValuePair(":method", this._resource.requestMethod, "h2-pseudo-header"); |
| this._requestHeadersSection.appendKeyValuePair(":scheme", urlComponents.scheme, "h2-pseudo-header"); |
| this._requestHeadersSection.appendKeyValuePair(":authority", WI.h2Authority(urlComponents), "h2-pseudo-header"); |
| this._requestHeadersSection.appendKeyValuePair(":path", WI.h2Path(urlComponents), "h2-pseudo-header"); |
| } |
| |
| let requestHeaders = this._resource.requestHeaders; |
| for (let key in requestHeaders) |
| this._requestHeadersSection.appendKeyValuePair(key, requestHeaders[key], "header"); |
| |
| if (!detailsElement.firstChild) |
| this._requestHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No request headers")); |
| } |
| |
| _refreshResponseHeadersSection() |
| { |
| let detailsElement = this._responseHeadersSection.detailsElement; |
| detailsElement.removeChildren(); |
| |
| if (!this._resource.hasResponse()) { |
| this._responseHeadersSection.markIncompleteSectionWithLoadingIndicator(); |
| return; |
| } |
| |
| this._responseHeadersSection.toggleIncomplete(false); |
| |
| let protocol = this._resource.protocol || ""; |
| if (protocol.startsWith("http/1")) { |
| // HTTP/1.1 response status line: |
| // https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1 |
| let responseLine = `${protocol.toUpperCase()} ${this._resource.statusCode} ${this._resource.statusText}`; |
| this._responseHeadersSection.appendKeyValuePair(responseLine, null, "h1-status"); |
| } else if (protocol === "h2") { |
| // HTTP/2 Response pseudo headers: |
| // https://tools.ietf.org/html/rfc7540#section-8.1.2.4 |
| this._responseHeadersSection.appendKeyValuePair(":status", this._resource.statusCode, "h2-pseudo-header"); |
| } |
| |
| let responseHeaders = this._resource.responseHeaders; |
| for (let key in responseHeaders) { |
| // Split multiple Set-Cookie response headers out into their multiple headers instead of as a combined value. |
| if (key.toLowerCase() === "set-cookie") { |
| let responseCookies = this._resource.responseCookies; |
| console.assert(responseCookies.length > 0); |
| for (let cookie of responseCookies) |
| this._responseHeadersSection.appendKeyValuePair(key, cookie.header, "header"); |
| continue; |
| } |
| |
| this._responseHeadersSection.appendKeyValuePair(key, responseHeaders[key], "header"); |
| } |
| |
| if (!detailsElement.firstChild) |
| this._responseHeadersSection.markIncompleteSectionWithMessage(WI.UIString("No response headers")); |
| } |
| |
| _refreshQueryStringSection() |
| { |
| if (!this._queryStringSection) |
| return; |
| |
| let detailsElement = this._queryStringSection.detailsElement; |
| detailsElement.removeChildren(); |
| |
| let queryString = this._resource.urlComponents.queryString; |
| let queryStringPairs = parseQueryString(queryString, true); |
| for (let {name, value} of queryStringPairs) |
| this._queryStringSection.appendKeyValuePair(name, value); |
| } |
| |
| _refreshRequestDataSection() |
| { |
| if (!this._requestDataSection) |
| return; |
| |
| let detailsElement = this._requestDataSection.detailsElement; |
| detailsElement.removeChildren(); |
| |
| let requestData = this._resource.requestData; |
| let requestDataContentType = this._resource.requestDataContentType || ""; |
| |
| if (requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i)) { |
| // Simple form data that should be parsable like a query string. |
| this._requestDataSection.appendKeyValuePair(WI.UIString("MIME Type"), requestDataContentType); |
| let queryStringPairs = parseQueryString(requestData, true); |
| for (let {name, value} of queryStringPairs) |
| this._requestDataSection.appendKeyValuePair(name, value); |
| return; |
| } |
| |
| let mimeTypeComponents = parseMIMEType(requestDataContentType); |
| let mimeType = mimeTypeComponents.type; |
| let boundary = mimeTypeComponents.boundary; |
| let encoding = mimeTypeComponents.encoding; |
| |
| this._requestDataSection.appendKeyValuePair(WI.UIString("MIME Type"), mimeType); |
| if (boundary) |
| this._requestDataSection.appendKeyValuePair(WI.UIString("Boundary"), boundary); |
| if (encoding) |
| this._requestDataSection.appendKeyValuePair(WI.UIString("Encoding"), encoding); |
| |
| let goToButton = detailsElement.appendChild(WI.createGoToArrowButton()); |
| goToButton.addEventListener("click", () => { this._delegate.headersContentViewGoToRequestData(this); }); |
| this._requestDataSection.appendKeyValuePair(WI.UIString("Request Data"), goToButton); |
| } |
| |
| _perfomSearchOnKeyValuePairs() |
| { |
| let searchRegex = WI.SearchUtilities.searchRegExpForString(this._searchQuery, WI.SearchUtilities.defaultSettings); |
| if (!searchRegex) { |
| this.searchCleared(); |
| this.dispatchEventToListeners(WI.TextEditor.Event.NumberOfSearchResultsDidChange); |
| return; |
| } |
| |
| let elements = this.element.querySelectorAll(".key, .value"); |
| for (let element of elements) { |
| let matchRanges = []; |
| let text = element.textContent; |
| let match; |
| while (match = searchRegex.exec(text)) |
| matchRanges.push({offset: match.index, length: match[0].length}); |
| |
| if (matchRanges.length) { |
| let highlightedNodes = WI.highlightRangesWithStyleClass(element, matchRanges, "search-highlight", this._searchDOMChanges); |
| this._searchResults.pushAll(highlightedNodes); |
| } |
| } |
| } |
| |
| _revealSearchResult(index, changeFocus) |
| { |
| let highlightElement = this._searchResults[index]; |
| if (!highlightElement) |
| return; |
| |
| highlightElement.scrollIntoViewIfNeeded(); |
| |
| if (!this._bouncyHighlightElement) { |
| this._bouncyHighlightElement = document.createElement("div"); |
| this._bouncyHighlightElement.className = "bouncy-highlight"; |
| this._bouncyHighlightElement.addEventListener("animationend", (event) => { |
| this._bouncyHighlightElement.remove(); |
| }); |
| } |
| |
| this._bouncyHighlightElement.remove(); |
| |
| let computedStyles = window.getComputedStyle(highlightElement); |
| let highlightElementRect = highlightElement.getBoundingClientRect(); |
| let contentViewRect = this.element.getBoundingClientRect(); |
| let contentViewScrollTop = this.element.scrollTop; |
| let contentViewScrollLeft = this.element.scrollLeft; |
| |
| this._bouncyHighlightElement.textContent = highlightElement.textContent; |
| this._bouncyHighlightElement.style.top = (highlightElementRect.top - contentViewRect.top + contentViewScrollTop) + "px"; |
| this._bouncyHighlightElement.style.left = (highlightElementRect.left - contentViewRect.left + contentViewScrollLeft) + "px"; |
| this._bouncyHighlightElement.style.fontWeight = computedStyles.fontWeight; |
| |
| this.element.appendChild(this._bouncyHighlightElement); |
| } |
| |
| _presentPopoverBelowCallStackElement() |
| { |
| let bounds = WI.Rect.rectFromClientRect(this._popoverCallStackIconElement.getBoundingClientRect()); |
| this._popover.present(bounds.pad(2), [WI.RectEdge.MAX_Y, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_X]); |
| } |
| |
| _resourceMetricsDidChange(event) |
| { |
| this._needsSummaryRefresh = true; |
| this._needsRequestHeadersRefresh = true; |
| this._needsResponseHeadersRefresh = true; |
| this.needsLayout(); |
| } |
| |
| _resourceRequestHeadersDidChange(event) |
| { |
| this._needsSummaryRefresh = true; |
| this._needsRedirectHeadersRefresh = true; |
| this._needsRequestHeadersRefresh = true; |
| this.needsLayout(); |
| } |
| |
| _resourceResponseReceived(event) |
| { |
| this._needsSummaryRefresh = true; |
| this._needsResponseHeadersRefresh = true; |
| this.needsLayout(); |
| } |
| }; |