123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- "use strict";
- /**
- * Copyright 2022 Google LLC.
- * Copyright (c) Microsoft Corporation.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.BrowsingContextImpl = void 0;
- const unitConversions_js_1 = require("../../../utils/unitConversions.js");
- const protocol_js_1 = require("../../../protocol/protocol.js");
- const log_js_1 = require("../../../utils/log.js");
- const deferred_js_1 = require("../../../utils/deferred.js");
- const realm_js_1 = require("../script/realm.js");
- class BrowsingContextImpl {
- /** The ID of this browsing context. */
- #id;
- /**
- * The ID of the parent browsing context.
- * If null, this is a top-level context.
- */
- #parentId;
- /** Direct children browsing contexts. */
- #children = new Set();
- #browsingContextStorage;
- #deferreds = {
- documentInitialized: new deferred_js_1.Deferred(),
- Page: {
- navigatedWithinDocument: new deferred_js_1.Deferred(),
- lifecycleEvent: {
- DOMContentLoaded: new deferred_js_1.Deferred(),
- load: new deferred_js_1.Deferred(),
- },
- },
- };
- #url = 'about:blank';
- #eventManager;
- #realmStorage;
- #loaderId;
- #cdpTarget;
- #maybeDefaultRealm;
- #isNavigating = false;
- #logger;
- constructor(cdpTarget, realmStorage, id, parentId, eventManager, browsingContextStorage, logger) {
- this.#cdpTarget = cdpTarget;
- this.#realmStorage = realmStorage;
- this.#id = id;
- this.#parentId = parentId;
- this.#eventManager = eventManager;
- this.#browsingContextStorage = browsingContextStorage;
- this.#logger = logger;
- }
- static create(cdpTarget, realmStorage, id, parentId, eventManager, browsingContextStorage, logger) {
- const context = new BrowsingContextImpl(cdpTarget, realmStorage, id, parentId, eventManager, browsingContextStorage, logger);
- context.#initListeners();
- browsingContextStorage.addContext(context);
- if (!context.isTopLevelContext()) {
- context.parent.addChild(context.id);
- }
- eventManager.registerEvent({
- method: protocol_js_1.BrowsingContext.EventNames.ContextCreatedEvent,
- params: context.serializeToBidiValue(),
- }, context.id);
- return context;
- }
- static getTimestamp() {
- // `timestamp` from the event is MonotonicTime, not real time, so
- // the best Mapper can do is to set the timestamp to the epoch time
- // of the event arrived.
- // https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-MonotonicTime
- return new Date().getTime();
- }
- /**
- * @see https://html.spec.whatwg.org/multipage/document-sequences.html#navigable
- */
- get navigableId() {
- return this.#loaderId;
- }
- delete() {
- this.#deleteAllChildren();
- this.#realmStorage.deleteRealms({
- browsingContextId: this.id,
- });
- // Remove context from the parent.
- if (!this.isTopLevelContext()) {
- this.parent.#children.delete(this.id);
- }
- this.#eventManager.registerEvent({
- method: protocol_js_1.BrowsingContext.EventNames.ContextDestroyedEvent,
- params: this.serializeToBidiValue(),
- }, this.id);
- this.#browsingContextStorage.deleteContextById(this.id);
- }
- /** Returns the ID of this context. */
- get id() {
- return this.#id;
- }
- /** Returns the parent context ID. */
- get parentId() {
- return this.#parentId;
- }
- /** Returns the parent context. */
- get parent() {
- if (this.parentId === null) {
- return null;
- }
- return this.#browsingContextStorage.getContext(this.parentId);
- }
- /** Returns all direct children contexts. */
- get directChildren() {
- return [...this.#children].map((id) => this.#browsingContextStorage.getContext(id));
- }
- /** Returns all children contexts, flattened. */
- get allChildren() {
- const children = this.directChildren;
- return children.concat(...children.map((child) => child.allChildren));
- }
- /**
- * Returns true if this is a top-level context.
- * This is the case whenever the parent context ID is null.
- */
- isTopLevelContext() {
- return this.#parentId === null;
- }
- get top() {
- // eslint-disable-next-line @typescript-eslint/no-this-alias
- let topContext = this;
- let parent = topContext.parent;
- while (parent) {
- topContext = parent;
- parent = topContext.parent;
- }
- return topContext;
- }
- addChild(childId) {
- this.#children.add(childId);
- }
- #deleteAllChildren() {
- this.directChildren.map((child) => child.delete());
- }
- get #defaultRealm() {
- if (this.#maybeDefaultRealm === undefined) {
- throw new Error(`No default realm for browsing context ${this.#id}`);
- }
- return this.#maybeDefaultRealm;
- }
- get cdpTarget() {
- return this.#cdpTarget;
- }
- updateCdpTarget(cdpTarget) {
- this.#cdpTarget = cdpTarget;
- this.#initListeners();
- }
- get url() {
- return this.#url;
- }
- async awaitLoaded() {
- await this.#deferreds.Page.lifecycleEvent.load;
- }
- awaitUnblocked() {
- return this.#cdpTarget.targetUnblocked;
- }
- async getOrCreateSandbox(sandbox) {
- if (sandbox === undefined || sandbox === '') {
- return this.#defaultRealm;
- }
- let maybeSandboxes = this.#realmStorage.findRealms({
- browsingContextId: this.id,
- sandbox,
- });
- if (maybeSandboxes.length === 0) {
- await this.#cdpTarget.cdpClient.sendCommand('Page.createIsolatedWorld', {
- frameId: this.id,
- worldName: sandbox,
- });
- // `Runtime.executionContextCreated` should be emitted by the time the
- // previous command is done.
- maybeSandboxes = this.#realmStorage.findRealms({
- browsingContextId: this.id,
- sandbox,
- });
- }
- if (maybeSandboxes.length !== 1) {
- throw Error(`Sandbox ${sandbox} wasn't created.`);
- }
- return maybeSandboxes[0];
- }
- serializeToBidiValue(maxDepth = 0, addParentField = true) {
- return {
- context: this.#id,
- url: this.url,
- children: maxDepth > 0
- ? this.directChildren.map((c) => c.serializeToBidiValue(maxDepth - 1, false))
- : null,
- ...(addParentField ? { parent: this.#parentId } : {}),
- };
- }
- onTargetInfoChanged(params) {
- this.#url = params.targetInfo.url;
- if (this.#isNavigating) {
- this.#eventManager.registerEvent({
- method: protocol_js_1.BrowsingContext.EventNames.NavigationStarted,
- params: {
- context: this.id,
- // TODO: The network event is send before the CDP Page.frameStartedLoading
- // It theory there should be a way to get the data.
- navigation: null,
- timestamp: BrowsingContextImpl.getTimestamp(),
- url: this.#url,
- },
- }, this.id);
- this.#isNavigating = false;
- }
- }
- #initListeners() {
- this.#cdpTarget.cdpClient.on('Page.frameNavigated', (params) => {
- if (this.id !== params.frame.id) {
- return;
- }
- const timestamp = BrowsingContextImpl.getTimestamp();
- this.#url = params.frame.url + (params.frame.urlFragment ?? '');
- // At the point the page is initialized, all the nested iframes from the
- // previous page are detached and realms are destroyed.
- // Remove children from context.
- this.#deleteAllChildren();
- this.#eventManager.registerEvent({
- method: protocol_js_1.BrowsingContext.EventNames.FragmentNavigated,
- params: {
- context: this.id,
- navigation: this.#loaderId ?? null,
- timestamp,
- url: this.#url,
- },
- }, this.id);
- });
- this.#cdpTarget.cdpClient.on('Page.navigatedWithinDocument', (params) => {
- if (this.id !== params.frameId) {
- return;
- }
- const timestamp = BrowsingContextImpl.getTimestamp();
- this.#url = params.url;
- this.#deferreds.Page.navigatedWithinDocument.resolve(params);
- // TODO: Remove this once History event for BiDi are added
- this.#eventManager.registerEvent({
- method: protocol_js_1.BrowsingContext.EventNames.FragmentNavigated,
- params: {
- context: this.id,
- navigation: null,
- timestamp,
- url: this.#url,
- },
- }, this.id);
- });
- this.#cdpTarget.cdpClient.on('Page.frameStartedLoading', (params) => {
- if (this.id !== params.frameId) {
- return;
- }
- this.#isNavigating = true;
- });
- this.#cdpTarget.cdpClient.on('Page.frameStoppedLoading', (params) => {
- if (this.id !== params.frameId) {
- return;
- }
- this.#isNavigating = false;
- });
- this.#cdpTarget.cdpClient.on('Page.lifecycleEvent', (params) => {
- if (this.id !== params.frameId) {
- return;
- }
- if (params.name === 'init') {
- this.#documentChanged(params.loaderId);
- this.#deferreds.documentInitialized.resolve();
- return;
- }
- if (params.name === 'commit') {
- this.#loaderId = params.loaderId;
- return;
- }
- // Ignore event from not current navigation.
- if (params.loaderId !== this.#loaderId) {
- return;
- }
- const timestamp = BrowsingContextImpl.getTimestamp();
- switch (params.name) {
- case 'DOMContentLoaded':
- this.#deferreds.Page.lifecycleEvent.DOMContentLoaded.resolve(params);
- this.#eventManager.registerEvent({
- method: protocol_js_1.BrowsingContext.EventNames.DomContentLoadedEvent,
- params: {
- context: this.id,
- navigation: this.#loaderId ?? null,
- timestamp,
- url: this.#url,
- },
- }, this.id);
- break;
- case 'load':
- this.#deferreds.Page.lifecycleEvent.load.resolve(params);
- this.#eventManager.registerEvent({
- method: protocol_js_1.BrowsingContext.EventNames.LoadEvent,
- params: {
- context: this.id,
- navigation: this.#loaderId ?? null,
- timestamp,
- url: this.#url,
- },
- }, this.id);
- break;
- }
- });
- this.#cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
- if (params.context.auxData.frameId !== this.id) {
- return;
- }
- // Only this execution contexts are supported for now.
- if (!['default', 'isolated'].includes(params.context.auxData.type)) {
- return;
- }
- const realm = new realm_js_1.Realm(this.#realmStorage, this.#browsingContextStorage, params.context.uniqueId, this.id, params.context.id, this.#getOrigin(params),
- // XXX: differentiate types.
- 'window',
- // Sandbox name for isolated world.
- params.context.auxData.type === 'isolated'
- ? params.context.name
- : undefined, this.#cdpTarget.cdpSessionId, this.#cdpTarget.cdpClient, this.#eventManager, this.#logger);
- if (params.context.auxData.isDefault) {
- this.#maybeDefaultRealm = realm;
- // Initialize ChannelProxy listeners for all the channels of all the
- // preload scripts related to this BrowsingContext.
- // TODO: extend for not default realms by the sandbox name.
- void Promise.all(this.#cdpTarget
- .getChannels(this.id)
- .map((channel) => channel.startListenerFromWindow(realm, this.#eventManager)));
- }
- });
- this.#cdpTarget.cdpClient.on('Runtime.executionContextDestroyed', (params) => {
- this.#realmStorage.deleteRealms({
- cdpSessionId: this.#cdpTarget.cdpSessionId,
- executionContextId: params.executionContextId,
- });
- });
- this.#cdpTarget.cdpClient.on('Runtime.executionContextsCleared', () => {
- this.#realmStorage.deleteRealms({
- cdpSessionId: this.#cdpTarget.cdpSessionId,
- });
- });
- }
- #getOrigin(params) {
- if (params.context.auxData.type === 'isolated') {
- // Sandbox should have the same origin as the context itself, but in CDP
- // it has an empty one.
- return this.#defaultRealm.origin;
- }
- // https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin
- return ['://', ''].includes(params.context.origin)
- ? 'null'
- : params.context.origin;
- }
- #documentChanged(loaderId) {
- // Same document navigation.
- if (loaderId === undefined || this.#loaderId === loaderId) {
- if (this.#deferreds.Page.navigatedWithinDocument.isFinished) {
- this.#deferreds.Page.navigatedWithinDocument =
- new deferred_js_1.Deferred();
- }
- else {
- this.#logger?.(log_js_1.LogType.browsingContexts, 'Document changed (navigatedWithinDocument)');
- }
- return;
- }
- this.#resetDeferredsIfFinished();
- this.#loaderId = loaderId;
- }
- #resetDeferredsIfFinished() {
- if (this.#deferreds.documentInitialized.isFinished) {
- this.#deferreds.documentInitialized = new deferred_js_1.Deferred();
- }
- else {
- this.#logger?.(log_js_1.LogType.browsingContexts, 'Document changed (document initialized)');
- }
- if (this.#deferreds.Page.lifecycleEvent.DOMContentLoaded.isFinished) {
- this.#deferreds.Page.lifecycleEvent.DOMContentLoaded =
- new deferred_js_1.Deferred();
- }
- else {
- this.#logger?.(log_js_1.LogType.browsingContexts, 'Document changed (DOMContentLoaded)');
- }
- if (this.#deferreds.Page.lifecycleEvent.load.isFinished) {
- this.#deferreds.Page.lifecycleEvent.load =
- new deferred_js_1.Deferred();
- }
- else {
- this.#logger?.(log_js_1.LogType.browsingContexts, 'Document changed (load)');
- }
- }
- async navigate(url, wait) {
- await this.awaitUnblocked();
- // TODO: handle loading errors.
- const cdpNavigateResult = await this.#cdpTarget.cdpClient.sendCommand('Page.navigate', {
- url,
- frameId: this.id,
- });
- if (cdpNavigateResult.errorText) {
- throw new protocol_js_1.Message.UnknownErrorException(cdpNavigateResult.errorText);
- }
- this.#documentChanged(cdpNavigateResult.loaderId);
- switch (wait) {
- case 'none':
- break;
- case 'interactive':
- // No `loaderId` means same-document navigation.
- if (cdpNavigateResult.loaderId === undefined) {
- await this.#deferreds.Page.navigatedWithinDocument;
- }
- else {
- await this.#deferreds.Page.lifecycleEvent.DOMContentLoaded;
- }
- break;
- case 'complete':
- // No `loaderId` means same-document navigation.
- if (cdpNavigateResult.loaderId === undefined) {
- await this.#deferreds.Page.navigatedWithinDocument;
- }
- else {
- await this.awaitLoaded();
- }
- break;
- }
- return {
- result: {
- navigation: cdpNavigateResult.loaderId ?? null,
- url,
- },
- };
- }
- async reload(ignoreCache, wait) {
- await this.awaitUnblocked();
- await this.#cdpTarget.cdpClient.sendCommand('Page.reload', {
- ignoreCache,
- });
- this.#resetDeferredsIfFinished();
- switch (wait) {
- case 'none':
- break;
- case 'interactive':
- await this.#deferreds.Page.lifecycleEvent.DOMContentLoaded;
- break;
- case 'complete':
- await this.awaitLoaded();
- break;
- }
- return { result: {} };
- }
- async setViewport(viewport) {
- if (viewport === null) {
- await this.#cdpTarget.cdpClient.sendCommand('Emulation.clearDeviceMetricsOverride');
- }
- else {
- try {
- await this.#cdpTarget.cdpClient.sendCommand('Emulation.setDeviceMetricsOverride', {
- width: viewport.width,
- height: viewport.height,
- deviceScaleFactor: 0,
- mobile: false,
- dontSetVisibleSize: true,
- });
- }
- catch (err) {
- if (err.message.startsWith(
- // https://crsrc.org/c/content/browser/devtools/protocol/emulation_handler.cc;l=257;drc=2f6eee84cf98d4227e7c41718dd71b82f26d90ff
- 'Width and height values must be positive')) {
- throw new protocol_js_1.Message.UnsupportedOperationException('Provided viewport dimensions are not supported');
- }
- throw err;
- }
- }
- }
- async captureScreenshot() {
- // XXX: Focus the original tab after the screenshot is taken.
- // This is needed because the screenshot gets blocked until the active tab gets focus.
- await this.#cdpTarget.cdpClient.sendCommand('Page.bringToFront');
- let clip;
- if (this.isTopLevelContext()) {
- const { cssContentSize, cssLayoutViewport } = await this.#cdpTarget.cdpClient.sendCommand('Page.getLayoutMetrics');
- clip = {
- x: cssContentSize.x,
- y: cssContentSize.y,
- width: cssLayoutViewport.clientWidth,
- height: cssLayoutViewport.clientHeight,
- };
- }
- else {
- const { result: { value: iframeDocRect }, } = await this.#cdpTarget.cdpClient.sendCommand('Runtime.callFunctionOn', {
- functionDeclaration: String(() => {
- const docRect = globalThis.document.documentElement.getBoundingClientRect();
- return JSON.stringify({
- x: docRect.x,
- y: docRect.y,
- width: docRect.width,
- height: docRect.height,
- });
- }),
- executionContextId: this.#defaultRealm.executionContextId,
- });
- clip = JSON.parse(iframeDocRect);
- }
- const result = await this.#cdpTarget.cdpClient.sendCommand('Page.captureScreenshot', {
- clip: {
- ...clip,
- scale: 1.0,
- },
- });
- return {
- result: {
- data: result.data,
- },
- };
- }
- async print(params) {
- const cdpParams = {};
- if (params.background !== undefined) {
- cdpParams.printBackground = params.background;
- }
- if (params.margin?.bottom !== undefined) {
- cdpParams.marginBottom = (0, unitConversions_js_1.inchesFromCm)(params.margin.bottom);
- }
- if (params.margin?.left !== undefined) {
- cdpParams.marginLeft = (0, unitConversions_js_1.inchesFromCm)(params.margin.left);
- }
- if (params.margin?.right !== undefined) {
- cdpParams.marginRight = (0, unitConversions_js_1.inchesFromCm)(params.margin.right);
- }
- if (params.margin?.top !== undefined) {
- cdpParams.marginTop = (0, unitConversions_js_1.inchesFromCm)(params.margin.top);
- }
- if (params.orientation !== undefined) {
- cdpParams.landscape = params.orientation === 'landscape';
- }
- if (params.page?.height !== undefined) {
- cdpParams.paperHeight = (0, unitConversions_js_1.inchesFromCm)(params.page.height);
- }
- if (params.page?.width !== undefined) {
- cdpParams.paperWidth = (0, unitConversions_js_1.inchesFromCm)(params.page.width);
- }
- if (params.pageRanges !== undefined) {
- cdpParams.pageRanges = params.pageRanges.join(',');
- }
- if (params.scale !== undefined) {
- cdpParams.scale = params.scale;
- }
- if (params.shrinkToFit !== undefined) {
- cdpParams.preferCSSPageSize = !params.shrinkToFit;
- }
- const result = await this.#cdpTarget.cdpClient.sendCommand('Page.printToPDF', cdpParams);
- return {
- result: {
- data: result.data,
- },
- };
- }
- }
- exports.BrowsingContextImpl = BrowsingContextImpl;
- //# sourceMappingURL=browsingContextImpl.js.map
|