Added SDK
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
* 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});
|
||||
}
|
||||
|
||||
navigateTabForExtension(extensionTabID, sourceURL)
|
||||
{
|
||||
let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.get(extensionTabID);
|
||||
if (!tabContentView) {
|
||||
WI.reportInternalError("Unable to navigate extension tab with unknown extensionTabID: " + extensionTabID);
|
||||
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
||||
}
|
||||
|
||||
tabContentView.iframeURL = sourceURL;
|
||||
}
|
||||
|
||||
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",
|
||||
};
|
||||
Reference in New Issue
Block a user