| /* |
| * 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. |
| */ |
| |
| /** |
| * @unrestricted |
| */ |
| export default class ExtensionServer extends Common.Object { |
| /** |
| * @suppressGlobalPropertiesCheck |
| */ |
| constructor() { |
| super(); |
| this._clientObjects = {}; |
| this._handlers = {}; |
| this._subscribers = {}; |
| this._subscriptionStartHandlers = {}; |
| this._subscriptionStopHandlers = {}; |
| this._extraHeaders = {}; |
| this._requests = {}; |
| this._lastRequestId = 0; |
| this._registeredExtensions = {}; |
| this._status = new ExtensionStatus(); |
| /** @type {!Array<!Extensions.ExtensionSidebarPane>} */ |
| this._sidebarPanes = []; |
| /** @type {!Array<!Extensions.ExtensionTraceProvider>} */ |
| this._traceProviders = []; |
| /** @type {!Map<string, !Extensions.TracingSession>} */ |
| this._traceSessions = new Map(); |
| |
| 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)); |
| window.addEventListener('message', this._onWindowMessage.bind(this), false); // Only for main window. |
| |
| /** @suppress {checkTypes} */ |
| const existingTabId = |
| window.DevToolsAPI && window.DevToolsAPI.getInspectedTabId && window.DevToolsAPI.getInspectedTabId(); |
| |
| if (existingTabId) { |
| this._setInspectedTabId({data: existingTabId}); |
| } |
| Host.InspectorFrontendHost.events.addEventListener( |
| Host.InspectorFrontendHostAPI.Events.SetInspectedTabId, this._setInspectedTabId, this); |
| |
| this._initExtensions(); |
| } |
| |
| initializeExtensions() { |
| Host.InspectorFrontendHost.setAddExtensionCallback(this._addExtension.bind(this)); |
| } |
| |
| /** |
| * @return {boolean} |
| */ |
| hasExtensions() { |
| return !!Object.keys(this._registeredExtensions).length; |
| } |
| |
| /** |
| * @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); |
| } |
| |
| _inspectedURLChanged(event) { |
| if (event.data !== SDK.targetManager.mainTarget()) { |
| return; |
| } |
| this._requests = {}; |
| const url = event.data.inspectedURL(); |
| this._postNotification(Extensions.extensionAPI.Events.InspectedURLChanged, url); |
| } |
| |
| /** |
| * @param {string} providerId |
| * @param {string} sessionId |
| * @param {!Extensions.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[type]; |
| } |
| |
| /** |
| * @param {string} type |
| * @param {...*} vararg |
| */ |
| _postNotification(type, vararg) { |
| const subscribers = this._subscribers[type]; |
| if (!subscribers) { |
| return; |
| } |
| const message = {command: 'notify-' + type, arguments: Array.prototype.slice.call(arguments, 1)}; |
| for (let i = 0; i < subscribers.length; ++i) { |
| subscribers[i].postMessage(message); |
| } |
| } |
| |
| _onSubscribe(message, port) { |
| const subscribers = this._subscribers[message.type]; |
| if (subscribers) { |
| subscribers.push(port); |
| } else { |
| this._subscribers[message.type] = [port]; |
| if (this._subscriptionStartHandlers[message.type]) { |
| this._subscriptionStartHandlers[message.type](); |
| } |
| } |
| } |
| |
| _onUnsubscribe(message, port) { |
| const subscribers = this._subscribers[message.type]; |
| if (!subscribers) { |
| return; |
| } |
| subscribers.remove(port); |
| if (!subscribers.length) { |
| delete this._subscribers[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[id]; |
| if (!extensionHeaders) { |
| extensionHeaders = {}; |
| this._extraHeaders[id] = extensionHeaders; |
| } |
| for (const name in message.headers) { |
| extensionHeaders[name] = message.headers[name]; |
| } |
| const allHeaders = /** @type {!Protocol.Network.Headers} */ ({}); |
| for (const extension in this._extraHeaders) { |
| const headers = this._extraHeaders[extension]; |
| for (const name in headers) { |
| if (typeof headers[name] === 'string') { |
| allHeaders[name] = headers[name]; |
| } |
| } |
| } |
| |
| SDK.multitargetNetworkManager.setExtraHTTPHeaders(allHeaders); |
| } |
| |
| /** |
| * @param {*} message |
| * @suppressGlobalPropertiesCheck |
| */ |
| _onApplyStyleSheet(message) { |
| if (!Root.Runtime.experiments.isEnabled('applyCustomStylesheet')) { |
| return; |
| } |
| const styleSheet = createElement('style'); |
| styleSheet.textContent = message.styleSheet; |
| document.head.appendChild(styleSheet); |
| |
| UI.themeSupport.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) { |
| UI.themeSupport.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.hasPanel(id)) { |
| return this._status.E_EXISTS(id); |
| } |
| |
| const page = this._expandResourcePath(port._extensionOrigin, message.page); |
| let persistentId = port._extensionOrigin + message.title; |
| persistentId = persistentId.replace(/\s/g, ''); |
| const panelView = new ExtensionServerPanelView( |
| persistentId, message.title, new Extensions.ExtensionPanel(this, persistentId, id, page)); |
| this._clientObjects[id] = panelView; |
| UI.inspectorView.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.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 Extensions.ExtensionButton( |
| this, message.id, this._expandResourcePath(port._extensionOrigin, message.icon), message.tooltip, |
| message.disabled); |
| this._clientObjects[message.id] = button; |
| |
| panelView.widget().then(appendButton); |
| |
| /** |
| * @param {!UI.Widget} panel |
| */ |
| function appendButton(panel) { |
| /** @type {!Extensions.ExtensionPanel} panel*/ (panel).addToolbarItem(button.toolbarButton()); |
| } |
| |
| return this._status.OK(); |
| } |
| |
| _onUpdateButton(message, port) { |
| const button = this._clientObjects[message.id]; |
| if (!button || !(button instanceof Extensions.ExtensionButton)) { |
| return this._status.E_NOTFOUND(message.id); |
| } |
| button.update(this._expandResourcePath(port._extensionOrigin, 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 Extensions.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.<!Extensions.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._extensionOrigin, 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._extensionOrigin, message.page)); |
| } |
| |
| _onOpenResource(message) { |
| const uiSourceCode = Workspace.workspace.uiSourceCodeForURL(message.url); |
| if (uiSourceCode) { |
| Common.Revealer.reveal(uiSourceCode.uiLocation(message.lineNumber, 0)); |
| return this._status.OK(); |
| } |
| |
| const resource = Bindings.resourceForURL(message.url); |
| if (resource) { |
| Common.Revealer.reveal(resource); |
| return this._status.OK(); |
| } |
| |
| const request = SDK.networkLog.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[port._extensionOrigin].name || ('Extension ' + port._extensionOrigin); |
| if (message.handlerPresent) { |
| Components.Linkifier.registerLinkHandler(name, this._handleOpenURL.bind(this, port)); |
| } else { |
| Components.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.multitargetNetworkManager.setUserAgentOverride(typeof options.userAgent === 'string' ? options.userAgent : ''); |
| let injectedScript; |
| if (options.injectedScript) { |
| injectedScript = '(function(){' + options.injectedScript + '})()'; |
| } |
| SDK.ResourceTreeModel.reloadAllPages(!!options.ignoreCache, injectedScript); |
| return this._status.OK(); |
| } |
| |
| _onEvaluateOnInspectedPage(message, port) { |
| /** |
| * @param {?Protocol.Error} error |
| * @param {?SDK.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._extensionOrigin, callback.bind(this)); |
| } |
| |
| async _onGetHAR() { |
| const requests = SDK.networkLog.requests(); |
| const harLog = await SDK.HARLog.build(requests); |
| for (let i = 0; i < harLog.entries.length; ++i) { |
| harLog.entries[i]._requestId = this._requestId(requests[i]); |
| } |
| return harLog; |
| } |
| |
| /** |
| * @param {!Common.ContentProvider} contentProvider |
| */ |
| _makeResource(contentProvider) { |
| return {url: contentProvider.contentURL(), type: contentProvider.contentType().name()}; |
| } |
| |
| /** |
| * @return {!Array<!Common.ContentProvider>} |
| */ |
| _onGetPageResources() { |
| /** @type {!Map<string, !Common.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.uiSourceCodesForProjectType(Workspace.projectTypes.Network); |
| uiSourceCodes = |
| uiSourceCodes.concat(Workspace.workspace.uiSourceCodesForProjectType(Workspace.projectTypes.ContentScripts)); |
| uiSourceCodes.forEach(pushResourceData.bind(this)); |
| for (const resourceTreeModel of SDK.targetManager.models(SDK.ResourceTreeModel)) { |
| resourceTreeModel.forAllResources(pushResourceData.bind(this)); |
| } |
| return resources.valuesArray(); |
| } |
| |
| /** |
| * @param {!Common.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.uiSourceCodeForURL(url) || Bindings.resourceForURL(url); |
| if (!contentProvider) { |
| return this._status.E_NOTFOUND(url); |
| } |
| this._getResourceContent(contentProvider, message, port); |
| } |
| |
| _onSetResourceContent(message, port) { |
| /** |
| * @param {?Protocol.Error} 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.uiSourceCodeForURL(url); |
| if (!uiSourceCode || !uiSourceCode.contentType().isDocumentOrScriptOrStyleSheet()) { |
| const resource = SDK.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 Extensions.ExtensionTraceProvider( |
| port._extensionOrigin, message.id, message.categoryName, message.categoryTooltip); |
| this._clientObjects[message.id] = provider; |
| this._traceProviders.push(provider); |
| this.dispatchEventToListeners(Events.TraceProviderAdded, provider); |
| } |
| |
| /** |
| * @return {!Array<!Extensions.ExtensionTraceProvider>} |
| */ |
| traceProviders() { |
| return this._traceProviders; |
| } |
| |
| _onForwardKeyboardEvent(message) { |
| message.entries.forEach(handleEventEntry); |
| |
| /** |
| * @param {*} entry |
| * @suppressGlobalPropertiesCheck |
| */ |
| 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, Workspace.Workspace.Events.UISourceCodeAdded, |
| this._notifyResourceAdded); |
| this._registerAutosubscriptionTargetManagerHandler( |
| Extensions.extensionAPI.Events.NetworkRequestFinished, SDK.NetworkManager, |
| SDK.NetworkManager.Events.RequestFinished, this._notifyRequestFinished); |
| |
| /** |
| * @this {ExtensionServer} |
| */ |
| function onElementsSubscriptionStarted() { |
| UI.context.addFlavorChangeListener(SDK.DOMNode, this._notifyElementsSelectionChanged, this); |
| } |
| |
| /** |
| * @this {ExtensionServer} |
| */ |
| function onElementsSubscriptionStopped() { |
| UI.context.removeFlavorChangeListener(SDK.DOMNode, this._notifyElementsSelectionChanged, this); |
| } |
| |
| this._registerSubscriptionHandler( |
| Extensions.extensionAPI.Events.PanelObjectSelected + 'elements', onElementsSubscriptionStarted.bind(this), |
| onElementsSubscriptionStopped.bind(this)); |
| this._registerResourceContentCommittedHandler(this._notifyUISourceCodeContentCommitted); |
| |
| SDK.targetManager.addEventListener(SDK.TargetManager.Events.InspectedURLChanged, this._inspectedURLChanged, this); |
| } |
| |
| _notifyResourceAdded(event) { |
| const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); |
| this._postNotification(Extensions.extensionAPI.Events.ResourceAdded, this._makeResource(uiSourceCode)); |
| } |
| |
| _notifyUISourceCodeContentCommitted(event) { |
| const uiSourceCode = /** @type {!Workspace.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} */ (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} 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.Event} event |
| */ |
| _setInspectedTabId(event) { |
| this._inspectedTabId = /** @type {string} */ (event.data); |
| } |
| |
| /** |
| * @param {!ExtensionDescriptor} extensionInfo |
| * @suppressGlobalPropertiesCheck |
| */ |
| _addExtension(extensionInfo) { |
| const urlOriginRegExp = new RegExp('([^:]+:\/\/[^/]*)\/'); // Can't use regexp literal here, MinJS chokes on it. |
| const startPage = extensionInfo.startPage; |
| const name = extensionInfo.name; |
| |
| try { |
| const originMatch = urlOriginRegExp.exec(startPage); |
| if (!originMatch) { |
| console.error('Skipping extension with invalid URL: ' + startPage); |
| return false; |
| } |
| const extensionOrigin = originMatch[1]; |
| if (!this._registeredExtensions[extensionOrigin]) { |
| // See ExtensionAPI.js for details. |
| const injectedAPI = self.buildExtensionAPIInjectedScript( |
| extensionInfo, this._inspectedTabId, UI.themeSupport.themeName(), UI.shortcutRegistry.globalShortcutKeys(), |
| Extensions.extensionServer['_extensionAPITestHook']); |
| Host.InspectorFrontendHost.setInjectedScriptForOrigin(extensionOrigin, injectedAPI); |
| this._registeredExtensions[extensionOrigin] = {name: name}; |
| } |
| const iframe = createElement('iframe'); |
| iframe.src = startPage; |
| 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.hasOwnProperty(origin)) { |
| if (origin !== window.location.origin) // Just ignore inspector frames. |
| { |
| console.error('Ignoring unauthorized client request from ' + origin); |
| } |
| return; |
| } |
| port._extensionOrigin = 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 = await this._handlers[message.command](message, event.target); |
| } else { |
| result = this._status.E_NOTSUPPORTED(message.command); |
| } |
| |
| 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.Event)} 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.Event)} handler |
| */ |
| _registerAutosubscriptionTargetManagerHandler(eventTopic, modelClass, frontendEventType, handler) { |
| this._registerSubscriptionHandler( |
| eventTopic, |
| SDK.targetManager.addModelListener.bind(SDK.targetManager, modelClass, frontendEventType, handler, this), |
| SDK.targetManager.removeModelListener.bind(SDK.targetManager, modelClass, frontendEventType, handler, this)); |
| } |
| |
| _registerResourceContentCommittedHandler(handler) { |
| /** |
| * @this {ExtensionServer} |
| */ |
| function addFirstEventListener() { |
| Workspace.workspace.addEventListener(Workspace.Workspace.Events.WorkingCopyCommittedByUser, handler, this); |
| Workspace.workspace.setHasResourceContentTrackingExtensions(true); |
| } |
| |
| /** |
| * @this {ExtensionServer} |
| */ |
| function removeLastEventListener() { |
| Workspace.workspace.setHasResourceContentTrackingExtensions(false); |
| Workspace.workspace.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, boolean)} callback |
| * @return {!Extensions.ExtensionStatus.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.frames().some(hasMatchingURL); |
| return found; |
| } |
| |
| options = options || {}; |
| let frame; |
| if (options.frameURL) { |
| frame = resolveURLToFrame(options.frameURL); |
| } else { |
| const target = SDK.targetManager.mainTarget(); |
| const resourceTreeModel = target && target.model(SDK.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>'); |
| } |
| |
| let contextSecurityOrigin; |
| if (options.useContentScriptContext) { |
| contextSecurityOrigin = securityOrigin; |
| } else if (options.scriptExecutionContext) { |
| contextSecurityOrigin = options.scriptExecutionContext; |
| } |
| |
| const runtimeModel = frame.resourceTreeModel().target().model(SDK.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'); |
| } |
| } |
| |
| 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, !!result.exceptionDetails); |
| } |
| } |
| } |
| |
| /** @enum {symbol} */ |
| export const Events = { |
| SidebarPaneAdded: Symbol('SidebarPaneAdded'), |
| TraceProviderAdded: Symbol('TraceProviderAdded') |
| }; |
| |
| /** |
| * @unrestricted |
| */ |
| class ExtensionServerPanelView extends UI.SimpleView { |
| /** |
| * @param {string} name |
| * @param {string} title |
| * @param {!UI.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() { |
| return /** @type {!Promise.<!UI.Widget>} */ (Promise.resolve(this._panel)); |
| } |
| } |
| |
| /** |
| * @unrestricted |
| */ |
| export class ExtensionStatus { |
| constructor() { |
| /** |
| * @param {string} code |
| * @param {string} description |
| * @return {!Extensions.ExtensionStatus.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: ' + String.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'); |
| } |
| } |
| |
| /* Legacy exported object */ |
| self.Extensions = self.Extensions || {}; |
| |
| /* Legacy exported object */ |
| Extensions = Extensions || {}; |
| |
| /** @constructor */ |
| Extensions.ExtensionServer = ExtensionServer; |
| |
| /** @enum {symbol} */ |
| Extensions.ExtensionServer.Events = Events; |
| |
| /** @constructor */ |
| Extensions.ExtensionStatus = ExtensionStatus; |
| |
| /** |
| * @typedef {{code: string, description: string, details: !Array.<*>}} |
| */ |
| Extensions.ExtensionStatus.Record; |
| |
| /** @type {!ExtensionServer} */ |
| Extensions.extensionServer; |