| /* |
| * Copyright (C) 2018 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.ResourceSecurityContentView = class ResourceSecurityContentView extends WI.ContentView |
| { |
| constructor(resource) |
| { |
| console.assert(resource instanceof WI.Resource); |
| |
| super(); |
| |
| this._resource = resource; |
| |
| this._insecureMessageElement = null; |
| this._needsConnectionRefresh = true; |
| this._needsCertificateRefresh = true; |
| |
| this._searchQuery = null; |
| this._searchResults = null; |
| this._searchDOMChanges = []; |
| this._searchIndex = -1; |
| this._automaticallyRevealFirstSearchResult = false; |
| this._bouncyHighlightElement = null; |
| |
| this.element.classList.add("resource-details", "resource-security"); |
| } |
| |
| // Protected |
| |
| initialLayout() |
| { |
| super.initialLayout(); |
| |
| this._connectionSection = new WI.ResourceDetailsSection(WI.UIString("Connection"), "connection"); |
| this.element.appendChild(this._connectionSection.element); |
| |
| this._certificateSection = new WI.ResourceDetailsSection(WI.UIString("Certificate"), "certificate"); |
| this.element.appendChild(this._certificateSection.element); |
| |
| this._resource.addEventListener(WI.Resource.Event.ResponseReceived, this._handleResourceResponseReceived, this); |
| this._resource.addEventListener(WI.Resource.Event.MetricsDidChange, this._handleResourceMetricsDidChange, this); |
| } |
| |
| layout() |
| { |
| super.layout(); |
| |
| if (!this._resource.loadedSecurely) { |
| if (!this._insecureMessageElement) |
| this._insecureMessageElement = WI.createMessageTextView(WI.UIString("The resource was requested insecurely."), true); |
| this.element.appendChild(this._insecureMessageElement); |
| return; |
| } |
| |
| if (this._needsConnectionRefresh) { |
| this._needsConnectionRefresh = false; |
| this._refreshConnectionSection(); |
| } |
| |
| if (this._needsCertificateRefresh) { |
| this._needsCertificateRefresh = false; |
| this._refreshCetificateSection(); |
| } |
| } |
| |
| closed() |
| { |
| if (this.didInitialLayout) { |
| this._resource.removeEventListener(WI.Resource.Event.ResponseReceived, this._handleResourceResponseReceived, this); |
| this._resource.removeEventListener(WI.Resource.Event.MetricsDidChange, this._handleResourceMetricsDidChange, 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 < this._searchResults.length - 1) |
| ++this._searchIndex; |
| else |
| this._searchIndex = 0; |
| |
| this._revealSearchResult(this._searchIndex, changeFocus); |
| } |
| |
| // Private |
| |
| _refreshConnectionSection() |
| { |
| let detailsElement = this._connectionSection.detailsElement; |
| detailsElement.removeChildren(); |
| |
| let security = this._resource.security; |
| if (isEmptyObject(security)) { |
| this._connectionSection.markIncompleteSectionWithMessage(WI.UIString("No connection security information.")); |
| return; |
| } |
| |
| let connection = security.connection; |
| if (isEmptyObject(connection) || Object.values(connection).every((value) => !value)) { |
| this._connectionSection.markIncompleteSectionWithMessage(WI.UIString("No connection security information.")); |
| return; |
| } |
| |
| this._connectionSection.appendKeyValuePair(WI.UIString("Protocol"), connection.protocol || emDash); |
| this._connectionSection.appendKeyValuePair(WI.UIString("Cipher"), connection.cipher || emDash); |
| } |
| |
| _refreshCetificateSection() |
| { |
| let detailsElement = this._certificateSection.detailsElement; |
| detailsElement.removeChildren(); |
| |
| let security = this._resource.security; |
| if (isEmptyObject(security)) { |
| this._certificateSection.markIncompleteSectionWithMessage(WI.UIString("No certificate security information.")); |
| return; |
| } |
| |
| let certificate = security.certificate; |
| if (isEmptyObject(certificate) || Object.values(certificate).every((value) => !value)) { |
| this._certificateSection.markIncompleteSectionWithMessage(WI.UIString("No certificate security information.")); |
| return; |
| } |
| |
| if (WI.NetworkManager.supportsShowCertificate()) { |
| let button = document.createElement("button"); |
| button.textContent = WI.UIString("Show full certificate"); |
| |
| let errorElement = null; |
| button.addEventListener("click", (event) => { |
| this._resource.showCertificate() |
| .then(() => { |
| if (errorElement) { |
| errorElement.remove(); |
| errorElement = null; |
| } |
| }) |
| .catch((error) => { |
| if (!errorElement) |
| errorElement = WI.ImageUtilities.useSVGSymbol("Images/Error.svg", "error", error); |
| button.insertAdjacentElement("afterend", errorElement); |
| }); |
| }); |
| |
| let pairElement = this._certificateSection.appendKeyValuePair(button); |
| pairElement.classList.add("show-certificate"); |
| } |
| |
| this._certificateSection.appendKeyValuePair(WI.UIString("Subject"), certificate.subject || emDash); |
| |
| let appendFormattedDate = (key, timestamp) => { |
| if (isNaN(timestamp)) |
| return; |
| |
| let date = new Date(timestamp * 1000); |
| |
| let timeElement = document.createElement("time"); |
| timeElement.datetime = date.toISOString(); |
| timeElement.textContent = date.toLocaleString(); |
| this._certificateSection.appendKeyValuePair(key, timeElement); |
| |
| }; |
| appendFormattedDate(WI.UIString("Valid From"), certificate.validFrom); |
| appendFormattedDate(WI.UIString("Valid Until"), certificate.validUntil); |
| |
| let appendList = (key, values, className) => { |
| if (!Array.isArray(values)) |
| return; |
| |
| const initialCount = 5; |
| for (let i = 0; i < Math.min(values.length, initialCount); ++i) |
| this._certificateSection.appendKeyValuePair(key, values[i], className); |
| |
| let remaining = values.length - initialCount; |
| if (remaining <= 0) |
| return; |
| |
| let showMoreElement = document.createElement("a"); |
| showMoreElement.classList.add("show-more"); |
| showMoreElement.textContent = WI.UIString("Show %d More").format(remaining); |
| |
| let showMorePair = this._certificateSection.appendKeyValuePair(key, showMoreElement, className); |
| |
| showMoreElement.addEventListener("click", (event) => { |
| showMorePair.remove(); |
| |
| for (let i = initialCount; i < values.length; ++i) |
| this._certificateSection.appendKeyValuePair(key, values[i], className); |
| }, {once: true}); |
| }; |
| appendList(WI.UIString("DNS"), certificate.dnsNames, "dns-name"); |
| appendList(WI.UIString("IP"), certificate.ipAddresses, "ip-address"); |
| } |
| |
| _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); |
| } |
| |
| _handleResourceResponseReceived(event) |
| { |
| this._needsCertificateRefresh = true; |
| this.needsLayout(); |
| } |
| |
| _handleResourceMetricsDidChange(event) |
| { |
| this._needsConnectionRefresh = true; |
| this.needsLayout(); |
| } |
| }; |