UNPKG

chrome-devtools-frontend

Version:
537 lines (452 loc) • 17 kB
/* * Copyright (C) 2009 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. */ // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) /* eslint-disable @typescript-eslint/no-unused-vars */ import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import * as Root from '../root/root.js'; import { EventDescriptors, Events, type CanShowSurveyResult, type ContextMenuDescriptor, type EnumeratedHistogram, type EventTypes, type ExtensionDescriptor, type InspectorFrontendHostAPI, type LoadNetworkResourceResult, type ShowSurveyResult, type SyncInformation, } from './InspectorFrontendHostAPI.js'; import {streamWrite as resourceLoaderStreamWrite} from './ResourceLoader.js'; interface DecompressionStream extends GenericTransformStream { readonly format: string; } declare const DecompressionStream: { prototype: DecompressionStream, new (format: string): DecompressionStream, }; const UIStrings = { /** *@description Document title in Inspector Frontend Host of the DevTools window *@example {example.com} PH1 */ devtoolsS: 'DevTools - {PH1}', }; const str_ = i18n.i18n.registerUIStrings('core/host/InspectorFrontendHost.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const MAX_RECORDED_HISTOGRAMS_SIZE = 100; const OVERRIDES_FILE_SYSTEM_PATH = '/overrides' as Platform.DevToolsPath.RawPathString; export class InspectorFrontendHostStub implements InspectorFrontendHostAPI { readonly #urlsBeingSaved: Map<Platform.DevToolsPath.RawPathString|Platform.DevToolsPath.UrlString, string[]>; events!: Common.EventTarget.EventTarget<EventTypes>; #fileSystem: FileSystem|null = null; recordedEnumeratedHistograms: {actionName: EnumeratedHistogram, actionCode: number}[] = []; recordedPerformanceHistograms: {histogramName: string, duration: number}[] = []; constructor() { this.#urlsBeingSaved = new Map(); // Guard against errors should this file ever be imported at the top level // within a worker - in which case this constructor is run. If there's no // document, we can early exit. if (typeof document === 'undefined') { return; } function stopEventPropagation(this: InspectorFrontendHostAPI, event: KeyboardEvent): void { // Let browser handle Ctrl+/Ctrl- shortcuts in hosted mode. const zoomModifier = this.platform() === 'mac' ? event.metaKey : event.ctrlKey; if (zoomModifier && (event.key === '+' || event.key === '-')) { event.stopPropagation(); } } document.addEventListener('keydown', event => { stopEventPropagation.call(this, (event as KeyboardEvent)); }, true); } platform(): string { const userAgent = navigator.userAgent; if (userAgent.includes('Windows NT')) { return 'windows'; } if (userAgent.includes('Mac OS X')) { return 'mac'; } return 'linux'; } loadCompleted(): void { } bringToFront(): void { } closeWindow(): void { } setIsDocked(isDocked: boolean, callback: () => void): void { window.setTimeout(callback, 0); } showSurvey(trigger: string, callback: (arg0: ShowSurveyResult) => void): void { window.setTimeout(() => callback({surveyShown: false}), 0); } canShowSurvey(trigger: string, callback: (arg0: CanShowSurveyResult) => void): void { window.setTimeout(() => callback({canShowSurvey: false}), 0); } /** * Requests inspected page to be placed atop of the inspector frontend with specified bounds. */ setInspectedPageBounds(bounds: { x: number, y: number, width: number, height: number, }): void { } inspectElementCompleted(): void { } setInjectedScriptForOrigin(origin: string, script: string): void { } inspectedURLChanged(url: Platform.DevToolsPath.UrlString): void { document.title = i18nString(UIStrings.devtoolsS, {PH1: url.replace(/^https?:\/\//, '')}); } copyText(text: string|null|undefined): void { if (text === undefined || text === null) { return; } void navigator.clipboard.writeText(text); } openInNewTab(url: Platform.DevToolsPath.UrlString): void { window.open(url, '_blank'); } showItemInFolder(fileSystemPath: Platform.DevToolsPath.RawPathString): void { Common.Console.Console.instance().error( 'Show item in folder is not enabled in hosted mode. Please inspect using chrome://inspect'); } save(url: Platform.DevToolsPath.RawPathString|Platform.DevToolsPath.UrlString, content: string, forceSaveAs: boolean): void { let buffer = this.#urlsBeingSaved.get(url); if (!buffer) { buffer = []; this.#urlsBeingSaved.set(url, buffer); } buffer.push(content); this.events.dispatchEventToListeners(Events.SavedURL, {url, fileSystemPath: url}); } append(url: Platform.DevToolsPath.RawPathString|Platform.DevToolsPath.UrlString, content: string): void { const buffer = this.#urlsBeingSaved.get(url); if (buffer) { buffer.push(content); this.events.dispatchEventToListeners(Events.AppendedToURL, url); } } close(url: Platform.DevToolsPath.RawPathString|Platform.DevToolsPath.UrlString): void { const buffer = this.#urlsBeingSaved.get(url) || []; this.#urlsBeingSaved.delete(url); let fileName = ''; if (url) { try { const trimmed = Platform.StringUtilities.trimURL(url); fileName = Platform.StringUtilities.removeURLFragment(trimmed); } catch (error) { // If url is not a valid URL, it is probably a filename. fileName = url; } } const link = document.createElement('a'); link.download = fileName; const blob = new Blob([buffer.join('')], {type: 'text/plain'}); const blobUrl = URL.createObjectURL(blob); link.href = blobUrl; link.click(); URL.revokeObjectURL(blobUrl); } sendMessageToBackend(message: string): void { } recordEnumeratedHistogram(actionName: EnumeratedHistogram, actionCode: number, bucketSize: number): void { if (this.recordedEnumeratedHistograms.length >= MAX_RECORDED_HISTOGRAMS_SIZE) { this.recordedEnumeratedHistograms.shift(); } this.recordedEnumeratedHistograms.push({actionName, actionCode}); } recordPerformanceHistogram(histogramName: string, duration: number): void { if (this.recordedPerformanceHistograms.length >= MAX_RECORDED_HISTOGRAMS_SIZE) { this.recordedPerformanceHistograms.shift(); } this.recordedPerformanceHistograms.push({histogramName, duration}); } recordUserMetricsAction(umaName: string): void { } requestFileSystems(): void { this.events.dispatchEventToListeners(Events.FileSystemsLoaded, []); } addFileSystem(type?: string): void { const onFileSystem = (fs: FileSystem): void => { this.#fileSystem = fs; const fileSystem = { fileSystemName: 'sandboxedRequestedFileSystem', fileSystemPath: OVERRIDES_FILE_SYSTEM_PATH, rootURL: 'filesystem:devtools://devtools/isolated/', type: 'overrides', }; this.events.dispatchEventToListeners(Events.FileSystemAdded, {fileSystem}); }; window.webkitRequestFileSystem(window.TEMPORARY, 1024 * 1024, onFileSystem); } removeFileSystem(fileSystemPath: Platform.DevToolsPath.RawPathString): void { const removalCallback = (entries: Entry[]): void => { entries.forEach(entry => { if (entry.isDirectory) { (entry as DirectoryEntry).removeRecursively(() => {}); } else if (entry.isFile) { entry.remove(() => {}); } }); }; if (this.#fileSystem) { this.#fileSystem.root.createReader().readEntries(removalCallback); } this.#fileSystem = null; this.events.dispatchEventToListeners(Events.FileSystemRemoved, OVERRIDES_FILE_SYSTEM_PATH); } isolatedFileSystem(fileSystemId: string, registeredName: string): FileSystem|null { return this.#fileSystem; } loadNetworkResource( url: string, headers: string, streamId: number, callback: (arg0: LoadNetworkResourceResult) => void): void { // Read the first 3 bytes looking for the gzip signature in the file header function isGzip(ab: ArrayBuffer): boolean { const buf = new Uint8Array(ab); if (!buf || buf.length < 3) { return false; } // https://www.rfc-editor.org/rfc/rfc1952#page-6 return buf[0] === 0x1F && buf[1] === 0x8B && buf[2] === 0x08; } fetch(url) .then(async result => { const resultArrayBuf = await result.arrayBuffer(); let decoded: ReadableStream|ArrayBuffer = resultArrayBuf; if (isGzip(resultArrayBuf)) { const ds = new DecompressionStream('gzip'); const writer = ds.writable.getWriter(); void writer.write(resultArrayBuf); void writer.close(); decoded = ds.readable; } const text = await new Response(decoded).text(); return text; }) .then(function(text) { resourceLoaderStreamWrite(streamId, text); callback({ statusCode: 200, headers: undefined, messageOverride: undefined, netError: undefined, netErrorName: undefined, urlValid: undefined, }); }) .catch(function() { callback({ statusCode: 404, headers: undefined, messageOverride: undefined, netError: undefined, netErrorName: undefined, urlValid: undefined, }); }); } registerPreference(name: string, options: {synced?: boolean}): void { } getPreferences(callback: (arg0: { [x: string]: string, }) => void): void { const prefs: { [x: string]: string, } = {}; for (const name in window.localStorage) { prefs[name] = window.localStorage[name]; } callback(prefs); } getPreference(name: string, callback: (arg0: string) => void): void { callback(window.localStorage[name]); } setPreference(name: string, value: string): void { window.localStorage[name] = value; } removePreference(name: string): void { delete window.localStorage[name]; } clearPreferences(): void { window.localStorage.clear(); } getSyncInformation(callback: (arg0: SyncInformation) => void): void { callback({ isSyncActive: false, arePreferencesSynced: false, }); } upgradeDraggedFileSystemPermissions(fileSystem: FileSystem): void { } indexPath(requestId: number, fileSystemPath: Platform.DevToolsPath.RawPathString, excludedFolders: string): void { } stopIndexing(requestId: number): void { } searchInPath(requestId: number, fileSystemPath: Platform.DevToolsPath.RawPathString, query: string): void { } zoomFactor(): number { return 1; } zoomIn(): void { } zoomOut(): void { } resetZoom(): void { } setWhitelistedShortcuts(shortcuts: string): void { } setEyeDropperActive(active: boolean): void { } showCertificateViewer(certChain: string[]): void { } reattach(callback: () => void): void { } readyForTest(): void { } connectionReady(): void { } setOpenNewWindowForPopups(value: boolean): void { } setDevicesDiscoveryConfig(config: Adb.Config): void { } setDevicesUpdatesEnabled(enabled: boolean): void { } performActionOnRemotePage(pageId: string, action: string): void { } openRemotePage(browserId: string, url: string): void { } openNodeFrontend(): void { } showContextMenuAtPoint(x: number, y: number, items: ContextMenuDescriptor[], document: Document): void { throw 'Soft context menu should be used'; } isHostedMode(): boolean { return true; } setAddExtensionCallback(callback: (arg0: ExtensionDescriptor) => void): void { // Extensions are not supported in hosted mode. } async initialTargetId(): Promise<string|null> { return null; } } // @ts-ignore Global injected by devtools-compatibility.js // eslint-disable-next-line @typescript-eslint/naming-convention export let InspectorFrontendHostInstance: InspectorFrontendHostStub = globalThis.InspectorFrontendHost; class InspectorFrontendAPIImpl { constructor() { for (const descriptor of EventDescriptors) { // @ts-ignore Dispatcher magic this[descriptor[1]] = this.dispatch.bind(this, descriptor[0], descriptor[2], descriptor[3]); } } private dispatch(name: symbol, signature: string[], runOnceLoaded: boolean, ...params: string[]): void { // Single argument methods get dispatched with the param. if (signature.length < 2) { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any InspectorFrontendHostInstance.events.dispatchEventToListeners<any>(name, params[0]); } catch (error) { console.error(error + ' ' + error.stack); } return; } const data: { [x: string]: string, } = {}; for (let i = 0; i < signature.length; ++i) { data[signature[i]] = params[i]; } try { // eslint-disable-next-line @typescript-eslint/no-explicit-any InspectorFrontendHostInstance.events.dispatchEventToListeners<any>(name, data); } catch (error) { console.error(error + ' ' + error.stack); } } streamWrite(id: number, chunk: string): void { resourceLoaderStreamWrite(id, chunk); } } (function(): void { function initializeInspectorFrontendHost(): void { let proto; if (!InspectorFrontendHostInstance) { // Instantiate stub for web-hosted mode if necessary. // @ts-ignore Global injected by devtools-compatibility.js globalThis.InspectorFrontendHost = InspectorFrontendHostInstance = new InspectorFrontendHostStub(); } else { // Otherwise add stubs for missing methods that are declared in the interface. proto = InspectorFrontendHostStub.prototype; for (const name of Object.getOwnPropertyNames(proto)) { // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // @ts-expect-error const stub = proto[name]; // @ts-ignore Global injected by devtools-compatibility.js if (typeof stub !== 'function' || InspectorFrontendHostInstance[name]) { continue; } console.error(`Incompatible embedder: method Host.InspectorFrontendHost.${name} is missing. Using stub instead.`); // @ts-ignore Global injected by devtools-compatibility.js InspectorFrontendHostInstance[name] = stub; } } // Attach the events object. InspectorFrontendHostInstance.events = new Common.ObjectWrapper.ObjectWrapper(); } // FIXME: This file is included into both apps, since the devtools_app needs the InspectorFrontendHostAPI only, // so the host instance should not be initialized there. initializeInspectorFrontendHost(); // @ts-ignore Global injected by devtools-compatibility.js globalThis.InspectorFrontendAPI = new InspectorFrontendAPIImpl(); })(); export function isUnderTest(prefs?: { [x: string]: string, }): boolean { // Integration tests rely on test queryParam. if (Root.Runtime.Runtime.queryParam('test')) { return true; } // Browser tests rely on prefs. if (prefs) { return prefs['isUnderTest'] === 'true'; } return Common.Settings.Settings.hasInstance() && Common.Settings.Settings.instance().createSetting('isUnderTest', false).get(); }