| /* |
| * Copyright (C) 2020-2021 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.WebInspectorExtensionController = class WebInspectorExtensionController extends WI.Object |
| { |
| constructor() |
| { |
| super(); |
| |
| this._extensionForExtensionIDMap = new Map; |
| this._extensionTabContentViewForExtensionTabIDMap = new Map; |
| this._tabIDsForExtensionIDMap = new Multimap; |
| this._nextExtensionTabID = 1; |
| |
| this._extensionTabPositions = null; |
| this._saveTabPositionsDebouncer = null; |
| |
| WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleMainResourceDidChange, this); |
| } |
| |
| // Static |
| |
| static get extensionTabPositionsObjectStoreKey() |
| { |
| return "extension-tab-positions"; |
| } |
| |
| // Public |
| |
| get registeredExtensionIDs() |
| { |
| return new Set(this._extensionForExtensionIDMap.keys()); |
| } |
| |
| registerExtension(extensionID, extensionBundleIdentifier, displayName) |
| { |
| if (this._extensionForExtensionIDMap.has(extensionID)) { |
| WI.reportInternalError("Unable to register extension, it's already registered: " + extensionID); |
| return WI.WebInspectorExtension.ErrorCode.RegistrationFailed; |
| } |
| |
| if (!this._extensionForExtensionIDMap.size) { |
| WI.tabBrowser.tabBar.addEventListener(WI.TabBar.Event.TabBarItemAdded, this._saveExtensionTabPositions, this); |
| WI.tabBrowser.tabBar.addEventListener(WI.TabBar.Event.TabBarItemRemoved, this._saveExtensionTabPositions, this); |
| WI.tabBrowser.tabBar.addEventListener(WI.TabBar.Event.TabBarItemsReordered, this._saveExtensionTabPositions, this); |
| } |
| |
| let extension = new WI.WebInspectorExtension(extensionID, extensionBundleIdentifier, displayName); |
| this._extensionForExtensionIDMap.set(extensionID, extension); |
| |
| this.dispatchEventToListeners(WI.WebInspectorExtensionController.Event.ExtensionAdded, {extension}); |
| } |
| |
| unregisterExtension(extensionID) |
| { |
| let extension = this._extensionForExtensionIDMap.take(extensionID); |
| if (!extension) { |
| WI.reportInternalError("Unable to unregister extension with unknown ID: " + extensionID); |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| } |
| |
| if (!this._extensionForExtensionIDMap.size) { |
| WI.tabBrowser.tabBar.removeEventListener(WI.TabBar.Event.TabBarItemAdded, this._saveExtensionTabPositions, this); |
| WI.tabBrowser.tabBar.removeEventListener(WI.TabBar.Event.TabBarItemRemoved, this._saveExtensionTabPositions, this); |
| WI.tabBrowser.tabBar.removeEventListener(WI.TabBar.Event.TabBarItemsReordered, this._saveExtensionTabPositions, this); |
| } |
| |
| let extensionTabIDsToRemove = this._tabIDsForExtensionIDMap.take(extensionID) || []; |
| for (let extensionTabID of extensionTabIDsToRemove) { |
| let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.take(extensionTabID); |
| |
| // Ensure that the iframe is actually detached and does not leak. |
| WI.tabBrowser.closeTabForContentView(tabContentView, {suppressAnimations: true}); |
| tabContentView.dispose(); |
| } |
| |
| this.dispatchEventToListeners(WI.WebInspectorExtensionController.Event.ExtensionRemoved, {extension}); |
| } |
| |
| async createTabForExtension(extensionID, tabName, tabIconURL, sourceURL) |
| { |
| let extension = this._extensionForExtensionIDMap.get(extensionID); |
| if (!extension) { |
| WI.reportInternalError("Unable to create tab for extension with unknown ID: " + extensionID + " sourceURL: " + sourceURL); |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| } |
| |
| let extensionTabID = `WebExtensionTab-${extensionID}-${this._nextExtensionTabID++}`; |
| let tabContentView = new WI.WebInspectorExtensionTabContentView(extension, extensionTabID, tabName, tabIconURL, sourceURL); |
| |
| this._tabIDsForExtensionIDMap.add(extensionID, extensionTabID); |
| this._extensionTabContentViewForExtensionTabIDMap.set(extensionTabID, tabContentView); |
| |
| if (!this._extensionTabPositions) |
| await this._loadExtensionTabPositions(); |
| |
| WI.tabBrowser.addTabForContentView(tabContentView, { |
| suppressAnimations: true, |
| insertionIndex: this._insertionIndexForExtensionTab(tabContentView), |
| }); |
| |
| // The calling convention is to return an error string or a result object. |
| return {"result": extensionTabID}; |
| } |
| |
| evaluateScriptForExtension(extensionID, scriptSource, {frameURL, contextSecurityOrigin, useContentScriptContext} = {}) |
| { |
| let extension = this._extensionForExtensionIDMap.get(extensionID); |
| if (!extension) { |
| WI.reportInternalError("Unable to evaluate script for extension with unknown ID: " + extensionID); |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| } |
| |
| let frame = this._frameForFrameURL(frameURL); |
| if (!frame) { |
| WI.reportInternalError("evaluateScriptForExtension: No frame matched provided frameURL: " + frameURL); |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| } |
| |
| if (contextSecurityOrigin) { |
| WI.reportInternalError("evaluateScriptForExtension: the 'contextSecurityOrigin' option is not yet implemented."); |
| return WI.WebInspectorExtension.ErrorCode.NotImplemented; |
| } |
| |
| if (useContentScriptContext) { |
| WI.reportInternalError("evaluateScriptForExtension: the 'useContentScriptContext' option is not yet implemented."); |
| return WI.WebInspectorExtension.ErrorCode.NotImplemented; |
| } |
| |
| let evaluationContext = frame.pageExecutionContext; |
| if (!evaluationContext) { |
| WI.reportInternalError("evaluateScriptForExtension: No 'pageExecutionContext' was present for frame with URL: " + frame.url); |
| return WI.WebInspectorExtension.ErrorCode.ContextDestroyed; |
| } |
| |
| return evaluationContext.target.RuntimeAgent.evaluate.invoke({ |
| expression: scriptSource, |
| objectGroup: "extension-evaluation", |
| includeCommandLineAPI: true, |
| returnByValue: true, |
| generatePreview: false, |
| saveResult: false, |
| contextId: evaluationContext.id, |
| }).then((payload) => { |
| let resultOrError = payload.result; |
| let wasThrown = payload.wasThrown; |
| let {type, value} = resultOrError; |
| return wasThrown ? {"error": resultOrError.description} : {"result": value}; |
| }).catch((error) => error.description); |
| } |
| |
| reloadForExtension(extensionID, {ignoreCache, userAgent, injectedScript} = {}) |
| { |
| let extension = this._extensionForExtensionIDMap.get(extensionID); |
| if (!extension) { |
| WI.reportInternalError("Unable to evaluate script for extension with unknown ID: " + extensionID); |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| } |
| |
| // FIXME: <webkit.org/b/222328> Implement `userAgent` and `injectedScript` options for `devtools.inspectedWindow.reload` command |
| if (userAgent) { |
| WI.reportInternalError("reloadForExtension: the 'userAgent' option is not yet implemented."); |
| return WI.WebInspectorExtension.ErrorCode.NotImplemented; |
| } |
| |
| if (injectedScript) { |
| WI.reportInternalError("reloadForExtension: the 'injectedScript' option is not yet implemented."); |
| return WI.WebInspectorExtension.ErrorCode.NotImplemented; |
| } |
| |
| let target = WI.assumingMainTarget(); |
| if (!target.hasCommand("Page.reload")) |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| |
| return target.PageAgent.reload.invoke({ignoreCache}); |
| } |
| |
| showExtensionTab(extensionTabID, options = {}) |
| { |
| let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.get(extensionTabID); |
| if (!tabContentView) { |
| WI.reportInternalError("Unable to show extension tab with unknown extensionTabID: " + extensionTabID); |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| } |
| |
| tabContentView.visible = true; |
| let success = WI.tabBrowser.showTabForContentView(tabContentView, { |
| ...options, |
| insertionIndex: this._insertionIndexForExtensionTab(tabContentView), |
| initiatorHint: WI.TabBrowser.TabNavigationInitiator.FrontendAPI, |
| }); |
| |
| if (!success) { |
| WI.reportInternalError("Unable to show extension tab with extensionTabID: " + extensionTabID); |
| return WI.WebInspectorExtension.ErrorCode.InternalError; |
| } |
| |
| tabContentView.visible = true; |
| |
| // Clients expect to be able to use evaluateScriptInExtensionTab() when this method |
| // returns, so wait for the extension tab to finish its loading sequence. Wrap the result. |
| return tabContentView.whenPageAvailable().then((sourceURL) => { return {"result": sourceURL}; }); |
| } |
| |
| hideExtensionTab(extensionTabID, options = {}) |
| { |
| let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.get(extensionTabID); |
| if (!tabContentView) { |
| WI.reportInternalError("Unable to show extension tab with unknown extensionTabID: " + extensionTabID); |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| } |
| |
| tabContentView.visible = false; |
| WI.tabBrowser.closeTabForContentView(tabContentView, options); |
| |
| console.assert(!tabContentView.visible); |
| console.assert(!tabContentView.isClosed); |
| } |
| |
| addContextMenuItemsForClosedExtensionTabs(contextMenu) |
| { |
| contextMenu.appendSeparator(); |
| |
| for (let tabContentView of this._extensionTabContentViewForExtensionTabIDMap.values()) { |
| // If the extension tab has been unchecked in the TabBar context menu, then the tabBarItem |
| // for the extension tab will not be connected to a parent TabBar. |
| let shouldIncludeTab = !tabContentView.visible || !tabContentView.tabBarItem.parentTabBar; |
| if (!shouldIncludeTab) |
| continue; |
| |
| contextMenu.appendItem(tabContentView.tabInfo().displayName, () => { |
| this.showExtensionTab(tabContentView.extensionTabID); |
| }); |
| } |
| } |
| |
| addContextMenuItemsForAllExtensionTabs(contextMenu) |
| { |
| contextMenu.appendSeparator(); |
| |
| for (let tabContentView of this._extensionTabContentViewForExtensionTabIDMap.values()) { |
| let checked = tabContentView.visible || !!tabContentView.tabBarItem.parentTabBar; |
| contextMenu.appendCheckboxItem(tabContentView.tabInfo().displayName, () => { |
| if (!checked) |
| this.showExtensionTab(tabContentView.extensionTabID); |
| else |
| this.hideExtensionTab(tabContentView.extensionTabID); |
| }, checked); |
| } |
| } |
| |
| activeExtensionTabContentViews() |
| { |
| return Array.from(this._extensionTabContentViewForExtensionTabIDMap.values()).filter((tab) => tab.visible || tab.tabBarItem.parentTabBar); |
| } |
| |
| evaluateScriptInExtensionTab(extensionTabID, scriptSource) |
| { |
| let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.get(extensionTabID); |
| if (!tabContentView) { |
| WI.reportInternalError("Unable to evaluate with unknown extensionTabID: " + extensionTabID); |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| } |
| |
| let iframe = tabContentView.iframeElement; |
| if (!(iframe instanceof HTMLIFrameElement)) { |
| WI.reportInternalError("Unable to evaluate without an <iframe> for extensionTabID: " + extensionTabID); |
| return WI.WebInspectorExtension.ErrorCode.InvalidRequest; |
| } |
| |
| return new Promise((resolve, reject) => { |
| try { |
| // If `result` is a promise, then it came from a different frame and `instanceof Promise` won't work. |
| let result = InspectorFrontendHost.evaluateScriptInExtensionTab(iframe, scriptSource); |
| if (result?.then) { |
| result.then((resolvedValue) => resolve({result: resolvedValue}), (errorValue) => reject({error: errorValue})); |
| return; |
| } |
| |
| resolve({result}); |
| } catch (error) { |
| // Include more context in the stringification of the error. |
| const stackIndent = " "; |
| let stackLines = (error.stack?.split("\n") || []).map((line) => `${stackIndent}${line}`); |
| let formattedMessage = [ |
| `Caught Exception: ${error.name}`, |
| `at ${error.sourceURL || "(unknown)"}:${error.line || 0}:${error.column || 0}:`, |
| error.message, |
| "", |
| "Backtrace:", |
| ...stackLines, |
| ].join("\n"); |
| reject({error: formattedMessage}); |
| } |
| }); |
| } |
| |
| // Private |
| |
| async _loadExtensionTabPositions() |
| { |
| let savedTabPositions = await WI.objectStores.general.get(WebInspectorExtensionController.extensionTabPositionsObjectStoreKey); |
| this._extensionTabPositions = savedTabPositions || {}; |
| } |
| |
| _saveExtensionTabPositions() |
| { |
| if (!this._extensionTabPositions) |
| return; |
| |
| this._saveTabPositionsDebouncer ||= new Debouncer(() => { |
| for (let tabBarItem of WI.tabBrowser.tabBar.visibleTabBarItemsFromLeftToRight) { |
| if (!(tabBarItem.representedObject instanceof WI.WebInspectorExtensionTabContentView)) |
| continue; |
| |
| let {anchorTabType, anchorTabIndex, distanceFromAnchorTab} = this._computeIndicesForExtensionTab(tabBarItem.representedObject, {recomputePositions: true}); |
| this._extensionTabPositions[tabBarItem.representedObject.savedTabPositionKey] = {anchorTabType, distanceFromAnchorTab}; |
| } |
| |
| WI.objectStores.general.put(this._extensionTabPositions, WebInspectorExtensionController.extensionTabPositionsObjectStoreKey); |
| }); |
| this._saveTabPositionsDebouncer.delayForTime(5000); |
| } |
| |
| _insertionIndexForExtensionTab(tabContentView, options = {}) |
| { |
| let {anchorTabType, anchorTabIndex, distanceFromAnchorTab} = this._computeIndicesForExtensionTab(tabContentView, options); |
| return anchorTabIndex + distanceFromAnchorTab + 1; |
| } |
| |
| _computeIndicesForExtensionTab(tabContentView, {recomputePositions} = {}) |
| { |
| // Note: pinned tabs always appear on the trailing edge, so we can ignore them |
| // for the purposes of computing an `insertionIndex`` for `tabContentView`. |
| let anchorTabIndex = 0; |
| let savedPositions = this._extensionTabPositions[tabContentView.savedTabPositionKey] || {}; |
| let anchorTabType = (recomputePositions && savedPositions.anchorTabType) || null; |
| let distanceFromAnchorTab = (recomputePositions && savedPositions.distanceFromAnchorTab) || 0; |
| |
| let visibleTabBarItems = WI.tabBrowser.tabBar.visibleTabBarItemsFromLeftToRight; |
| for (let i = 0; i < visibleTabBarItems.length; ++i) { |
| let visibleTab = visibleTabBarItems[i].representedObject; |
| if (!visibleTab) |
| continue; |
| |
| if (visibleTab === tabContentView) |
| break; |
| |
| if (visibleTab instanceof WI.WebInspectorExtensionTabContentView) |
| continue; |
| |
| if (recomputePositions) { |
| anchorTabType = visibleTab.type || null; |
| continue; |
| } |
| |
| if (visibleTab.type !== anchorTabType) |
| continue; |
| |
| anchorTabIndex = i; |
| break; |
| } |
| |
| // Find the count of extension tabs after the anchor tab to compute the real distanceFromAnchorTab. |
| // Adding `distanceFromAnchorTab` to `anchorTabIndex` should not insert the tab after a different anchor tab. |
| for (let i = 1; i < visibleTabBarItems.length - anchorTabIndex; ++i) { |
| if (visibleTabBarItems[anchorTabIndex + i].representedObject?.constructor?.shouldSaveTab?.()) { |
| distanceFromAnchorTab = Number.constrain(distanceFromAnchorTab, 0, Math.max(0, i - 1)); |
| return {anchorTabType, anchorTabIndex, distanceFromAnchorTab}; |
| } |
| } |
| |
| // If the anchor tab is now hidden upon restoring, place the extension at the end. |
| // This could happen if a smaller set of tabs are enabled for the inspection target. |
| anchorTabIndex = visibleTabBarItems.length - 1; |
| return {anchorTabType, anchorTabIndex, distanceFromAnchorTab}; |
| } |
| |
| _frameForFrameURL(frameURL) |
| { |
| if (!frameURL) |
| return WI.networkManager.mainFrame; |
| |
| function findFrame(frameURL, adjustKnownFrameURL) { |
| return WI.networkManager.frames.find((knownFrame) => { |
| let knownFrameURL = new URL(knownFrame.url); |
| adjustKnownFrameURL?.(knownFrameURL); |
| return knownFrameURL.toString() === frameURL; |
| }); |
| } |
| |
| let frame = findFrame(frameURL); |
| if (frame) |
| return frame; |
| |
| let frameURLParts = new URL(frameURL); |
| if (frameURLParts.hash.length) |
| return null; |
| |
| frame = findFrame(frameURL, (knownFrameURL) => { |
| knownFrameURL.hash = ""; |
| }); |
| if (frame) |
| return frame; |
| |
| if (frameURLParts.search.length) |
| return null; |
| |
| return findFrame(frameURL, (knownFrameURL) => { |
| knownFrameURL.hash = ""; |
| knownFrameURL.search = ""; |
| }); |
| } |
| |
| _handleMainResourceDidChange(event) |
| { |
| if (!event.target.isMainFrame()) |
| return; |
| |
| // Don't fire the event unless one or more extensions are registered. |
| if (!this._extensionForExtensionIDMap.size) |
| return; |
| |
| InspectorFrontendHost.inspectedPageDidNavigate(WI.networkManager.mainFrame.url); |
| } |
| }; |
| |
| WI.WebInspectorExtensionController.Event = { |
| ExtensionAdded: "extension-added", |
| ExtensionRemoved: "extension-removed", |
| }; |