UNPKG

chrome-devtools-frontend

Version:
1,145 lines (1,024 loc) • 40.9 kB
/* * Copyright (C) 2011 Google 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: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 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. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT * OWNER OR 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. */ // @ts-nocheck // TODO(crbug.com/1011811): Enable TypeScript compiler checks import * as Bindings from '../bindings/bindings.js'; import * as Common from '../common/common.js'; import * as Components from '../components/components.js'; import * as Host from '../host/host.js'; import * as Platform from '../platform/platform.js'; import * as ProtocolClient from '../protocol_client/protocol_client.js'; // eslint-disable-line no-unused-vars import * as Root from '../root/root.js'; // eslint-disable-line no-unused-vars import * as SDK from '../sdk/sdk.js'; import * as TextUtils from '../text_utils/text_utils.js'; // eslint-disable-line no-unused-vars import * as ThemeSupport from '../theme_support/theme_support.js'; import * as UI from '../ui/ui.js'; import * as Workspace from '../workspace/workspace.js'; import {ExtensionButton, ExtensionPanel, ExtensionSidebarPane} from './ExtensionPanel.js'; import {ExtensionTraceProvider, TracingSession} from './ExtensionTraceProvider.js'; // eslint-disable-line no-unused-vars import {LanguageExtensionEndpoint} from './LanguageExtensionEndpoint.js'; const extensionOriginSymbol = Symbol('extensionOrigin'); const kAllowedOrigins = [ 'chrome://newtab', 'chrome://new-tab-page', ].map(url => (new URL(url)).origin); /** @type {?ExtensionServer} */ let extensionServerInstance; export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper { /** * @private */ constructor() { super(); this._clientObjects = {}; this._handlers = {}; /** @type {!Map<string, !Set<!MessagePort>>} */ this._subscribers = new Map(); this._subscriptionStartHandlers = {}; this._subscriptionStopHandlers = {}; /** @type {!Map<string, !Map<string, *>>} */ this._extraHeaders = new Map(); this._requests = {}; this._lastRequestId = 0; /** @type {!Map<string, !{name: string}>} */ this._registeredExtensions = new Map(); this._status = new ExtensionStatus(); /** @type {!Array<!ExtensionSidebarPane>} */ this._sidebarPanes = []; /** @type {!Array<!ExtensionTraceProvider>} */ this._traceProviders = []; /** @type {!Map<string, !TracingSession>} */ this._traceSessions = new Map(); // TODO(caseq): properly unload extensions when we disable them. this._extensionsEnabled = true; const commands = Extensions.extensionAPI.Commands; this._registerHandler(commands.AddRequestHeaders, this._onAddRequestHeaders.bind(this)); this._registerHandler(commands.AddTraceProvider, this._onAddTraceProvider.bind(this)); this._registerHandler(commands.ApplyStyleSheet, this._onApplyStyleSheet.bind(this)); this._registerHandler(commands.CompleteTraceSession, this._onCompleteTraceSession.bind(this)); this._registerHandler(commands.CreatePanel, this._onCreatePanel.bind(this)); this._registerHandler(commands.CreateSidebarPane, this._onCreateSidebarPane.bind(this)); this._registerHandler(commands.CreateToolbarButton, this._onCreateToolbarButton.bind(this)); this._registerHandler(commands.EvaluateOnInspectedPage, this._onEvaluateOnInspectedPage.bind(this)); this._registerHandler(commands.ForwardKeyboardEvent, this._onForwardKeyboardEvent.bind(this)); this._registerHandler(commands.GetHAR, this._onGetHAR.bind(this)); this._registerHandler(commands.GetPageResources, this._onGetPageResources.bind(this)); this._registerHandler(commands.GetRequestContent, this._onGetRequestContent.bind(this)); this._registerHandler(commands.GetResourceContent, this._onGetResourceContent.bind(this)); this._registerHandler(commands.Reload, this._onReload.bind(this)); this._registerHandler(commands.SetOpenResourceHandler, this._onSetOpenResourceHandler.bind(this)); this._registerHandler(commands.SetResourceContent, this._onSetResourceContent.bind(this)); this._registerHandler(commands.SetSidebarHeight, this._onSetSidebarHeight.bind(this)); this._registerHandler(commands.SetSidebarContent, this._onSetSidebarContent.bind(this)); this._registerHandler(commands.SetSidebarPage, this._onSetSidebarPage.bind(this)); this._registerHandler(commands.ShowPanel, this._onShowPanel.bind(this)); this._registerHandler(commands.Subscribe, this._onSubscribe.bind(this)); this._registerHandler(commands.OpenResource, this._onOpenResource.bind(this)); this._registerHandler(commands.Unsubscribe, this._onUnsubscribe.bind(this)); this._registerHandler(commands.UpdateButton, this._onUpdateButton.bind(this)); this._registerHandler(commands.RegisterLanguageExtensionPlugin, this._registerLanguageExtensionEndpoint.bind(this)); window.addEventListener('message', this._onWindowMessage.bind(this), false); // Only for main window. const existingTabId = window.DevToolsAPI && window.DevToolsAPI.getInspectedTabId && window.DevToolsAPI.getInspectedTabId(); if (existingTabId) { this._setInspectedTabId({data: existingTabId}); } Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener( Host.InspectorFrontendHostAPI.Events.SetInspectedTabId, this._setInspectedTabId, this); this._languageExtensionRequests = new Map(); this._initExtensions(); } /** * @param {{forceNew: ?boolean}} opts */ static instance(opts = {forceNew: null}) { const {forceNew} = opts; if (!extensionServerInstance || forceNew) { extensionServerInstance = new ExtensionServer(); } return extensionServerInstance; } initializeExtensions() { Host.InspectorFrontendHost.InspectorFrontendHostInstance.setAddExtensionCallback(this._addExtension.bind(this)); } /** * @return {boolean} */ hasExtensions() { return Boolean(this._registeredExtensions.size); } /** * @param {string} panelId * @param {string} action * @param {string=} searchString */ notifySearchAction(panelId, action, searchString) { this._postNotification(Extensions.extensionAPI.Events.PanelSearch + panelId, action, searchString); } /** * @param {string} identifier * @param {number=} frameIndex */ notifyViewShown(identifier, frameIndex) { this._postNotification(Extensions.extensionAPI.Events.ViewShown + identifier, frameIndex); } /** * @param {string} identifier */ notifyViewHidden(identifier) { this._postNotification(Extensions.extensionAPI.Events.ViewHidden + identifier); } /** * @param {string} identifier */ notifyButtonClicked(identifier) { this._postNotification(Extensions.extensionAPI.Events.ButtonClicked + identifier); } _registerLanguageExtensionEndpoint(message, shared_port) { const {pluginManager} = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(); if (!pluginManager) { return this._status.E_FAILED('WebAssembly DWARF support needs to be enabled to use this extension'); } const {pluginName, port, supportedScriptTypes: {language, symbol_types}} = message; const symbol_types_array = /** @type !Array<string> */ (Array.isArray(symbol_types) && symbol_types.every(e => typeof e === 'string') ? symbol_types : []); const endpoint = new LanguageExtensionEndpoint(pluginName, {language, symbol_types: symbol_types_array}, port); pluginManager.addPlugin(endpoint); return this._status.OK(); } _inspectedURLChanged(event) { if (!this._canInspectURL(event.data.inspectedURL())) { this._disableExtensions(); return; } if (event.data !== SDK.SDKModel.TargetManager.instance().mainTarget()) { return; } this._requests = {}; const url = event.data.inspectedURL(); this._postNotification(Extensions.extensionAPI.Events.InspectedURLChanged, url); } /** * @param {string} providerId * @param {string} sessionId * @param {!TracingSession} session */ startTraceRecording(providerId, sessionId, session) { this._traceSessions.set(sessionId, session); this._postNotification('trace-recording-started-' + providerId, sessionId); } /** * @param {string} providerId */ stopTraceRecording(providerId) { this._postNotification('trace-recording-stopped-' + providerId); } /** * @param {string} type * @return {boolean} */ hasSubscribers(type) { return this._subscribers.has(type); } /** * @param {string} type * @param {...*} vararg */ _postNotification(type, vararg) { if (!this._extensionsEnabled) { return; } const subscribers = this._subscribers.get(type); if (!subscribers) { return; } const message = {command: 'notify-' + type, arguments: Array.prototype.slice.call(arguments, 1)}; for (const subscriber of subscribers) { subscriber.postMessage(message); } } _onSubscribe(message, port) { const subscribers = this._subscribers.get(message.type); if (subscribers) { subscribers.add(port); } else { this._subscribers.set(message.type, new Set([port])); if (this._subscriptionStartHandlers[message.type]) { this._subscriptionStartHandlers[message.type](); } } } _onUnsubscribe(message, port) { const subscribers = this._subscribers.get(message.type); if (!subscribers) { return; } subscribers.delete(port); if (!subscribers.size) { this._subscribers.delete(message.type); if (this._subscriptionStopHandlers[message.type]) { this._subscriptionStopHandlers[message.type](); } } } _onAddRequestHeaders(message) { const id = message.extensionId; if (typeof id !== 'string') { return this._status.E_BADARGTYPE('extensionId', typeof id, 'string'); } let extensionHeaders = this._extraHeaders.get(id); if (!extensionHeaders) { extensionHeaders = new Map(); this._extraHeaders.set(id, extensionHeaders); } for (const name in message.headers) { extensionHeaders.set(name, message.headers[name]); } const allHeaders = /** @type {!Protocol.Network.Headers} */ ({}); for (const headers of this._extraHeaders.values()) { for (const name of headers.keys()) { if (name !== '__proto__' && typeof headers.get(name) === 'string') { allHeaders[name] = headers.get(name); } } } SDK.NetworkManager.MultitargetNetworkManager.instance().setExtraHTTPHeaders(allHeaders); } /** * @param {*} message */ _onApplyStyleSheet(message) { if (!Root.Runtime.experiments.isEnabled('applyCustomStylesheet')) { return; } const styleSheet = createElement('style'); styleSheet.textContent = message.styleSheet; document.head.appendChild(styleSheet); ThemeSupport.ThemeSupport.instance().addCustomStylesheet(message.styleSheet); // Add to all the shadow roots that have already been created for (let node = document.body; node; node = node.traverseNextNode(document.body)) { if (node instanceof ShadowRoot) { ThemeSupport.ThemeSupport.instance().injectCustomStyleSheets(node); } } } _onCreatePanel(message, port) { const id = message.id; // The ids are generated on the client API side and must be unique, so the check below // shouldn't be hit unless someone is bypassing the API. if (id in this._clientObjects || UI.InspectorView.InspectorView.instance().hasPanel(id)) { return this._status.E_EXISTS(id); } const page = this._expandResourcePath(port[extensionOriginSymbol], message.page); let persistentId = port[extensionOriginSymbol] + message.title; persistentId = persistentId.replace(/\s/g, ''); const panelView = new ExtensionServerPanelView(persistentId, message.title, new ExtensionPanel(this, persistentId, id, page)); this._clientObjects[id] = panelView; UI.InspectorView.InspectorView.instance().addPanel(panelView); return this._status.OK(); } _onShowPanel(message) { let panelViewId = message.id; const panelView = this._clientObjects[message.id]; if (panelView && panelView instanceof ExtensionServerPanelView) { panelViewId = panelView.viewId(); } UI.InspectorView.InspectorView.instance().showPanel(panelViewId); } _onCreateToolbarButton(message, port) { const panelView = this._clientObjects[message.panel]; if (!panelView || !(panelView instanceof ExtensionServerPanelView)) { return this._status.E_NOTFOUND(message.panel); } const button = new ExtensionButton( this, message.id, this._expandResourcePath(port[extensionOriginSymbol], message.icon), message.tooltip, message.disabled); this._clientObjects[message.id] = button; panelView.widget().then(appendButton); /** * @param {!UI.Widget.Widget} panel */ function appendButton(panel) { /** @type {!ExtensionPanel} panel*/ (panel).addToolbarItem(button.toolbarButton()); } return this._status.OK(); } _onUpdateButton(message, port) { const button = this._clientObjects[message.id]; if (!button || !(button instanceof ExtensionButton)) { return this._status.E_NOTFOUND(message.id); } button.update( this._expandResourcePath(port[extensionOriginSymbol], message.icon), message.tooltip, message.disabled); return this._status.OK(); } /** * @param {!Object} message */ _onCompleteTraceSession(message) { const session = this._traceSessions.get(message.id); if (!session) { return this._status.E_NOTFOUND(message.id); } this._traceSessions.delete(message.id); session.complete(message.url, message.timeOffset); } _onCreateSidebarPane(message) { if (message.panel !== 'elements' && message.panel !== 'sources') { return this._status.E_NOTFOUND(message.panel); } const id = message.id; const sidebar = new ExtensionSidebarPane(this, message.panel, message.title, id); this._sidebarPanes.push(sidebar); this._clientObjects[id] = sidebar; this.dispatchEventToListeners(Events.SidebarPaneAdded, sidebar); return this._status.OK(); } /** * @return {!Array.<!ExtensionSidebarPane>} */ sidebarPanes() { return this._sidebarPanes; } _onSetSidebarHeight(message) { const sidebar = this._clientObjects[message.id]; if (!sidebar) { return this._status.E_NOTFOUND(message.id); } sidebar.setHeight(message.height); return this._status.OK(); } _onSetSidebarContent(message, port) { const sidebar = this._clientObjects[message.id]; if (!sidebar) { return this._status.E_NOTFOUND(message.id); } /** * @this {ExtensionServer} */ function callback(error) { const result = error ? this._status.E_FAILED(error) : this._status.OK(); this._dispatchCallback(message.requestId, port, result); } if (message.evaluateOnPage) { return sidebar.setExpression( message.expression, message.rootTitle, message.evaluateOptions, port[extensionOriginSymbol], callback.bind(this)); } sidebar.setObject(message.expression, message.rootTitle, callback.bind(this)); } _onSetSidebarPage(message, port) { const sidebar = this._clientObjects[message.id]; if (!sidebar) { return this._status.E_NOTFOUND(message.id); } sidebar.setPage(this._expandResourcePath(port[extensionOriginSymbol], message.page)); } _onOpenResource(message) { const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(message.url); if (uiSourceCode) { Common.Revealer.reveal(uiSourceCode.uiLocation(message.lineNumber, 0)); return this._status.OK(); } const resource = Bindings.ResourceUtils.resourceForURL(message.url); if (resource) { Common.Revealer.reveal(resource); return this._status.OK(); } const request = SDK.NetworkLog.NetworkLog.instance().requestForURL(message.url); if (request) { Common.Revealer.reveal(request); return this._status.OK(); } return this._status.E_NOTFOUND(message.url); } _onSetOpenResourceHandler(message, port) { const name = this._registeredExtensions.get(port[extensionOriginSymbol]).name; if (message.handlerPresent) { Components.Linkifier.Linkifier.registerLinkHandler(name, this._handleOpenURL.bind(this, port)); } else { Components.Linkifier.Linkifier.unregisterLinkHandler(name); } } _handleOpenURL(port, contentProvider, lineNumber) { port.postMessage( {command: 'open-resource', resource: this._makeResource(contentProvider), lineNumber: lineNumber + 1}); } _onReload(message) { const options = /** @type {!ExtensionReloadOptions} */ (message.options || {}); SDK.NetworkManager.MultitargetNetworkManager.instance().setUserAgentOverride( typeof options.userAgent === 'string' ? options.userAgent : '', null); let injectedScript; if (options.injectedScript) { injectedScript = '(function(){' + options.injectedScript + '})()'; } SDK.ResourceTreeModel.ResourceTreeModel.reloadAllPages(Boolean(options.ignoreCache), injectedScript); return this._status.OK(); } _onEvaluateOnInspectedPage(message, port) { /** * @param {?ProtocolClient.InspectorBackend.ProtocolError} error * @param {?SDK.RemoteObject.RemoteObject} object * @param {boolean} wasThrown * @this {ExtensionServer} */ function callback(error, object, wasThrown) { let result; if (error || !object) { result = this._status.E_PROTOCOLERROR(error.toString()); } else if (wasThrown) { result = {isException: true, value: object.description}; } else { result = {value: object.value}; } this._dispatchCallback(message.requestId, port, result); } return this.evaluate( message.expression, true, true, message.evaluateOptions, port[extensionOriginSymbol], callback.bind(this)); } async _onGetHAR() { const requests = SDK.NetworkLog.NetworkLog.instance().requests(); const harLog = await SDK.HARLog.HARLog.build(requests); for (let i = 0; i < harLog.entries.length; ++i) { harLog.entries[i]._requestId = this._requestId(requests[i]); } return harLog; } /** * @param {!TextUtils.ContentProvider.ContentProvider} contentProvider */ _makeResource(contentProvider) { return {url: contentProvider.contentURL(), type: contentProvider.contentType().name()}; } /** * @return {!Array<!TextUtils.ContentProvider.ContentProvider>} */ _onGetPageResources() { /** @type {!Map<string, !TextUtils.ContentProvider.ContentProvider>} */ const resources = new Map(); /** * @this {ExtensionServer} */ function pushResourceData(contentProvider) { if (!resources.has(contentProvider.contentURL())) { resources.set(contentProvider.contentURL(), this._makeResource(contentProvider)); } } let uiSourceCodes = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodesForProjectType( Workspace.Workspace.projectTypes.Network); uiSourceCodes = uiSourceCodes.concat(Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodesForProjectType( Workspace.Workspace.projectTypes.ContentScripts)); uiSourceCodes.forEach(pushResourceData.bind(this)); for (const resourceTreeModel of SDK.SDKModel.TargetManager.instance().models( SDK.ResourceTreeModel.ResourceTreeModel)) { resourceTreeModel.forAllResources(pushResourceData.bind(this)); } return [...resources.values()]; } /** * @param {!TextUtils.ContentProvider.ContentProvider} contentProvider * @param {!Object} message * @param {!MessagePort} port */ async _getResourceContent(contentProvider, message, port) { const {content} = await contentProvider.requestContent(); const encoded = await contentProvider.contentEncoded(); this._dispatchCallback(message.requestId, port, {encoding: encoded ? 'base64' : '', content: content}); } _onGetRequestContent(message, port) { const request = this._requestById(message.id); if (!request) { return this._status.E_NOTFOUND(message.id); } this._getResourceContent(request, message, port); } _onGetResourceContent(message, port) { const url = /** @type {string} */ (message.url); const contentProvider = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url) || Bindings.ResourceUtils.resourceForURL(url); if (!contentProvider) { return this._status.E_NOTFOUND(url); } this._getResourceContent(contentProvider, message, port); } _onSetResourceContent(message, port) { /** * @param {?ProtocolClient.InspectorBackend.ProtocolError} error * @this {ExtensionServer} */ function callbackWrapper(error) { const response = error ? this._status.E_FAILED(error) : this._status.OK(); this._dispatchCallback(message.requestId, port, response); } const url = /** @type {string} */ (message.url); const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url); if (!uiSourceCode || !uiSourceCode.contentType().isDocumentOrScriptOrStyleSheet()) { const resource = SDK.ResourceTreeModel.ResourceTreeModel.resourceForURL(url); if (!resource) { return this._status.E_NOTFOUND(url); } return this._status.E_NOTSUPPORTED('Resource is not editable'); } uiSourceCode.setWorkingCopy(message.content); if (message.commit) { uiSourceCode.commitWorkingCopy(); } callbackWrapper.call(this, null); } _requestId(request) { if (!request._extensionRequestId) { request._extensionRequestId = ++this._lastRequestId; this._requests[request._extensionRequestId] = request; } return request._extensionRequestId; } _requestById(id) { return this._requests[id]; } /** * @param {!Object} message * @param {!MessagePort} port */ _onAddTraceProvider(message, port) { const provider = new ExtensionTraceProvider( port[extensionOriginSymbol], message.id, message.categoryName, message.categoryTooltip); this._clientObjects[message.id] = provider; this._traceProviders.push(provider); this.dispatchEventToListeners(Events.TraceProviderAdded, provider); } /** * @return {!Array<!ExtensionTraceProvider>} */ traceProviders() { return this._traceProviders; } _onForwardKeyboardEvent(message) { message.entries.forEach(handleEventEntry); /** * @param {*} entry */ function handleEventEntry(entry) { // Fool around closure compiler -- it has its own notion of both KeyboardEvent constructor // and initKeyboardEvent methods and overriding these in externs.js does not have effect. const event = new window.KeyboardEvent(entry.eventType, { key: entry.key, code: entry.code, keyCode: entry.keyCode, location: entry.location, ctrlKey: entry.ctrlKey, altKey: entry.altKey, shiftKey: entry.shiftKey, metaKey: entry.metaKey }); event.__keyCode = keyCodeForEntry(entry); document.dispatchEvent(event); } function keyCodeForEntry(entry) { let keyCode = entry.keyCode; if (!keyCode) { // This is required only for synthetic events (e.g. dispatched in tests). if (entry.key === 'Escape') { keyCode = 27; } } return keyCode || 0; } } _dispatchCallback(requestId, port, result) { if (requestId) { port.postMessage({command: 'callback', requestId: requestId, result: result}); } } _initExtensions() { this._registerAutosubscriptionHandler( Extensions.extensionAPI.Events.ResourceAdded, Workspace.Workspace.WorkspaceImpl.instance(), Workspace.Workspace.Events.UISourceCodeAdded, this._notifyResourceAdded); this._registerAutosubscriptionTargetManagerHandler( Extensions.extensionAPI.Events.NetworkRequestFinished, SDK.NetworkManager.NetworkManager, SDK.NetworkManager.Events.RequestFinished, this._notifyRequestFinished); /** * @this {ExtensionServer} */ function onElementsSubscriptionStarted() { UI.Context.Context.instance().addFlavorChangeListener( SDK.DOMModel.DOMNode, this._notifyElementsSelectionChanged, this); } /** * @this {ExtensionServer} */ function onElementsSubscriptionStopped() { UI.Context.Context.instance().removeFlavorChangeListener( SDK.DOMModel.DOMNode, this._notifyElementsSelectionChanged, this); } this._registerSubscriptionHandler( Extensions.extensionAPI.Events.PanelObjectSelected + 'elements', onElementsSubscriptionStarted.bind(this), onElementsSubscriptionStopped.bind(this)); this._registerResourceContentCommittedHandler(this._notifyUISourceCodeContentCommitted); SDK.SDKModel.TargetManager.instance().addEventListener( SDK.SDKModel.Events.InspectedURLChanged, this._inspectedURLChanged, this); } _notifyResourceAdded(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode.UISourceCode} */ (event.data); this._postNotification(Extensions.extensionAPI.Events.ResourceAdded, this._makeResource(uiSourceCode)); } _notifyUISourceCodeContentCommitted(event) { const uiSourceCode = /** @type {!Workspace.UISourceCode.UISourceCode} */ (event.data.uiSourceCode); const content = /** @type {string} */ (event.data.content); this._postNotification( Extensions.extensionAPI.Events.ResourceContentCommitted, this._makeResource(uiSourceCode), content); } async _notifyRequestFinished(event) { const request = /** @type {!SDK.NetworkRequest.NetworkRequest} */ (event.data); const entry = await SDK.HARLog.Entry.build(request); this._postNotification(Extensions.extensionAPI.Events.NetworkRequestFinished, this._requestId(request), entry); } _notifyElementsSelectionChanged() { this._postNotification(Extensions.extensionAPI.Events.PanelObjectSelected + 'elements'); } /** * @param {string} url * @param {!TextUtils.TextRange.TextRange} range */ sourceSelectionChanged(url, range) { this._postNotification(Extensions.extensionAPI.Events.PanelObjectSelected + 'sources', { startLine: range.startLine, startColumn: range.startColumn, endLine: range.endLine, endColumn: range.endColumn, url: url, }); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _setInspectedTabId(event) { this._inspectedTabId = /** @type {string} */ (event.data); } /** * @param {!Root.Runtime.RuntimeExtensionDescriptor} extensionInfo */ _addExtension(extensionInfo) { const startPage = extensionInfo.startPage; const inspectedURL = SDK.SDKModel.TargetManager.instance().mainTarget().inspectedURL(); if (inspectedURL !== '' && !this._canInspectURL(inspectedURL)) { this._disableExtensions(); } if (!this._extensionsEnabled) { return; } try { const startPageURL = new URL(/** @type {string} */ (startPage)); const extensionOrigin = startPageURL.origin; if (!this._registeredExtensions.get(extensionOrigin)) { // See ExtensionAPI.js for details. const injectedAPI = self.buildExtensionAPIInjectedScript( /** @type {!{startPage: string, name: string, exposeExperimentalAPIs: boolean}} */ (extensionInfo), this._inspectedTabId, ThemeSupport.ThemeSupport.instance().themeName(), UI.ShortcutRegistry.ShortcutRegistry.instance().globalShortcutKeys(), ExtensionServer.instance()['_extensionAPITestHook']); Host.InspectorFrontendHost.InspectorFrontendHostInstance.setInjectedScriptForOrigin( extensionOrigin, injectedAPI); const name = extensionInfo.name || `Extension ${extensionOrigin}`; this._registeredExtensions.set(extensionOrigin, {name}); } const iframe = createElement('iframe'); iframe.src = startPage; iframe.dataset.devtoolsExtension = extensionInfo.name; iframe.style.display = 'none'; document.body.appendChild(iframe); // Only for main window. } catch (e) { console.error('Failed to initialize extension ' + startPage + ':' + e); return false; } return true; } _registerExtension(origin, port) { if (!this._registeredExtensions.has(origin)) { if (origin !== window.location.origin) { // Just ignore inspector frames. console.error('Ignoring unauthorized client request from ' + origin); } return; } port[extensionOriginSymbol] = origin; port.addEventListener('message', this._onmessage.bind(this), false); port.start(); } _onWindowMessage(event) { if (event.data === 'registerExtension') { this._registerExtension(event.origin, event.ports[0]); } } async _onmessage(event) { const message = event.data; let result; if (!(message.command in this._handlers)) { result = this._status.E_NOTSUPPORTED(message.command); } else if (!this._extensionsEnabled) { result = this._status.E_FAILED('Permission denied'); } else { result = await this._handlers[message.command](message, event.target); } if (result && message.requestId) { this._dispatchCallback(message.requestId, event.target, result); } } _registerHandler(command, callback) { console.assert(command); this._handlers[command] = callback; } _registerSubscriptionHandler(eventTopic, onSubscribeFirst, onUnsubscribeLast) { this._subscriptionStartHandlers[eventTopic] = onSubscribeFirst; this._subscriptionStopHandlers[eventTopic] = onUnsubscribeLast; } /** * @param {string} eventTopic * @param {!Object} eventTarget * @param {symbol} frontendEventType * @param {function(!Common.EventTarget.EventTargetEvent)} handler */ _registerAutosubscriptionHandler(eventTopic, eventTarget, frontendEventType, handler) { this._registerSubscriptionHandler( eventTopic, eventTarget.addEventListener.bind(eventTarget, frontendEventType, handler, this), eventTarget.removeEventListener.bind(eventTarget, frontendEventType, handler, this)); } /** * @param {string} eventTopic * @param {!Function} modelClass * @param {symbol} frontendEventType * @param {function(!Common.EventTarget.EventTargetEvent)} handler */ _registerAutosubscriptionTargetManagerHandler(eventTopic, modelClass, frontendEventType, handler) { this._registerSubscriptionHandler( eventTopic, SDK.SDKModel.TargetManager.instance().addModelListener.bind( SDK.SDKModel.TargetManager.instance(), modelClass, frontendEventType, handler, this), SDK.SDKModel.TargetManager.instance().removeModelListener.bind( SDK.SDKModel.TargetManager.instance(), modelClass, frontendEventType, handler, this)); } _registerResourceContentCommittedHandler(handler) { /** * @this {ExtensionServer} */ function addFirstEventListener() { Workspace.Workspace.WorkspaceImpl.instance().addEventListener( Workspace.Workspace.Events.WorkingCopyCommittedByUser, handler, this); Workspace.Workspace.WorkspaceImpl.instance().setHasResourceContentTrackingExtensions(true); } /** * @this {ExtensionServer} */ function removeLastEventListener() { Workspace.Workspace.WorkspaceImpl.instance().setHasResourceContentTrackingExtensions(false); Workspace.Workspace.WorkspaceImpl.instance().removeEventListener( Workspace.Workspace.Events.WorkingCopyCommittedByUser, handler, this); } this._registerSubscriptionHandler( Extensions.extensionAPI.Events.ResourceContentCommitted, addFirstEventListener.bind(this), removeLastEventListener.bind(this)); } _expandResourcePath(extensionPath, resourcePath) { if (!resourcePath) { return; } return extensionPath + this._normalizePath(resourcePath); } _normalizePath(path) { const source = path.split('/'); const result = []; for (let i = 0; i < source.length; ++i) { if (source[i] === '.') { continue; } // Ignore empty path components resulting from //, as well as a leading and traling slashes. if (source[i] === '') { continue; } if (source[i] === '..') { result.pop(); } else { result.push(source[i]); } } return '/' + result.join('/'); } /** * @param {string} expression * @param {boolean} exposeCommandLineAPI * @param {boolean} returnByValue * @param {?Object} options * @param {string} securityOrigin * @param {function(?string, ?SDK.RemoteObject.RemoteObject, boolean)} callback * @return {!Record|undefined} */ evaluate(expression, exposeCommandLineAPI, returnByValue, options, securityOrigin, callback) { let context; /** * @param {string} url * @return {boolean} */ function resolveURLToFrame(url) { let found; function hasMatchingURL(frame) { found = (frame.url === url) ? frame : null; return found; } SDK.ResourceTreeModel.ResourceTreeModel.frames().some(hasMatchingURL); return found; } options = options || {}; let frame; if (options.frameURL) { frame = resolveURLToFrame(options.frameURL); } else { const target = SDK.SDKModel.TargetManager.instance().mainTarget(); const resourceTreeModel = target && target.model(SDK.ResourceTreeModel.ResourceTreeModel); frame = resourceTreeModel && resourceTreeModel.mainFrame; } if (!frame) { if (options.frameURL) { console.warn('evaluate: there is no frame with URL ' + options.frameURL); } else { console.warn('evaluate: the main frame is not yet available'); } return this._status.E_NOTFOUND(options.frameURL || '<top>'); } // We shouldn't get here if the top frame can't be inspected by an extension, but // let's double check for subframes. if (!this._canInspectURL(frame.url)) { return this._status.E_FAILED('Permission denied'); } let contextSecurityOrigin; if (options.useContentScriptContext) { contextSecurityOrigin = securityOrigin; } else if (options.scriptExecutionContext) { contextSecurityOrigin = options.scriptExecutionContext; } const runtimeModel = frame.resourceTreeModel().target().model(SDK.RuntimeModel.RuntimeModel); const executionContexts = runtimeModel ? runtimeModel.executionContexts() : []; if (contextSecurityOrigin) { for (let i = 0; i < executionContexts.length; ++i) { const executionContext = executionContexts[i]; if (executionContext.frameId === frame.id && executionContext.origin === contextSecurityOrigin && !executionContext.isDefault) { context = executionContext; } } if (!context) { console.warn('The JavaScript context ' + contextSecurityOrigin + ' was not found in the frame ' + frame.url); return this._status.E_NOTFOUND(contextSecurityOrigin); } } else { for (let i = 0; i < executionContexts.length; ++i) { const executionContext = executionContexts[i]; if (executionContext.frameId === frame.id && executionContext.isDefault) { context = executionContext; } } if (!context) { return this._status.E_FAILED(frame.url + ' has no execution context'); } } if (!this._canInspectURL(context.origin)) { return this._status.E_FAILED('Permission denied'); } context .evaluate( { expression: expression, objectGroup: 'extension', includeCommandLineAPI: exposeCommandLineAPI, silent: true, returnByValue: returnByValue, generatePreview: false }, /* userGesture */ false, /* awaitPromise */ false) .then(onEvaluate); /** * @param {!SDK.RuntimeModel.EvaluationResult} result */ function onEvaluate(result) { if (result.error) { callback(result.error, null, false); return; } callback(null, result.object || null, Boolean(result.exceptionDetails)); } } /** * * @param {string} url */ _canInspectURL(url) { let parsedURL; // This is only to work around invalid URLs we're occasionally getting from some tests. // TODO(caseq): make sure tests supply valid URLs or we specifically handle invalid ones. try { parsedURL = new URL(url); } catch (exception) { return false; } if (kAllowedOrigins.includes(parsedURL.origin)) { return true; } if (parsedURL.protocol === 'chrome:' || parsedURL.protocol === 'devtools:') { return false; } if (parsedURL.protocol.startsWith('http') && parsedURL.hostname === 'chrome.google.com' && parsedURL.pathname.startsWith('/webstore')) { return false; } return true; } _disableExtensions() { this._extensionsEnabled = false; } } /** @enum {symbol} */ export const Events = { SidebarPaneAdded: Symbol('SidebarPaneAdded'), TraceProviderAdded: Symbol('TraceProviderAdded') }; class ExtensionServerPanelView extends UI.View.SimpleView { /** * @param {string} name * @param {string} title * @param {!UI.Panel.Panel} panel */ constructor(name, title, panel) { super(title); this._name = name; this._panel = panel; } /** * @override * @return {string} */ viewId() { return this._name; } /** * @override * @return {!Promise.<!UI.Widget.Widget>} */ widget() { return /** @type {!Promise.<!UI.Widget.Widget>} */ (Promise.resolve(this._panel)); } } export class ExtensionStatus { constructor() { /** * @param {string} code * @param {string} description * @return {!Record} */ function makeStatus(code, description) { const details = Array.prototype.slice.call(arguments, 2); const status = {code: code, description: description, details: details}; if (code !== 'OK') { status.isError = true; console.error('Extension server error: ' + Platform.StringUtilities.vsprintf(description, details)); } return status; } this.OK = makeStatus.bind(null, 'OK', 'OK'); this.E_EXISTS = makeStatus.bind(null, 'E_EXISTS', 'Object already exists: %s'); this.E_BADARG = makeStatus.bind(null, 'E_BADARG', 'Invalid argument %s: %s'); this.E_BADARGTYPE = makeStatus.bind(null, 'E_BADARGTYPE', 'Invalid type for argument %s: got %s, expected %s'); this.E_NOTFOUND = makeStatus.bind(null, 'E_NOTFOUND', 'Object not found: %s'); this.E_NOTSUPPORTED = makeStatus.bind(null, 'E_NOTSUPPORTED', 'Object does not support requested operation: %s'); this.E_PROTOCOLERROR = makeStatus.bind(null, 'E_PROTOCOLERROR', 'Inspector protocol error: %s'); this.E_FAILED = makeStatus.bind(null, 'E_FAILED', 'Operation failed: %s'); } } /** * @typedef {{code: string, description: string, details: !Array.<*>}} */ export let Record;