UNPKG

chrome-devtools-frontend

Version:
1,160 lines (999 loc) • 42 kB
// Copyright 2011 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import type * as ProtocolProxyApi from '../../generated/protocol-proxy-api.js'; import * as Protocol from '../../generated/protocol.js'; import * as Common from '../common/common.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import {type DeferredDOMNode, DOMModel, type DOMNode} from './DOMModel.js'; import {FrameManager} from './FrameManager.js'; import {Events as NetworkManagerEvents, NetworkManager, type RequestUpdateDroppedEventData} from './NetworkManager.js'; import type {NetworkRequest} from './NetworkRequest.js'; import {Resource} from './Resource.js'; import {ExecutionContext, RuntimeModel} from './RuntimeModel.js'; import {SDKModel} from './SDKModel.js'; import {SecurityOriginManager} from './SecurityOriginManager.js'; import {StorageKeyManager} from './StorageKeyManager.js'; import {Capability, type Target, Type} from './Target.js'; import {TargetManager} from './TargetManager.js'; export class ResourceTreeModel extends SDKModel<EventTypes> { readonly agent: ProtocolProxyApi.PageApi; readonly storageAgent: ProtocolProxyApi.StorageApi; readonly #securityOriginManager: SecurityOriginManager; readonly #storageKeyManager: StorageKeyManager; readonly framesInternal = new Map<string, ResourceTreeFrame>(); #cachedResourcesProcessed = false; #pendingReloadOptions: { ignoreCache: (boolean|undefined), scriptToEvaluateOnLoad: (string|undefined), }|null = null; #reloadSuspensionCount = 0; isInterstitialShowing = false; mainFrame: ResourceTreeFrame|null = null; #pendingBackForwardCacheNotUsedEvents = new Set<Protocol.Page.BackForwardCacheNotUsedEvent>(); constructor(target: Target) { super(target); const networkManager = target.model(NetworkManager); if (networkManager) { networkManager.addEventListener(NetworkManagerEvents.RequestFinished, this.onRequestFinished, this); networkManager.addEventListener(NetworkManagerEvents.RequestUpdateDropped, this.onRequestUpdateDropped, this); } this.agent = target.pageAgent(); this.storageAgent = target.storageAgent(); void this.agent.invoke_enable({}); this.#securityOriginManager = (target.model(SecurityOriginManager) as SecurityOriginManager); this.#storageKeyManager = (target.model(StorageKeyManager) as StorageKeyManager); target.registerPageDispatcher(new PageDispatcher(this)); void this.#buildResourceTree(); } async #buildResourceTree(): Promise<void> { return await this.agent.invoke_getResourceTree().then(event => { this.processCachedResources(event.getError() ? null : event.frameTree); if (this.mainFrame) { this.processPendingEvents(this.mainFrame); } }); } static frameForRequest(request: NetworkRequest): ResourceTreeFrame|null { const networkManager = NetworkManager.forRequest(request); const resourceTreeModel = networkManager ? networkManager.target().model(ResourceTreeModel) : null; if (!resourceTreeModel) { return null; } return request.frameId ? resourceTreeModel.frameForId(request.frameId) : null; } static frames(): ResourceTreeFrame[] { const result = []; for (const resourceTreeModel of TargetManager.instance().models(ResourceTreeModel)) { result.push(...resourceTreeModel.frames()); } return result; } static resourceForURL(url: Platform.DevToolsPath.UrlString): Resource|null { for (const resourceTreeModel of TargetManager.instance().models(ResourceTreeModel)) { const mainFrame = resourceTreeModel.mainFrame; // Workers call into this with no #frames available. const result = mainFrame ? mainFrame.resourceForURL(url) : null; if (result) { return result; } } return null; } static reloadAllPages(bypassCache?: boolean, scriptToEvaluateOnLoad?: string): void { for (const resourceTreeModel of TargetManager.instance().models(ResourceTreeModel)) { if (resourceTreeModel.target().parentTarget()?.type() !== Type.FRAME) { resourceTreeModel.reloadPage(bypassCache, scriptToEvaluateOnLoad); } } } async storageKeyForFrame(frameId: Protocol.Page.FrameId): Promise<string|null> { if (!this.framesInternal.has(frameId)) { return null; } const response = await this.storageAgent.invoke_getStorageKey({frameId}); if (response.getError() === 'Frame tree node for given frame not found') { return null; } return response.storageKey; } domModel(): DOMModel { return this.target().model(DOMModel) as DOMModel; } private processCachedResources(mainFramePayload: Protocol.Page.FrameResourceTree|null): void { // TODO(caseq): the url check below is a mergeable, conservative // workaround for a problem caused by us requesting resources from a // subtarget frame before it has committed. The proper fix is likely // to be too complicated to be safely merged. // See https://crbug.com/1081270 for details. if (mainFramePayload && mainFramePayload.frame.url !== ':') { this.dispatchEventToListeners(Events.WillLoadCachedResources); this.addFramesRecursively(null, mainFramePayload); this.target().setInspectedURL(mainFramePayload.frame.url as Platform.DevToolsPath.UrlString); } this.#cachedResourcesProcessed = true; const runtimeModel = this.target().model(RuntimeModel); if (runtimeModel) { runtimeModel.setExecutionContextComparator(this.executionContextComparator.bind(this)); runtimeModel.fireExecutionContextOrderChanged(); } this.dispatchEventToListeners(Events.CachedResourcesLoaded, this); } cachedResourcesLoaded(): boolean { return this.#cachedResourcesProcessed; } private addFrame(frame: ResourceTreeFrame, _aboutToNavigate?: boolean): void { this.framesInternal.set(frame.id, frame); if (frame.isMainFrame()) { this.mainFrame = frame; } this.dispatchEventToListeners(Events.FrameAdded, frame); this.updateSecurityOrigins(); void this.updateStorageKeys(); } frameAttached( frameId: Protocol.Page.FrameId, parentFrameId: Protocol.Page.FrameId|null, stackTrace?: Protocol.Runtime.StackTrace): ResourceTreeFrame|null { const sameTargetParentFrame = parentFrameId ? (this.framesInternal.get(parentFrameId) || null) : null; // Do nothing unless cached resource tree is processed - it will overwrite everything. if (!this.#cachedResourcesProcessed && sameTargetParentFrame) { return null; } if (this.framesInternal.has(frameId)) { return null; } const frame = new ResourceTreeFrame(this, sameTargetParentFrame, frameId, null, stackTrace || null); if (parentFrameId && !sameTargetParentFrame) { frame.crossTargetParentFrameId = parentFrameId; } if (frame.isMainFrame() && this.mainFrame) { // Navigation to the new backend process. this.frameDetached(this.mainFrame.id, false); } this.addFrame(frame, true); return frame; } frameNavigated(framePayload: Protocol.Page.Frame, type: Protocol.Page.NavigationType|undefined): void { const sameTargetParentFrame = framePayload.parentId ? (this.framesInternal.get(framePayload.parentId) || null) : null; // Do nothing unless cached resource tree is processed - it will overwrite everything. if (!this.#cachedResourcesProcessed && sameTargetParentFrame) { return; } let frame: (ResourceTreeFrame|null) = this.framesInternal.get(framePayload.id) || null; if (!frame) { // Simulate missed "frameAttached" for a main frame navigation to the new backend process. frame = this.frameAttached(framePayload.id, framePayload.parentId || null); console.assert(Boolean(frame)); if (!frame) { return; } } this.dispatchEventToListeners(Events.FrameWillNavigate, frame); frame.navigate(framePayload); if (type) { frame.backForwardCacheDetails.restoredFromCache = type === Protocol.Page.NavigationType.BackForwardCacheRestore; } if (frame.isMainFrame()) { this.target().setInspectedURL(frame.url); } this.dispatchEventToListeners(Events.FrameNavigated, frame); if (frame.isPrimaryFrame()) { this.primaryPageChanged(frame, PrimaryPageChangeType.NAVIGATION); } // Fill frame with retained resources (the ones loaded using new loader). const resources = frame.resources(); for (let i = 0; i < resources.length; ++i) { this.dispatchEventToListeners(Events.ResourceAdded, resources[i]); } this.updateSecurityOrigins(); void this.updateStorageKeys(); if (frame.backForwardCacheDetails.restoredFromCache) { FrameManager.instance().modelRemoved(this); FrameManager.instance().modelAdded(this); void this.#buildResourceTree(); } } primaryPageChanged(frame: ResourceTreeFrame, type: PrimaryPageChangeType): void { this.processPendingEvents(frame); this.dispatchEventToListeners(Events.PrimaryPageChanged, {frame, type}); const networkManager = this.target().model(NetworkManager); if (networkManager && frame.isOutermostFrame()) { networkManager.clearRequests(); } } documentOpened(framePayload: Protocol.Page.Frame): void { this.frameNavigated(framePayload, undefined); const frame = this.framesInternal.get(framePayload.id); if (frame && !frame.getResourcesMap().get(framePayload.url)) { const frameResource = this.createResourceFromFramePayload( framePayload, framePayload.url as Platform.DevToolsPath.UrlString, Common.ResourceType.resourceTypes.Document, framePayload.mimeType, null, null); frameResource.isGenerated = true; frame.addResource(frameResource); } } frameDetached(frameId: Protocol.Page.FrameId, isSwap: boolean): void { // Do nothing unless cached resource tree is processed - it will overwrite everything. if (!this.#cachedResourcesProcessed) { return; } const frame = this.framesInternal.get(frameId); if (!frame) { return; } const sameTargetParentFrame = frame.sameTargetParentFrame(); if (sameTargetParentFrame) { sameTargetParentFrame.removeChildFrame(frame, isSwap); } else { frame.remove(isSwap); } this.updateSecurityOrigins(); void this.updateStorageKeys(); } private onRequestFinished(event: Common.EventTarget.EventTargetEvent<NetworkRequest>): void { if (!this.#cachedResourcesProcessed) { return; } const request = event.data; if (request.failed) { return; } const frame = request.frameId ? this.framesInternal.get(request.frameId) : null; if (frame) { frame.addRequest(request); } } private onRequestUpdateDropped(event: Common.EventTarget.EventTargetEvent<RequestUpdateDroppedEventData>): void { if (!this.#cachedResourcesProcessed) { return; } const data = event.data; const frameId = data.frameId; if (!frameId) { return; } const frame = this.framesInternal.get(frameId); if (!frame) { return; } const url = data.url; if (frame.getResourcesMap().get(url)) { return; } const resource = new Resource( this, null, url, frame.url, frameId, data.loaderId, Common.ResourceType.resourceTypes[data.resourceType], data.mimeType, data.lastModified, null); frame.addResource(resource); } frameForId(frameId: Protocol.Page.FrameId): ResourceTreeFrame|null { return this.framesInternal.get(frameId) || null; } forAllResources(callback: (arg0: Resource) => boolean): boolean { if (this.mainFrame) { return this.mainFrame.callForFrameResources(callback); } return false; } frames(): ResourceTreeFrame[] { return [...this.framesInternal.values()]; } private addFramesRecursively( sameTargetParentFrame: ResourceTreeFrame|null, frameTreePayload: Protocol.Page.FrameResourceTree): void { const framePayload = frameTreePayload.frame; let frame = this.framesInternal.get(framePayload.id); if (!frame) { frame = new ResourceTreeFrame(this, sameTargetParentFrame, framePayload.id, framePayload, null); } if (!sameTargetParentFrame && framePayload.parentId) { frame.crossTargetParentFrameId = framePayload.parentId; } this.addFrame(frame); for (const childFrame of frameTreePayload.childFrames || []) { this.addFramesRecursively(frame, childFrame); } for (let i = 0; i < frameTreePayload.resources.length; ++i) { const subresource = frameTreePayload.resources[i]; const resource = this.createResourceFromFramePayload( framePayload, subresource.url as Platform.DevToolsPath.UrlString, Common.ResourceType.resourceTypes[subresource.type], subresource.mimeType, subresource.lastModified || null, subresource.contentSize || null); frame.addResource(resource); } if (!frame.getResourcesMap().get(framePayload.url)) { const frameResource = this.createResourceFromFramePayload( framePayload, framePayload.url as Platform.DevToolsPath.UrlString, Common.ResourceType.resourceTypes.Document, framePayload.mimeType, null, null); frame.addResource(frameResource); } } private createResourceFromFramePayload( frame: Protocol.Page.Frame, url: Platform.DevToolsPath.UrlString, type: Common.ResourceType.ResourceType, mimeType: string, lastModifiedTime: number|null, contentSize: number|null): Resource { const lastModified = typeof lastModifiedTime === 'number' ? new Date(lastModifiedTime * 1000) : null; return new Resource( this, null, url, frame.url as Platform.DevToolsPath.UrlString, frame.id, frame.loaderId, type, mimeType, lastModified, contentSize); } suspendReload(): void { this.#reloadSuspensionCount++; } resumeReload(): void { this.#reloadSuspensionCount--; console.assert(this.#reloadSuspensionCount >= 0, 'Unbalanced call to ResourceTreeModel.resumeReload()'); if (!this.#reloadSuspensionCount && this.#pendingReloadOptions) { const {ignoreCache, scriptToEvaluateOnLoad} = this.#pendingReloadOptions; this.reloadPage(ignoreCache, scriptToEvaluateOnLoad); } } reloadPage(ignoreCache?: boolean, scriptToEvaluateOnLoad?: string): void { const loaderId = this.mainFrame?.loaderId; if (!loaderId) { return; } // Only dispatch PageReloadRequested upon first reload request to simplify client logic. if (!this.#pendingReloadOptions) { this.dispatchEventToListeners(Events.PageReloadRequested, this); } if (this.#reloadSuspensionCount) { this.#pendingReloadOptions = {ignoreCache, scriptToEvaluateOnLoad}; return; } this.#pendingReloadOptions = null; const networkManager = this.target().model(NetworkManager); if (networkManager) { networkManager.clearRequests(); } this.dispatchEventToListeners(Events.WillReloadPage); void this.agent.invoke_reload({ignoreCache, scriptToEvaluateOnLoad, loaderId}); } navigate(url: Platform.DevToolsPath.UrlString): Promise<Protocol.Page.NavigateResponse> { return this.agent.invoke_navigate({url}); } async navigationHistory(): Promise<{ currentIndex: number, entries: Protocol.Page.NavigationEntry[], }|null> { const response = await this.agent.invoke_getNavigationHistory(); if (response.getError()) { return null; } return {currentIndex: response.currentIndex, entries: response.entries}; } navigateToHistoryEntry(entry: Protocol.Page.NavigationEntry): void { void this.agent.invoke_navigateToHistoryEntry({entryId: entry.id}); } setLifecycleEventsEnabled(enabled: boolean): Promise<Protocol.ProtocolResponseWithError> { return this.agent.invoke_setLifecycleEventsEnabled({enabled}); } async fetchAppManifest(): Promise<{ url: Platform.DevToolsPath.UrlString, data: string|null, errors: Protocol.Page.AppManifestError[], }> { const response = await this.agent.invoke_getAppManifest({}); if (response.getError()) { return {url: response.url as Platform.DevToolsPath.UrlString, data: null, errors: []}; } return {url: response.url as Platform.DevToolsPath.UrlString, data: response.data || null, errors: response.errors}; } async getInstallabilityErrors(): Promise<Protocol.Page.InstallabilityError[]> { const response = await this.agent.invoke_getInstallabilityErrors(); return response.installabilityErrors || []; } async getAppId(): Promise<Protocol.Page.GetAppIdResponse> { return await this.agent.invoke_getAppId(); } private executionContextComparator(a: ExecutionContext, b: ExecutionContext): number { function framePath(frame: ResourceTreeFrame|null): ResourceTreeFrame[] { let currentFrame: (ResourceTreeFrame|null) = frame; const parents = []; while (currentFrame) { parents.push(currentFrame); currentFrame = currentFrame.sameTargetParentFrame(); } return parents.reverse(); } if (a.target() !== b.target()) { return ExecutionContext.comparator(a, b); } const framesA = a.frameId ? framePath(this.frameForId(a.frameId)) : []; const framesB = b.frameId ? framePath(this.frameForId(b.frameId)) : []; let frameA; let frameB; for (let i = 0;; i++) { if (!framesA[i] || !framesB[i] || (framesA[i] !== framesB[i])) { frameA = framesA[i]; frameB = framesB[i]; break; } } if (!frameA && frameB) { return -1; } if (!frameB && frameA) { return 1; } if (frameA && frameB) { return frameA.id.localeCompare(frameB.id); } return ExecutionContext.comparator(a, b); } private getSecurityOriginData(): SecurityOriginData { const securityOrigins = new Set<string>(); let mainSecurityOrigin: string|null = null; let unreachableMainSecurityOrigin: string|null = null; for (const frame of this.framesInternal.values()) { const origin = frame.securityOrigin; if (!origin) { continue; } securityOrigins.add(origin); if (frame.isMainFrame()) { mainSecurityOrigin = origin; if (frame.unreachableUrl()) { const unreachableParsed = new Common.ParsedURL.ParsedURL(frame.unreachableUrl()); unreachableMainSecurityOrigin = unreachableParsed.securityOrigin(); } } } return { securityOrigins, mainSecurityOrigin, unreachableMainSecurityOrigin, }; } private async getStorageKeyData(): Promise<StorageKeyData> { const storageKeys = new Set<string>(); let mainStorageKey: string|null = null; for (const {isMainFrame, storageKey} of await Promise.all([...this.framesInternal.values()].map( f => f.getStorageKey(/* forceFetch */ false).then(k => ({ isMainFrame: f.isMainFrame(), storageKey: k, }))))) { if (isMainFrame) { mainStorageKey = storageKey; } if (storageKey) { storageKeys.add(storageKey); } } return {storageKeys, mainStorageKey}; } private updateSecurityOrigins(): void { const data = this.getSecurityOriginData(); this.#securityOriginManager.setMainSecurityOrigin( data.mainSecurityOrigin || '', data.unreachableMainSecurityOrigin || ''); this.#securityOriginManager.updateSecurityOrigins(data.securityOrigins); } private async updateStorageKeys(): Promise<void> { const data = await this.getStorageKeyData(); this.#storageKeyManager.setMainStorageKey(data.mainStorageKey || ''); this.#storageKeyManager.updateStorageKeys(data.storageKeys); } async getMainStorageKey(): Promise<string|null> { return this.mainFrame ? await this.mainFrame.getStorageKey(/* forceFetch */ false) : null; } getMainSecurityOrigin(): string|null { const data = this.getSecurityOriginData(); return data.mainSecurityOrigin || data.unreachableMainSecurityOrigin; } onBackForwardCacheNotUsed(event: Protocol.Page.BackForwardCacheNotUsedEvent): void { if (this.mainFrame && this.mainFrame.id === event.frameId && this.mainFrame.loaderId === event.loaderId) { this.mainFrame.setBackForwardCacheDetails(event); this.dispatchEventToListeners(Events.BackForwardCacheDetailsUpdated, this.mainFrame); } else { this.#pendingBackForwardCacheNotUsedEvents.add(event); } } processPendingEvents(frame: ResourceTreeFrame): void { if (!frame.isMainFrame()) { return; } for (const event of this.#pendingBackForwardCacheNotUsedEvents) { if (frame.id === event.frameId && frame.loaderId === event.loaderId) { frame.setBackForwardCacheDetails(event); this.#pendingBackForwardCacheNotUsedEvents.delete(event); break; } } // No need to dispatch events here as this method call is followed by a `PrimaryPageChanged` event. } } export enum Events { /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */ FrameAdded = 'FrameAdded', FrameNavigated = 'FrameNavigated', FrameDetached = 'FrameDetached', FrameResized = 'FrameResized', FrameWillNavigate = 'FrameWillNavigate', PrimaryPageChanged = 'PrimaryPageChanged', ResourceAdded = 'ResourceAdded', WillLoadCachedResources = 'WillLoadCachedResources', CachedResourcesLoaded = 'CachedResourcesLoaded', DOMContentLoaded = 'DOMContentLoaded', LifecycleEvent = 'LifecycleEvent', Load = 'Load', PageReloadRequested = 'PageReloadRequested', WillReloadPage = 'WillReloadPage', InterstitialShown = 'InterstitialShown', InterstitialHidden = 'InterstitialHidden', BackForwardCacheDetailsUpdated = 'BackForwardCacheDetailsUpdated', JavaScriptDialogOpening = 'JavaScriptDialogOpening', /* eslint-enable @typescript-eslint/naming-convention */ } export interface EventTypes { [Events.FrameAdded]: ResourceTreeFrame; [Events.FrameNavigated]: ResourceTreeFrame; [Events.FrameDetached]: {frame: ResourceTreeFrame, isSwap: boolean}; [Events.FrameResized]: void; [Events.FrameWillNavigate]: ResourceTreeFrame; [Events.PrimaryPageChanged]: {frame: ResourceTreeFrame, type: PrimaryPageChangeType}; [Events.ResourceAdded]: Resource; [Events.WillLoadCachedResources]: void; [Events.CachedResourcesLoaded]: ResourceTreeModel; [Events.DOMContentLoaded]: number; [Events.LifecycleEvent]: {frameId: Protocol.Page.FrameId, name: string}; [Events.Load]: {resourceTreeModel: ResourceTreeModel, loadTime: number}; [Events.PageReloadRequested]: ResourceTreeModel; [Events.WillReloadPage]: void; [Events.InterstitialShown]: void; [Events.InterstitialHidden]: void; [Events.BackForwardCacheDetailsUpdated]: ResourceTreeFrame; [Events.JavaScriptDialogOpening]: Protocol.Page.JavascriptDialogOpeningEvent; } export class ResourceTreeFrame { #model: ResourceTreeModel; #sameTargetParentFrame: ResourceTreeFrame|null; readonly #id: Protocol.Page.FrameId; crossTargetParentFrameId: string|null = null; #loaderId: Protocol.Network.LoaderId; #name: string|null|undefined; #url: Platform.DevToolsPath.UrlString; #domainAndRegistry: string; #securityOrigin: string|null; #securityOriginDetails?: Protocol.Page.SecurityOriginDetails; #storageKey?: Promise<string|null>; #unreachableUrl: Platform.DevToolsPath.UrlString; #adFrameStatus?: Protocol.Page.AdFrameStatus; #secureContextType: Protocol.Page.SecureContextType|null; #crossOriginIsolatedContextType: Protocol.Page.CrossOriginIsolatedContextType|null; #gatedAPIFeatures: Protocol.Page.GatedAPIFeatures[]|null; #creationStackTrace: Protocol.Runtime.StackTrace|null; #creationStackTraceTarget: Target|null = null; #childFrames = new Set<ResourceTreeFrame>(); resourcesMap = new Map<Platform.DevToolsPath.UrlString, Resource>(); backForwardCacheDetails: { restoredFromCache: boolean|undefined, explanations: Protocol.Page.BackForwardCacheNotRestoredExplanation[], explanationsTree: Protocol.Page.BackForwardCacheNotRestoredExplanationTree|undefined, } = { restoredFromCache: undefined, explanations: [], explanationsTree: undefined, }; constructor( model: ResourceTreeModel, parentFrame: ResourceTreeFrame|null, frameId: Protocol.Page.FrameId, payload: Protocol.Page.Frame|null, creationStackTrace: Protocol.Runtime.StackTrace|null) { this.#model = model; this.#sameTargetParentFrame = parentFrame; this.#id = frameId; this.#loaderId = payload?.loaderId ?? '' as Protocol.Network.LoaderId; this.#name = payload?.name; this.#url = payload && payload.url as Platform.DevToolsPath.UrlString || Platform.DevToolsPath.EmptyUrlString; this.#domainAndRegistry = (payload?.domainAndRegistry) || ''; this.#securityOrigin = payload?.securityOrigin ?? null; this.#securityOriginDetails = payload?.securityOriginDetails; this.#unreachableUrl = (payload && payload.unreachableUrl as Platform.DevToolsPath.UrlString) || Platform.DevToolsPath.EmptyUrlString; this.#adFrameStatus = payload?.adFrameStatus; this.#secureContextType = payload?.secureContextType ?? null; this.#crossOriginIsolatedContextType = payload?.crossOriginIsolatedContextType ?? null; this.#gatedAPIFeatures = payload?.gatedAPIFeatures ?? null; this.#creationStackTrace = creationStackTrace; if (this.#sameTargetParentFrame) { this.#sameTargetParentFrame.#childFrames.add(this); } } isSecureContext(): boolean { return this.#secureContextType !== null && this.#secureContextType.startsWith('Secure'); } getSecureContextType(): Protocol.Page.SecureContextType|null { return this.#secureContextType; } isCrossOriginIsolated(): boolean { return this.#crossOriginIsolatedContextType !== null && this.#crossOriginIsolatedContextType.startsWith('Isolated'); } getCrossOriginIsolatedContextType(): Protocol.Page.CrossOriginIsolatedContextType|null { return this.#crossOriginIsolatedContextType; } getGatedAPIFeatures(): Protocol.Page.GatedAPIFeatures[]|null { return this.#gatedAPIFeatures; } getCreationStackTraceData(): {creationStackTrace: Protocol.Runtime.StackTrace|null, creationStackTraceTarget: Target} { return { creationStackTrace: this.#creationStackTrace, creationStackTraceTarget: this.#creationStackTraceTarget || this.resourceTreeModel().target(), }; } navigate(framePayload: Protocol.Page.Frame): void { this.#loaderId = framePayload.loaderId; this.#name = framePayload.name; this.#url = framePayload.url as Platform.DevToolsPath.UrlString; this.#domainAndRegistry = framePayload.domainAndRegistry; this.#securityOrigin = framePayload.securityOrigin; this.#securityOriginDetails = framePayload.securityOriginDetails; void this.getStorageKey(/* forceFetch */ true); this.#unreachableUrl = framePayload.unreachableUrl as Platform.DevToolsPath.UrlString || Platform.DevToolsPath.EmptyUrlString; this.#adFrameStatus = framePayload?.adFrameStatus; this.#secureContextType = framePayload.secureContextType; this.#crossOriginIsolatedContextType = framePayload.crossOriginIsolatedContextType; this.#gatedAPIFeatures = framePayload.gatedAPIFeatures; this.backForwardCacheDetails = { restoredFromCache: undefined, explanations: [], explanationsTree: undefined, }; const mainResource = this.resourcesMap.get(this.#url); this.resourcesMap.clear(); this.removeChildFrames(); if (mainResource && mainResource.loaderId === this.#loaderId) { this.addResource(mainResource); } } resourceTreeModel(): ResourceTreeModel { return this.#model; } get id(): Protocol.Page.FrameId { return this.#id; } get name(): string { return this.#name || ''; } get url(): Platform.DevToolsPath.UrlString { return this.#url; } domainAndRegistry(): string { return this.#domainAndRegistry; } async getAdScriptAncestry(frameId: Protocol.Page.FrameId): Promise<Protocol.Page.AdScriptAncestry|null> { const res = await this.#model.agent.invoke_getAdScriptAncestry({frameId}); return res.adScriptAncestry || null; } get securityOrigin(): string|null { return this.#securityOrigin; } get securityOriginDetails(): Protocol.Page.SecurityOriginDetails|null { return this.#securityOriginDetails ?? null; } getStorageKey(forceFetch: boolean): Promise<string|null> { if (!this.#storageKey || forceFetch) { this.#storageKey = this.#model.storageKeyForFrame(this.#id); } return this.#storageKey; } unreachableUrl(): Platform.DevToolsPath.UrlString { return this.#unreachableUrl; } get loaderId(): Protocol.Network.LoaderId { return this.#loaderId; } adFrameType(): Protocol.Page.AdFrameType { return this.#adFrameStatus?.adFrameType || Protocol.Page.AdFrameType.None; } adFrameStatus(): Protocol.Page.AdFrameStatus|undefined { return this.#adFrameStatus; } get childFrames(): ResourceTreeFrame[] { return [...this.#childFrames]; } /** * Returns the parent frame if both #frames are part of the same process/target. */ sameTargetParentFrame(): ResourceTreeFrame|null { return this.#sameTargetParentFrame; } /** * Returns the parent frame if both #frames are part of different processes/targets (child is an OOPIF). */ crossTargetParentFrame(): ResourceTreeFrame|null { if (!this.crossTargetParentFrameId) { return null; } const parentTarget = this.#model.target().parentTarget(); if (parentTarget?.type() !== Type.FRAME) { return null; } const parentModel = parentTarget.model(ResourceTreeModel); if (!parentModel) { return null; } // Note that parent #model has already processed cached resources: // - when parent target was created, we issued getResourceTree call; // - strictly after we issued setAutoAttach call; // - both of them were handled in renderer in the same order; // - cached resource tree got processed on parent #model; // - child target was created as a result of setAutoAttach call. return parentModel.framesInternal.get(this.crossTargetParentFrameId) || null; } /** * Returns the parent frame. There is only 1 parent and it's either in the * same target or it's cross-target. */ parentFrame(): ResourceTreeFrame|null { return this.sameTargetParentFrame() || this.crossTargetParentFrame(); } /** * Returns true if this is the main frame of its target. A main frame is the root of the frame tree i.e. a frame without * a parent, but the whole frame tree could be embedded in another frame tree (e.g. OOPIFs, fenced frames, portals). * https://chromium.googlesource.com/chromium/src/+/HEAD/docs/frame_trees.md */ isMainFrame(): boolean { return !this.#sameTargetParentFrame; } /** * Returns true if this is a main frame which is not embedded in another frame tree. With MPArch features such as * back/forward cache or prerender there can be multiple outermost frames. * https://chromium.googlesource.com/chromium/src/+/HEAD/docs/frame_trees.md */ isOutermostFrame(): boolean { return this.#model.target().parentTarget()?.type() !== Type.FRAME && !this.#sameTargetParentFrame && !this.crossTargetParentFrameId; } /** * Returns true if this is the primary frame of the browser tab. There can only be one primary frame for each * browser tab. It is the outermost frame being actively displayed in the browser tab. * https://chromium.googlesource.com/chromium/src/+/HEAD/docs/frame_trees.md */ isPrimaryFrame(): boolean { return !this.#sameTargetParentFrame && this.#model.target() === TargetManager.instance().primaryPageTarget(); } removeChildFrame(frame: ResourceTreeFrame, isSwap: boolean): void { this.#childFrames.delete(frame); frame.remove(isSwap); } private removeChildFrames(): void { const frames = this.#childFrames; this.#childFrames = new Set(); for (const frame of frames) { frame.remove(false); } } remove(isSwap: boolean): void { this.removeChildFrames(); this.#model.framesInternal.delete(this.id); this.#model.dispatchEventToListeners(Events.FrameDetached, {frame: this, isSwap}); } addResource(resource: Resource): void { if (this.resourcesMap.get(resource.url) === resource) { // Already in the tree, we just got an extra update. return; } this.resourcesMap.set(resource.url, resource); this.#model.dispatchEventToListeners(Events.ResourceAdded, resource); } addRequest(request: NetworkRequest): void { let resource = this.resourcesMap.get(request.url()); if (resource?.request === request) { // Already in the tree, we just got an extra update. return; } resource = new Resource( this.#model, request, request.url(), request.documentURL, request.frameId, request.loaderId, request.resourceType(), request.mimeType, null, null); this.resourcesMap.set(resource.url, resource); this.#model.dispatchEventToListeners(Events.ResourceAdded, resource); } resources(): Resource[] { return Array.from(this.resourcesMap.values()); } resourceForURL(url: Platform.DevToolsPath.UrlString): Resource|null { const resource = this.resourcesMap.get(url); if (resource) { return resource; } for (const frame of this.#childFrames) { const resource = frame.resourceForURL(url); if (resource) { return resource; } } return null; } callForFrameResources(callback: (arg0: Resource) => boolean): boolean { for (const resource of this.resourcesMap.values()) { if (callback(resource)) { return true; } } for (const frame of this.#childFrames) { if (frame.callForFrameResources(callback)) { return true; } } return false; } displayName(): string { if (this.isOutermostFrame()) { return i18n.i18n.lockedString('top'); } const subtitle = new Common.ParsedURL.ParsedURL(this.#url).displayName; if (subtitle) { if (!this.#name) { return subtitle; } return this.#name + ' (' + subtitle + ')'; } return i18n.i18n.lockedString('iframe'); } async getOwnerDeferredDOMNode(): Promise<DeferredDOMNode|null> { const parentFrame = this.parentFrame(); if (!parentFrame) { return null; } return await parentFrame.resourceTreeModel().domModel().getOwnerNodeForFrame(this.#id); } async getOwnerDOMNodeOrDocument(): Promise<DOMNode|null> { const deferredNode = await this.getOwnerDeferredDOMNode(); if (deferredNode) { return await deferredNode.resolvePromise(); } if (this.isOutermostFrame()) { return await this.resourceTreeModel().domModel().requestDocument(); } return null; } async highlight(): Promise<void> { const parentFrame = this.parentFrame(); const parentTarget = this.resourceTreeModel().target().parentTarget(); const highlightFrameOwner = async(domModel: DOMModel): Promise<void> => { const deferredNode = await domModel.getOwnerNodeForFrame(this.#id); if (deferredNode) { domModel.overlayModel().highlightInOverlay({deferredNode, selectorList: ''}, 'all', true); } }; if (parentFrame) { return await highlightFrameOwner(parentFrame.resourceTreeModel().domModel()); } // Fenced frames. if (parentTarget?.type() === Type.FRAME) { const domModel = parentTarget.model(DOMModel); if (domModel) { return await highlightFrameOwner(domModel); } } // For the outermost frame there is no owner node. Highlight the whole #document instead. const document = await this.resourceTreeModel().domModel().requestDocument(); if (document) { this.resourceTreeModel().domModel().overlayModel().highlightInOverlay( {node: document, selectorList: ''}, 'all', true); } } async getPermissionsPolicyState(): Promise<Protocol.Page.PermissionsPolicyFeatureState[]|null> { const response = await this.resourceTreeModel().target().pageAgent().invoke_getPermissionsPolicyState({frameId: this.#id}); if (response.getError()) { return null; } return response.states; } async getOriginTrials(): Promise<Protocol.Page.OriginTrial[]> { const response = await this.resourceTreeModel().target().pageAgent().invoke_getOriginTrials({frameId: this.#id}); if (response.getError()) { return []; } return response.originTrials; } setCreationStackTrace(creationStackTraceData: { creationStackTrace: Protocol.Runtime.StackTrace|null, creationStackTraceTarget: Target, }): void { this.#creationStackTrace = creationStackTraceData.creationStackTrace; this.#creationStackTraceTarget = creationStackTraceData.creationStackTraceTarget; } setBackForwardCacheDetails(event: Protocol.Page.BackForwardCacheNotUsedEvent): void { this.backForwardCacheDetails.restoredFromCache = false; this.backForwardCacheDetails.explanations = event.notRestoredExplanations; this.backForwardCacheDetails.explanationsTree = event.notRestoredExplanationsTree; } getResourcesMap(): Map<string, Resource> { return this.resourcesMap; } } export class PageDispatcher implements ProtocolProxyApi.PageDispatcher { #resourceTreeModel: ResourceTreeModel; constructor(resourceTreeModel: ResourceTreeModel) { this.#resourceTreeModel = resourceTreeModel; } backForwardCacheNotUsed(params: Protocol.Page.BackForwardCacheNotUsedEvent): void { this.#resourceTreeModel.onBackForwardCacheNotUsed(params); } domContentEventFired({timestamp}: Protocol.Page.DomContentEventFiredEvent): void { this.#resourceTreeModel.dispatchEventToListeners(Events.DOMContentLoaded, timestamp); } loadEventFired({timestamp}: Protocol.Page.LoadEventFiredEvent): void { this.#resourceTreeModel.dispatchEventToListeners( Events.Load, {resourceTreeModel: this.#resourceTreeModel, loadTime: timestamp}); } lifecycleEvent({frameId, name}: Protocol.Page.LifecycleEventEvent): void { this.#resourceTreeModel.dispatchEventToListeners(Events.LifecycleEvent, {frameId, name}); } frameAttached({frameId, parentFrameId, stack}: Protocol.Page.FrameAttachedEvent): void { this.#resourceTreeModel.frameAttached(frameId, parentFrameId, stack); } frameNavigated({frame, type}: Protocol.Page.FrameNavigatedEvent): void { this.#resourceTreeModel.frameNavigated(frame, type); } documentOpened({frame}: Protocol.Page.DocumentOpenedEvent): void { this.#resourceTreeModel.documentOpened(frame); } frameDetached({frameId, reason}: Protocol.Page.FrameDetachedEvent): void { this.#resourceTreeModel.frameDetached(frameId, reason === Protocol.Page.FrameDetachedEventReason.Swap); } frameSubtreeWillBeDetached(_params: Protocol.Page.FrameSubtreeWillBeDetachedEvent): void { } frameStartedLoading({}: Protocol.Page.FrameStartedLoadingEvent): void { } frameStoppedLoading({}: Protocol.Page.FrameStoppedLoadingEvent): void { } frameRequestedNavigation({}: Protocol.Page.FrameRequestedNavigationEvent): void { } frameScheduledNavigation({}: Protocol.Page.FrameScheduledNavigationEvent): void { } frameClearedScheduledNavigation({}: Protocol.Page.FrameClearedScheduledNavigationEvent): void { } frameStartedNavigating({}: Protocol.Page.FrameStartedNavigatingEvent): void { } navigatedWithinDocument({}: Protocol.Page.NavigatedWithinDocumentEvent): void { } frameResized(): void { this.#resourceTreeModel.dispatchEventToListeners(Events.FrameResized); } javascriptDialogOpening(event: Protocol.Page.JavascriptDialogOpeningEvent): void { this.#resourceTreeModel.dispatchEventToListeners(Events.JavaScriptDialogOpening, event); if (!event.hasBrowserHandler) { void this.#resourceTreeModel.agent.invoke_handleJavaScriptDialog({accept: false}); } } javascriptDialogClosed({}: Protocol.Page.JavascriptDialogClosedEvent): void { } screencastFrame({}: Protocol.Page.ScreencastFrameEvent): void { } screencastVisibilityChanged({}: Protocol.Page.ScreencastVisibilityChangedEvent): void { } interstitialShown(): void { this.#resourceTreeModel.isInterstitialShowing = true; this.#resourceTreeModel.dispatchEventToListeners(Events.InterstitialShown); } interstitialHidden(): void { this.#resourceTreeModel.isInterstitialShowing = false; this.#resourceTreeModel.dispatchEventToListeners(Events.InterstitialHidden); } windowOpen({}: Protocol.Page.WindowOpenEvent): void { } compilationCacheProduced({}: Protocol.Page.CompilationCacheProducedEvent): void { } fileChooserOpened({}: Protocol.Page.FileChooserOpenedEvent): void { } downloadWillBegin({}: Protocol.Page.DownloadWillBeginEvent): void { } downloadProgress(): void { } } SDKModel.register(ResourceTreeModel, {capabilities: Capability.DOM, autostart: true, early: true}); export interface SecurityOriginData { securityOrigins: Set<string>; mainSecurityOrigin: string|null; unreachableMainSecurityOrigin: string|null; } export interface StorageKeyData { storageKeys: Set<string>; mainStorageKey: string|null; } export const enum PrimaryPageChangeType { NAVIGATION = 'Navigation', ACTIVATION = 'Activation', }