/* * Copyright (C) 2013-2019 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.ResourceContentView = class ResourceContentView extends WI.ContentView { constructor(resource, styleClassName) { console.assert(resource instanceof WI.Resource || resource instanceof WI.CSSStyleSheet, resource); console.assert(typeof styleClassName === "string"); super(resource); this._resource = resource; this.element.classList.add(styleClassName, "resource"); this._spinnerTimeout = setTimeout(() => { if (!this._hasContent()) { // Append a spinner while waiting for contentAvailable. Subclasses are responsible for // removing the spinner before showing the resource content by calling removeLoadingIndicator. let spinner = new WI.IndeterminateProgressSpinner; this.element.appendChild(spinner.element); } this._spinnerTimeout = undefined; }, 100); this.element.addEventListener("click", this._mouseWasClicked.bind(this), false); // Request content last so the spinner will always be removed in case the content is immediately available. resource.requestContent().then(this._contentAvailable.bind(this)).catch(this.showGenericErrorMessage.bind(this)); if (!this.managesOwnIssues) { WI.consoleManager.addEventListener(WI.ConsoleManager.Event.IssueAdded, this._issueWasAdded, this); var issues = WI.consoleManager.issuesForSourceCode(resource); for (var i = 0; i < issues.length; ++i) this.addIssue(issues[i]); } if (WI.NetworkManager.supportsOverridingResponses()) { if (resource.localResourceOverride) { this._localResourceOverrideBannerView = new WI.LocalResourceOverrideLabelView(resource.localResourceOverride); this._importLocalResourceOverrideButtonNavigationItem = new WI.ButtonNavigationItem("import-local-resource-override", WI.UIString("Import"), "Images/Import.svg", 15, 15); this._importLocalResourceOverrideButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText; this._importLocalResourceOverrideButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; this._importLocalResourceOverrideButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleImportLocalResourceOverride, this); if (resource.localResourceOverride.canMapToFile) { this._mapLocalResourceOverrideToFileButtonNavigationItem = new WI.ButtonNavigationItem("map-local-resource-override", WI.UIString("Map to File"), "Images/Disk.svg", 15, 15); this._mapLocalResourceOverrideToFileButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText; this._mapLocalResourceOverrideToFileButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; this._mapLocalResourceOverrideToFileButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleMapLocalResourceOverrideToFile, this); resource.addEventListener(WI.LocalResource.Event.MappedFilePathChanged, this._handleMappedFilePathChanged, this); } this._removeLocalResourceOverrideButtonNavigationItem = new WI.ButtonNavigationItem("remove-local-resource-override", WI.UIString("Delete Local Override"), "Images/NavigationItemTrash.svg", 15, 15); this._removeLocalResourceOverrideButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleRemoveLocalResourceOverride, this); this._removeLocalResourceOverrideButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; } else { this._localResourceOverrideBannerView = new WI.LocalResourceOverrideWarningView(resource); this._createLocalResourceOverrideButtonNavigationItem = new WI.ButtonNavigationItem("create-local-resource-override", this.createLocalResourceOverrideTooltip, "Images/NavigationItemNetworkOverride.svg", 13, 14); this._createLocalResourceOverrideButtonNavigationItem.enabled = false; // Enabled when the content is available. this._createLocalResourceOverrideButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.Low; if (WI.NetworkManager.supportsOverridingRequests() || WI.NetworkManager.supportsBlockingRequests()) WI.addMouseDownContextMenuHandlers(this._createLocalResourceOverrideButtonNavigationItem.element, this._populateCreateLocalResourceOverrideContextMenu.bind(this)); else this._createLocalResourceOverrideButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleCreateLocalResourceOverride, this); } WI.networkManager.addEventListener(WI.NetworkManager.Event.LocalResourceOverrideAdded, this._handleLocalResourceOverrideChanged, this); WI.networkManager.addEventListener(WI.NetworkManager.Event.LocalResourceOverrideRemoved, this._handleLocalResourceOverrideChanged, this); } } // Public get resource() { return this._resource; } get navigationItems() { let items = []; if (this._importLocalResourceOverrideButtonNavigationItem) items.push(this._importLocalResourceOverrideButtonNavigationItem); if (this._mapLocalResourceOverrideToFileButtonNavigationItem) items.push(this._mapLocalResourceOverrideToFileButtonNavigationItem); if (items.length) items.push(new WI.DividerNavigationItem); if (this._removeLocalResourceOverrideButtonNavigationItem) items.push(this._removeLocalResourceOverrideButtonNavigationItem); if (this._createLocalResourceOverrideButtonNavigationItem) items.push(this._createLocalResourceOverrideButtonNavigationItem); return items; } get supportsSave() { return this._resource.finished; } get saveMode() { return WI.FileUtilities.SaveMode.SingleFile; } get saveData() { let saveData = { url: this._resource.url, content: this._resource.content, }; if (this._resource.urlComponents.path === "/") { let extension = WI.fileExtensionForMIMEType(this._resource.mimeType); if (extension) saveData.suggestedName = `index.${extension}`; } return saveData; } contentAvailable(content, base64Encoded) { throw WI.NotImplementedError.subclassMustOverride(); } get createLocalResourceOverrideTooltip() { return WI.UIString("Click to import a file and create a Local Override\nShift-click to create a Local Override from this content"); } requestLocalResourceOverrideInitialContent() { // Overridden by subclasses if needed. return new Promise((resolve, reject) => { WI.FileUtilities.import(async (fileList) => { console.assert(fileList.length === 1); this._getContentForLocalResourceOverrideFromFile(fileList[0], ({mimeType, base64Encoded, content}) => { resolve({mimeType, base64Encoded, content}); }); }); }); } showGenericNoContentMessage() { this.showMessage(WI.UIString("Resource has no content.")); this.dispatchEventToListeners(WI.ResourceContentView.Event.ContentError); } showNoCachedContentMessage() { this.showMessage(WI.UIString("Resource has no cached content.", "Resource has no cached content. @ Resource Preview", "An error message shown when there is no cached content for a HTTP 304 Not Modified resource response.")); this.dispatchEventToListeners(WI.ResourceContentView.Event.ContentError); } showGenericErrorMessage() { this._contentError(WI.UIString("An error occurred trying to load the resource.")); } showMessage(message) { this.removeAllSubviews(); this.element.appendChild(WI.createMessageTextView(message)); } addIssue(issue) { // This generically shows only the last issue, subclasses can override for better handling. this.removeAllSubviews(); this.element.appendChild(WI.createMessageTextView(issue.text, issue.level === WI.IssueMessage.Level.Error)); } closed() { super.closed(); if (WI.NetworkManager.supportsOverridingResponses()) { WI.networkManager.removeEventListener(WI.NetworkManager.Event.LocalResourceOverrideAdded, this._handleLocalResourceOverrideChanged, this); WI.networkManager.removeEventListener(WI.NetworkManager.Event.LocalResourceOverrideRemoved, this._handleLocalResourceOverrideChanged, this); } if (!this.managesOwnIssues) WI.consoleManager.removeEventListener(WI.ConsoleManager.Event.IssueAdded, this._issueWasAdded, this); } // Protected removeLoadingIndicator() { if (this._spinnerTimeout) { clearTimeout(this._spinnerTimeout); this._spinnerTimeout = undefined; } this.removeAllSubviews(); if (this._localResourceOverrideBannerView) this.addSubview(this._localResourceOverrideBannerView); } // Private _contentAvailable(parameters) { if (parameters.error) { // A 304 Not Modified request that is missing content means we didn't have a cached copy. if (parameters.sourceCode.statusCode == 304 && parameters.reason === "Missing content of resource for given requestId") { this.showNoCachedContentMessage(); return; } this._contentError(parameters.error); return; } if (parameters.message) { this.showMessage(parameters.message); return; } if (parameters.sourceCode instanceof WI.LocalResource) { if (this.resource.mappedFilePath) { this._handleMappedFilePathChanged(); return; } } // The view maybe populated with inline scripts content by the time resource // content arrives. SourceCodeTextEditor will handle that. if (this._hasContent()) return; if (!parameters.sourceCode.content && !parameters.sourceCode.mimeType) { this.showGenericNoContentMessage(); return; } // Content is ready to show, call the public method now. console.assert(parameters.sourceCode === this._resource); this.contentAvailable(parameters.sourceCode.content, parameters.base64Encoded); if (this._createLocalResourceOverrideButtonNavigationItem) this._createLocalResourceOverrideButtonNavigationItem.enabled = WI.networkManager.canBeOverridden(this._resource); } _contentError(error) { if (this._hasContent()) return; this.removeLoadingIndicator(); this.element.appendChild(WI.createMessageTextView(error, true)); this.dispatchEventToListeners(WI.ResourceContentView.Event.ContentError); } _hasContent() { return this.element.hasChildNodes() && !this.element.querySelector(".indeterminate-progress-spinner"); } _issueWasAdded(event) { console.assert(!this.managesOwnIssues); var issue = event.data.issue; if (!WI.ConsoleManager.issueMatchSourceCode(issue, this._resource)) return; this.addIssue(issue); } async _getContentForLocalResourceOverrideFromFile(file, callback) { let mimeType = file.type || WI.mimeTypeForFileExtension(WI.fileExtensionForFilename(file.name)); if (WI.shouldTreatMIMETypeAsText(mimeType)) { await WI.FileUtilities.readText(file, async ({text}) => { await callback({ mimeType, base64Encoded: false, content: text, }); }); } else { await WI.FileUtilities.readData(file, async ({mimeType, base64Encoded, content}) => { await callback({mimeType, base64Encoded, content}); }); } } async _createAndShowLocalResourceOverride(type, {requestInitialContent} = {}) { let initialContent = requestInitialContent ? await this.requestLocalResourceOverrideInitialContent() : {}; let localResourceOverride = await this._resource.createLocalResourceOverride(type, initialContent); WI.networkManager.addLocalResourceOverride(localResourceOverride); WI.showLocalResourceOverride(localResourceOverride, {overriddenResource: this._resource}); } _populateCreateLocalResourceOverrideContextMenu(contextMenu, event) { if (!this._createLocalResourceOverrideButtonNavigationItem.enabled) return; if (WI.NetworkManager.supportsOverridingRequests()) { contextMenu.appendItem(WI.UIString("Create Request Local Override"), () => { // Request overrides cannot be created from a file as files don't have network info. this._createAndShowLocalResourceOverride(WI.LocalResourceOverride.InterceptType.Request); }); } contextMenu.appendItem(WI.UIString("Create Response Local Override"), () => { this._createAndShowLocalResourceOverride(WI.LocalResourceOverride.InterceptType.Response, { requestInitialContent: !event.shiftKey, }); }); if (WI.NetworkManager.supportsBlockingRequests()) { contextMenu.appendItem(WI.UIString("Block Request URL"), async () => { let localResourceOverride = await this._resource.createLocalResourceOverride(WI.LocalResourceOverride.InterceptType.Block); WI.networkManager.addLocalResourceOverride(localResourceOverride); }); } } _handleCreateLocalResourceOverride(event) { let {nativeEvent} = event.data; this._createAndShowLocalResourceOverride(WI.LocalResourceOverride.InterceptType.Response, { requestInitialContent: !nativeEvent.shiftKey, }); } _handleImportLocalResourceOverride(event) { let localResourceOverride = this.resource.localResourceOverride || WI.networkManager.localResourceOverridesForURL(this.resource.url)[0]; console.assert(localResourceOverride); WI.FileUtilities.import(async (fileList) => { console.assert(fileList.length === 1); await this._getContentForLocalResourceOverrideFromFile(fileList[0], ({mimeType, base64Encoded, content}) => { let revision = localResourceOverride.localResource.editableRevision; revision.updateRevisionContent(content, {base64Encoded, mimeType}); }); if (!this._resource.localResourceOverride) WI.showLocalResourceOverride(localResourceOverride, {overriddenResource: this._resource}); }); } _handleMapLocalResourceOverrideToFile(event) { WI.FileUtilities.import((files) => { console.assert(files.length === 1, files); this.resource.mappedFilePath = files[0].getPath(); }); } _handleMappedFilePathChanged(event) { let mappedFilePath = this.resource.mappedFilePath; let mappedFilePathLink = document.createElement("a"); mappedFilePathLink.href = "file://" + mappedFilePath; mappedFilePathLink.textContent = mappedFilePath.insertWordBreakCharacters(); mappedFilePathLink.addEventListener("click", (event) => { event.stop(); InspectorFrontendHost.revealFileExternally(event.target.href); }); let fragment = document.createDocumentFragment(); String.format(WI.UIString("Mapped to \u201C%s\u201D"), [mappedFilePathLink], String.standardFormatters, fragment, (a, b) => { a.append(b); return a; }); this.showMessage(fragment); if (this._localResourceOverrideBannerView) { this.element.insertBefore(this._localResourceOverrideBannerView.element, this.element.firstChild); this.addSubview(this._localResourceOverrideBannerView); } } _handleRemoveLocalResourceOverride(event) { let localResourceOverride = this.resource.localResourceOverride || WI.networkManager.localResourceOverridesForURL(this._resource.url)[0]; console.assert(localResourceOverride); WI.networkManager.removeLocalResourceOverride(localResourceOverride); } _handleLocalResourceOverrideChanged(event) { let {localResourceOverride} = event.data; if (!localResourceOverride.matches(this._resource.url)) return; if (this._createLocalResourceOverrideButtonNavigationItem) this._createLocalResourceOverrideButtonNavigationItem.enabled = WI.networkManager.canBeOverridden(this._resource); } _mouseWasClicked(event) { WI.handlePossibleLinkClick(event, {frame: this._resource.parentFrame}); } }; WI.ResourceContentView.Event = { ContentError: "resource-content-view-content-error", };