| /* |
| * 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.element.classList.add("resource-details", "resource-headers"); |
| this.element.tabIndex = 0; |
| |
| this._needsSummaryRefresh = 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._requestHeadersSection = new WI.ResourceDetailsSection(WI.UIString("Request"), "headers"); |
| this.element.appendChild(this._requestHeadersSection.element); |
| this._refreshRequestHeadersSection(); |
| |
| // FIXME: <https://webkit.org/b/150005> Web Inspector: Redirect requests are not shown in either Network or Timeline tabs |
| |
| 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._needsRequestHeadersRefresh = false; |
| this._needsResponseHeadersRefresh = false; |
| } |
| |
| layout() |
| { |
| super.layout(); |
| |
| if (this._needsSummaryRefresh) { |
| this._refreshSummarySection(); |
| this._needsSummaryRefresh = false; |
| } |
| |
| if (this._needsRequestHeadersRefresh) { |
| this._refreshRequestHeadersSection(); |
| this._needsRequestHeadersRefresh = false; |
| } |
| |
| if (this._needsResponseHeadersRefresh) { |
| this._refreshResponseHeadersSection(); |
| this._needsResponseHeadersRefresh = false; |
| } |
| } |
| |
| closed() |
| { |
| this._resource.removeEventListener(null, null, 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 |
| |
| _markIncompleteSectionWithMessage(section, message) |
| { |
| section.toggleIncomplete(true); |
| |
| let p = section.detailsElement.appendChild(document.createElement("p")); |
| p.textContent = message; |
| } |
| |
| _markIncompleteSectionWithLoadingIndicator(section) |
| { |
| section.toggleIncomplete(true); |
| |
| let p = section.detailsElement.appendChild(document.createElement("p")); |
| let spinner = new WI.IndeterminateProgressSpinner; |
| p.appendChild(spinner.element); |
| } |
| |
| _appendKeyValuePair(parentElement, key, value, className) |
| { |
| let p = parentElement.appendChild(document.createElement("p")); |
| p.className = "pair"; |
| if (className) |
| p.classList.add(className); |
| |
| // Don't include a colon if no value. |
| console.assert(typeof key === "string"); |
| let displayKey = key + (value ? ": " : ""); |
| |
| let keyElement = p.appendChild(document.createElement("span")); |
| keyElement.className = "key"; |
| keyElement.textContent = displayKey; |
| |
| let valueElement = p.appendChild(document.createElement("span")); |
| valueElement.className = "value"; |
| if (value instanceof Node) |
| valueElement.appendChild(value); |
| else |
| valueElement.textContent = value; |
| } |
| |
| _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.Unknown: |
| default: |
| return null; |
| } |
| } |
| |
| _refreshSummarySection() |
| { |
| let detailsElement = this._summarySection.detailsElement; |
| detailsElement.removeChildren(); |
| |
| this._summarySection.toggleError(this._resource.hadLoadingError()); |
| |
| this._appendKeyValuePair(detailsElement, WI.UIString("URL"), this._resource.url.insertWordBreakCharacters()); |
| |
| let status = emDash; |
| if (!isNaN(this._resource.statusCode)) |
| status = this._resource.statusCode + (this._resource.statusText ? " " + this._resource.statusText : ""); |
| this._appendKeyValuePair(detailsElement, 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._appendKeyValuePair(detailsElement, WI.UIString("Source"), source); |
| |
| if (this._resource.remoteAddress) |
| this._appendKeyValuePair(detailsElement, WI.UIString("Address"), this._resource.remoteAddress); |
| } |
| |
| _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._markIncompleteSectionWithMessage(this._requestHeadersSection, WI.UIString("No request, served from the memory cache.")); |
| return; |
| } |
| if (this._resource.responseSource === WI.Resource.ResponseSource.DiskCache) { |
| this._markIncompleteSectionWithMessage(this._requestHeadersSection, 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._appendKeyValuePair(detailsElement, 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._appendKeyValuePair(detailsElement, ":method", this._resource.requestMethod, "h2-pseudo-header"); |
| this._appendKeyValuePair(detailsElement, ":scheme", urlComponents.scheme, "h2-pseudo-header"); |
| this._appendKeyValuePair(detailsElement, ":authority", WI.h2Authority(urlComponents), "h2-pseudo-header"); |
| this._appendKeyValuePair(detailsElement, ":path", WI.h2Path(urlComponents), "h2-pseudo-header"); |
| } |
| |
| let requestHeaders = this._resource.requestHeaders; |
| for (let key in requestHeaders) |
| this._appendKeyValuePair(detailsElement, key, requestHeaders[key], "header"); |
| |
| if (!detailsElement.firstChild) |
| this._markIncompleteSectionWithMessage(this._requestHeadersSection, WI.UIString("No request headers")); |
| } |
| |
| _refreshResponseHeadersSection() |
| { |
| let detailsElement = this._responseHeadersSection.detailsElement; |
| detailsElement.removeChildren(); |
| |
| if (!this._resource.hasResponse()) { |
| this._markIncompleteSectionWithLoadingIndicator(this._responseHeadersSection); |
| 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._appendKeyValuePair(detailsElement, 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._appendKeyValuePair(detailsElement, ":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._appendKeyValuePair(detailsElement, key, cookie.rawHeader, "header"); |
| continue; |
| } |
| |
| this._appendKeyValuePair(detailsElement, key, responseHeaders[key], "header"); |
| } |
| |
| if (!detailsElement.firstChild) |
| this._markIncompleteSectionWithMessage(this._responseHeadersSection, 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._appendKeyValuePair(detailsElement, 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._appendKeyValuePair(detailsElement, WI.UIString("MIME Type"), requestDataContentType); |
| let queryStringPairs = parseQueryString(requestData, true); |
| for (let {name, value} of queryStringPairs) |
| this._appendKeyValuePair(detailsElement, name, value); |
| return; |
| } |
| |
| let mimeTypeComponents = parseMIMEType(requestDataContentType); |
| let mimeType = mimeTypeComponents.type; |
| let boundary = mimeTypeComponents.boundary; |
| let encoding = mimeTypeComponents.encoding; |
| |
| this._appendKeyValuePair(detailsElement, WI.UIString("MIME Type"), mimeType); |
| if (boundary) |
| this._appendKeyValuePair(detailsElement, WI.UIString("Boundary"), boundary); |
| if (encoding) |
| this._appendKeyValuePair(detailsElement, WI.UIString("Encoding"), encoding); |
| |
| let goToButton = detailsElement.appendChild(WI.createGoToArrowButton()); |
| goToButton.addEventListener("click", () => { this._delegate.headersContentViewGoToRequestData(this); }); |
| this._appendKeyValuePair(detailsElement, WI.UIString("Request Data"), goToButton); |
| } |
| |
| _perfomSearchOnKeyValuePairs() |
| { |
| let searchRegex = new RegExp(this._searchQuery.escapeForRegExp(), "gi"); |
| |
| 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 = this._searchResults.concat(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); |
| } |
| |
| _resourceMetricsDidChange(event) |
| { |
| this._needsSummaryRefresh = true; |
| this._needsRequestHeadersRefresh = true; |
| this._needsResponseHeadersRefresh = true; |
| this.needsLayout(); |
| } |
| |
| _resourceRequestHeadersDidChange(event) |
| { |
| this._needsRequestHeadersRefresh = true; |
| this.needsLayout(); |
| } |
| |
| _resourceResponseReceived(event) |
| { |
| this._needsSummaryRefresh = true; |
| this._needsResponseHeadersRefresh = true; |
| this.needsLayout(); |
| } |
| }; |