| // 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', |
| } |