browsingContextImpl.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. "use strict";
  2. /**
  3. * Copyright 2022 Google LLC.
  4. * Copyright (c) Microsoft Corporation.
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. Object.defineProperty(exports, "__esModule", { value: true });
  19. exports.BrowsingContextImpl = void 0;
  20. const unitConversions_js_1 = require("../../../utils/unitConversions.js");
  21. const protocol_js_1 = require("../../../protocol/protocol.js");
  22. const log_js_1 = require("../../../utils/log.js");
  23. const deferred_js_1 = require("../../../utils/deferred.js");
  24. const realm_js_1 = require("../script/realm.js");
  25. class BrowsingContextImpl {
  26. /** The ID of this browsing context. */
  27. #id;
  28. /**
  29. * The ID of the parent browsing context.
  30. * If null, this is a top-level context.
  31. */
  32. #parentId;
  33. /** Direct children browsing contexts. */
  34. #children = new Set();
  35. #browsingContextStorage;
  36. #deferreds = {
  37. documentInitialized: new deferred_js_1.Deferred(),
  38. Page: {
  39. navigatedWithinDocument: new deferred_js_1.Deferred(),
  40. lifecycleEvent: {
  41. DOMContentLoaded: new deferred_js_1.Deferred(),
  42. load: new deferred_js_1.Deferred(),
  43. },
  44. },
  45. };
  46. #url = 'about:blank';
  47. #eventManager;
  48. #realmStorage;
  49. #loaderId;
  50. #cdpTarget;
  51. #maybeDefaultRealm;
  52. #isNavigating = false;
  53. #logger;
  54. constructor(cdpTarget, realmStorage, id, parentId, eventManager, browsingContextStorage, logger) {
  55. this.#cdpTarget = cdpTarget;
  56. this.#realmStorage = realmStorage;
  57. this.#id = id;
  58. this.#parentId = parentId;
  59. this.#eventManager = eventManager;
  60. this.#browsingContextStorage = browsingContextStorage;
  61. this.#logger = logger;
  62. }
  63. static create(cdpTarget, realmStorage, id, parentId, eventManager, browsingContextStorage, logger) {
  64. const context = new BrowsingContextImpl(cdpTarget, realmStorage, id, parentId, eventManager, browsingContextStorage, logger);
  65. context.#initListeners();
  66. browsingContextStorage.addContext(context);
  67. if (!context.isTopLevelContext()) {
  68. context.parent.addChild(context.id);
  69. }
  70. eventManager.registerEvent({
  71. method: protocol_js_1.BrowsingContext.EventNames.ContextCreatedEvent,
  72. params: context.serializeToBidiValue(),
  73. }, context.id);
  74. return context;
  75. }
  76. static getTimestamp() {
  77. // `timestamp` from the event is MonotonicTime, not real time, so
  78. // the best Mapper can do is to set the timestamp to the epoch time
  79. // of the event arrived.
  80. // https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-MonotonicTime
  81. return new Date().getTime();
  82. }
  83. /**
  84. * @see https://html.spec.whatwg.org/multipage/document-sequences.html#navigable
  85. */
  86. get navigableId() {
  87. return this.#loaderId;
  88. }
  89. delete() {
  90. this.#deleteAllChildren();
  91. this.#realmStorage.deleteRealms({
  92. browsingContextId: this.id,
  93. });
  94. // Remove context from the parent.
  95. if (!this.isTopLevelContext()) {
  96. this.parent.#children.delete(this.id);
  97. }
  98. this.#eventManager.registerEvent({
  99. method: protocol_js_1.BrowsingContext.EventNames.ContextDestroyedEvent,
  100. params: this.serializeToBidiValue(),
  101. }, this.id);
  102. this.#browsingContextStorage.deleteContextById(this.id);
  103. }
  104. /** Returns the ID of this context. */
  105. get id() {
  106. return this.#id;
  107. }
  108. /** Returns the parent context ID. */
  109. get parentId() {
  110. return this.#parentId;
  111. }
  112. /** Returns the parent context. */
  113. get parent() {
  114. if (this.parentId === null) {
  115. return null;
  116. }
  117. return this.#browsingContextStorage.getContext(this.parentId);
  118. }
  119. /** Returns all direct children contexts. */
  120. get directChildren() {
  121. return [...this.#children].map((id) => this.#browsingContextStorage.getContext(id));
  122. }
  123. /** Returns all children contexts, flattened. */
  124. get allChildren() {
  125. const children = this.directChildren;
  126. return children.concat(...children.map((child) => child.allChildren));
  127. }
  128. /**
  129. * Returns true if this is a top-level context.
  130. * This is the case whenever the parent context ID is null.
  131. */
  132. isTopLevelContext() {
  133. return this.#parentId === null;
  134. }
  135. get top() {
  136. // eslint-disable-next-line @typescript-eslint/no-this-alias
  137. let topContext = this;
  138. let parent = topContext.parent;
  139. while (parent) {
  140. topContext = parent;
  141. parent = topContext.parent;
  142. }
  143. return topContext;
  144. }
  145. addChild(childId) {
  146. this.#children.add(childId);
  147. }
  148. #deleteAllChildren() {
  149. this.directChildren.map((child) => child.delete());
  150. }
  151. get #defaultRealm() {
  152. if (this.#maybeDefaultRealm === undefined) {
  153. throw new Error(`No default realm for browsing context ${this.#id}`);
  154. }
  155. return this.#maybeDefaultRealm;
  156. }
  157. get cdpTarget() {
  158. return this.#cdpTarget;
  159. }
  160. updateCdpTarget(cdpTarget) {
  161. this.#cdpTarget = cdpTarget;
  162. this.#initListeners();
  163. }
  164. get url() {
  165. return this.#url;
  166. }
  167. async awaitLoaded() {
  168. await this.#deferreds.Page.lifecycleEvent.load;
  169. }
  170. awaitUnblocked() {
  171. return this.#cdpTarget.targetUnblocked;
  172. }
  173. async getOrCreateSandbox(sandbox) {
  174. if (sandbox === undefined || sandbox === '') {
  175. return this.#defaultRealm;
  176. }
  177. let maybeSandboxes = this.#realmStorage.findRealms({
  178. browsingContextId: this.id,
  179. sandbox,
  180. });
  181. if (maybeSandboxes.length === 0) {
  182. await this.#cdpTarget.cdpClient.sendCommand('Page.createIsolatedWorld', {
  183. frameId: this.id,
  184. worldName: sandbox,
  185. });
  186. // `Runtime.executionContextCreated` should be emitted by the time the
  187. // previous command is done.
  188. maybeSandboxes = this.#realmStorage.findRealms({
  189. browsingContextId: this.id,
  190. sandbox,
  191. });
  192. }
  193. if (maybeSandboxes.length !== 1) {
  194. throw Error(`Sandbox ${sandbox} wasn't created.`);
  195. }
  196. return maybeSandboxes[0];
  197. }
  198. serializeToBidiValue(maxDepth = 0, addParentField = true) {
  199. return {
  200. context: this.#id,
  201. url: this.url,
  202. children: maxDepth > 0
  203. ? this.directChildren.map((c) => c.serializeToBidiValue(maxDepth - 1, false))
  204. : null,
  205. ...(addParentField ? { parent: this.#parentId } : {}),
  206. };
  207. }
  208. onTargetInfoChanged(params) {
  209. this.#url = params.targetInfo.url;
  210. if (this.#isNavigating) {
  211. this.#eventManager.registerEvent({
  212. method: protocol_js_1.BrowsingContext.EventNames.NavigationStarted,
  213. params: {
  214. context: this.id,
  215. // TODO: The network event is send before the CDP Page.frameStartedLoading
  216. // It theory there should be a way to get the data.
  217. navigation: null,
  218. timestamp: BrowsingContextImpl.getTimestamp(),
  219. url: this.#url,
  220. },
  221. }, this.id);
  222. this.#isNavigating = false;
  223. }
  224. }
  225. #initListeners() {
  226. this.#cdpTarget.cdpClient.on('Page.frameNavigated', (params) => {
  227. if (this.id !== params.frame.id) {
  228. return;
  229. }
  230. const timestamp = BrowsingContextImpl.getTimestamp();
  231. this.#url = params.frame.url + (params.frame.urlFragment ?? '');
  232. // At the point the page is initialized, all the nested iframes from the
  233. // previous page are detached and realms are destroyed.
  234. // Remove children from context.
  235. this.#deleteAllChildren();
  236. this.#eventManager.registerEvent({
  237. method: protocol_js_1.BrowsingContext.EventNames.FragmentNavigated,
  238. params: {
  239. context: this.id,
  240. navigation: this.#loaderId ?? null,
  241. timestamp,
  242. url: this.#url,
  243. },
  244. }, this.id);
  245. });
  246. this.#cdpTarget.cdpClient.on('Page.navigatedWithinDocument', (params) => {
  247. if (this.id !== params.frameId) {
  248. return;
  249. }
  250. const timestamp = BrowsingContextImpl.getTimestamp();
  251. this.#url = params.url;
  252. this.#deferreds.Page.navigatedWithinDocument.resolve(params);
  253. // TODO: Remove this once History event for BiDi are added
  254. this.#eventManager.registerEvent({
  255. method: protocol_js_1.BrowsingContext.EventNames.FragmentNavigated,
  256. params: {
  257. context: this.id,
  258. navigation: null,
  259. timestamp,
  260. url: this.#url,
  261. },
  262. }, this.id);
  263. });
  264. this.#cdpTarget.cdpClient.on('Page.frameStartedLoading', (params) => {
  265. if (this.id !== params.frameId) {
  266. return;
  267. }
  268. this.#isNavigating = true;
  269. });
  270. this.#cdpTarget.cdpClient.on('Page.frameStoppedLoading', (params) => {
  271. if (this.id !== params.frameId) {
  272. return;
  273. }
  274. this.#isNavigating = false;
  275. });
  276. this.#cdpTarget.cdpClient.on('Page.lifecycleEvent', (params) => {
  277. if (this.id !== params.frameId) {
  278. return;
  279. }
  280. if (params.name === 'init') {
  281. this.#documentChanged(params.loaderId);
  282. this.#deferreds.documentInitialized.resolve();
  283. return;
  284. }
  285. if (params.name === 'commit') {
  286. this.#loaderId = params.loaderId;
  287. return;
  288. }
  289. // Ignore event from not current navigation.
  290. if (params.loaderId !== this.#loaderId) {
  291. return;
  292. }
  293. const timestamp = BrowsingContextImpl.getTimestamp();
  294. switch (params.name) {
  295. case 'DOMContentLoaded':
  296. this.#deferreds.Page.lifecycleEvent.DOMContentLoaded.resolve(params);
  297. this.#eventManager.registerEvent({
  298. method: protocol_js_1.BrowsingContext.EventNames.DomContentLoadedEvent,
  299. params: {
  300. context: this.id,
  301. navigation: this.#loaderId ?? null,
  302. timestamp,
  303. url: this.#url,
  304. },
  305. }, this.id);
  306. break;
  307. case 'load':
  308. this.#deferreds.Page.lifecycleEvent.load.resolve(params);
  309. this.#eventManager.registerEvent({
  310. method: protocol_js_1.BrowsingContext.EventNames.LoadEvent,
  311. params: {
  312. context: this.id,
  313. navigation: this.#loaderId ?? null,
  314. timestamp,
  315. url: this.#url,
  316. },
  317. }, this.id);
  318. break;
  319. }
  320. });
  321. this.#cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
  322. if (params.context.auxData.frameId !== this.id) {
  323. return;
  324. }
  325. // Only this execution contexts are supported for now.
  326. if (!['default', 'isolated'].includes(params.context.auxData.type)) {
  327. return;
  328. }
  329. const realm = new realm_js_1.Realm(this.#realmStorage, this.#browsingContextStorage, params.context.uniqueId, this.id, params.context.id, this.#getOrigin(params),
  330. // XXX: differentiate types.
  331. 'window',
  332. // Sandbox name for isolated world.
  333. params.context.auxData.type === 'isolated'
  334. ? params.context.name
  335. : undefined, this.#cdpTarget.cdpSessionId, this.#cdpTarget.cdpClient, this.#eventManager, this.#logger);
  336. if (params.context.auxData.isDefault) {
  337. this.#maybeDefaultRealm = realm;
  338. // Initialize ChannelProxy listeners for all the channels of all the
  339. // preload scripts related to this BrowsingContext.
  340. // TODO: extend for not default realms by the sandbox name.
  341. void Promise.all(this.#cdpTarget
  342. .getChannels(this.id)
  343. .map((channel) => channel.startListenerFromWindow(realm, this.#eventManager)));
  344. }
  345. });
  346. this.#cdpTarget.cdpClient.on('Runtime.executionContextDestroyed', (params) => {
  347. this.#realmStorage.deleteRealms({
  348. cdpSessionId: this.#cdpTarget.cdpSessionId,
  349. executionContextId: params.executionContextId,
  350. });
  351. });
  352. this.#cdpTarget.cdpClient.on('Runtime.executionContextsCleared', () => {
  353. this.#realmStorage.deleteRealms({
  354. cdpSessionId: this.#cdpTarget.cdpSessionId,
  355. });
  356. });
  357. }
  358. #getOrigin(params) {
  359. if (params.context.auxData.type === 'isolated') {
  360. // Sandbox should have the same origin as the context itself, but in CDP
  361. // it has an empty one.
  362. return this.#defaultRealm.origin;
  363. }
  364. // https://html.spec.whatwg.org/multipage/origin.html#ascii-serialisation-of-an-origin
  365. return ['://', ''].includes(params.context.origin)
  366. ? 'null'
  367. : params.context.origin;
  368. }
  369. #documentChanged(loaderId) {
  370. // Same document navigation.
  371. if (loaderId === undefined || this.#loaderId === loaderId) {
  372. if (this.#deferreds.Page.navigatedWithinDocument.isFinished) {
  373. this.#deferreds.Page.navigatedWithinDocument =
  374. new deferred_js_1.Deferred();
  375. }
  376. else {
  377. this.#logger?.(log_js_1.LogType.browsingContexts, 'Document changed (navigatedWithinDocument)');
  378. }
  379. return;
  380. }
  381. this.#resetDeferredsIfFinished();
  382. this.#loaderId = loaderId;
  383. }
  384. #resetDeferredsIfFinished() {
  385. if (this.#deferreds.documentInitialized.isFinished) {
  386. this.#deferreds.documentInitialized = new deferred_js_1.Deferred();
  387. }
  388. else {
  389. this.#logger?.(log_js_1.LogType.browsingContexts, 'Document changed (document initialized)');
  390. }
  391. if (this.#deferreds.Page.lifecycleEvent.DOMContentLoaded.isFinished) {
  392. this.#deferreds.Page.lifecycleEvent.DOMContentLoaded =
  393. new deferred_js_1.Deferred();
  394. }
  395. else {
  396. this.#logger?.(log_js_1.LogType.browsingContexts, 'Document changed (DOMContentLoaded)');
  397. }
  398. if (this.#deferreds.Page.lifecycleEvent.load.isFinished) {
  399. this.#deferreds.Page.lifecycleEvent.load =
  400. new deferred_js_1.Deferred();
  401. }
  402. else {
  403. this.#logger?.(log_js_1.LogType.browsingContexts, 'Document changed (load)');
  404. }
  405. }
  406. async navigate(url, wait) {
  407. await this.awaitUnblocked();
  408. // TODO: handle loading errors.
  409. const cdpNavigateResult = await this.#cdpTarget.cdpClient.sendCommand('Page.navigate', {
  410. url,
  411. frameId: this.id,
  412. });
  413. if (cdpNavigateResult.errorText) {
  414. throw new protocol_js_1.Message.UnknownErrorException(cdpNavigateResult.errorText);
  415. }
  416. this.#documentChanged(cdpNavigateResult.loaderId);
  417. switch (wait) {
  418. case 'none':
  419. break;
  420. case 'interactive':
  421. // No `loaderId` means same-document navigation.
  422. if (cdpNavigateResult.loaderId === undefined) {
  423. await this.#deferreds.Page.navigatedWithinDocument;
  424. }
  425. else {
  426. await this.#deferreds.Page.lifecycleEvent.DOMContentLoaded;
  427. }
  428. break;
  429. case 'complete':
  430. // No `loaderId` means same-document navigation.
  431. if (cdpNavigateResult.loaderId === undefined) {
  432. await this.#deferreds.Page.navigatedWithinDocument;
  433. }
  434. else {
  435. await this.awaitLoaded();
  436. }
  437. break;
  438. }
  439. return {
  440. result: {
  441. navigation: cdpNavigateResult.loaderId ?? null,
  442. url,
  443. },
  444. };
  445. }
  446. async reload(ignoreCache, wait) {
  447. await this.awaitUnblocked();
  448. await this.#cdpTarget.cdpClient.sendCommand('Page.reload', {
  449. ignoreCache,
  450. });
  451. this.#resetDeferredsIfFinished();
  452. switch (wait) {
  453. case 'none':
  454. break;
  455. case 'interactive':
  456. await this.#deferreds.Page.lifecycleEvent.DOMContentLoaded;
  457. break;
  458. case 'complete':
  459. await this.awaitLoaded();
  460. break;
  461. }
  462. return { result: {} };
  463. }
  464. async setViewport(viewport) {
  465. if (viewport === null) {
  466. await this.#cdpTarget.cdpClient.sendCommand('Emulation.clearDeviceMetricsOverride');
  467. }
  468. else {
  469. try {
  470. await this.#cdpTarget.cdpClient.sendCommand('Emulation.setDeviceMetricsOverride', {
  471. width: viewport.width,
  472. height: viewport.height,
  473. deviceScaleFactor: 0,
  474. mobile: false,
  475. dontSetVisibleSize: true,
  476. });
  477. }
  478. catch (err) {
  479. if (err.message.startsWith(
  480. // https://crsrc.org/c/content/browser/devtools/protocol/emulation_handler.cc;l=257;drc=2f6eee84cf98d4227e7c41718dd71b82f26d90ff
  481. 'Width and height values must be positive')) {
  482. throw new protocol_js_1.Message.UnsupportedOperationException('Provided viewport dimensions are not supported');
  483. }
  484. throw err;
  485. }
  486. }
  487. }
  488. async captureScreenshot() {
  489. // XXX: Focus the original tab after the screenshot is taken.
  490. // This is needed because the screenshot gets blocked until the active tab gets focus.
  491. await this.#cdpTarget.cdpClient.sendCommand('Page.bringToFront');
  492. let clip;
  493. if (this.isTopLevelContext()) {
  494. const { cssContentSize, cssLayoutViewport } = await this.#cdpTarget.cdpClient.sendCommand('Page.getLayoutMetrics');
  495. clip = {
  496. x: cssContentSize.x,
  497. y: cssContentSize.y,
  498. width: cssLayoutViewport.clientWidth,
  499. height: cssLayoutViewport.clientHeight,
  500. };
  501. }
  502. else {
  503. const { result: { value: iframeDocRect }, } = await this.#cdpTarget.cdpClient.sendCommand('Runtime.callFunctionOn', {
  504. functionDeclaration: String(() => {
  505. const docRect = globalThis.document.documentElement.getBoundingClientRect();
  506. return JSON.stringify({
  507. x: docRect.x,
  508. y: docRect.y,
  509. width: docRect.width,
  510. height: docRect.height,
  511. });
  512. }),
  513. executionContextId: this.#defaultRealm.executionContextId,
  514. });
  515. clip = JSON.parse(iframeDocRect);
  516. }
  517. const result = await this.#cdpTarget.cdpClient.sendCommand('Page.captureScreenshot', {
  518. clip: {
  519. ...clip,
  520. scale: 1.0,
  521. },
  522. });
  523. return {
  524. result: {
  525. data: result.data,
  526. },
  527. };
  528. }
  529. async print(params) {
  530. const cdpParams = {};
  531. if (params.background !== undefined) {
  532. cdpParams.printBackground = params.background;
  533. }
  534. if (params.margin?.bottom !== undefined) {
  535. cdpParams.marginBottom = (0, unitConversions_js_1.inchesFromCm)(params.margin.bottom);
  536. }
  537. if (params.margin?.left !== undefined) {
  538. cdpParams.marginLeft = (0, unitConversions_js_1.inchesFromCm)(params.margin.left);
  539. }
  540. if (params.margin?.right !== undefined) {
  541. cdpParams.marginRight = (0, unitConversions_js_1.inchesFromCm)(params.margin.right);
  542. }
  543. if (params.margin?.top !== undefined) {
  544. cdpParams.marginTop = (0, unitConversions_js_1.inchesFromCm)(params.margin.top);
  545. }
  546. if (params.orientation !== undefined) {
  547. cdpParams.landscape = params.orientation === 'landscape';
  548. }
  549. if (params.page?.height !== undefined) {
  550. cdpParams.paperHeight = (0, unitConversions_js_1.inchesFromCm)(params.page.height);
  551. }
  552. if (params.page?.width !== undefined) {
  553. cdpParams.paperWidth = (0, unitConversions_js_1.inchesFromCm)(params.page.width);
  554. }
  555. if (params.pageRanges !== undefined) {
  556. cdpParams.pageRanges = params.pageRanges.join(',');
  557. }
  558. if (params.scale !== undefined) {
  559. cdpParams.scale = params.scale;
  560. }
  561. if (params.shrinkToFit !== undefined) {
  562. cdpParams.preferCSSPageSize = !params.shrinkToFit;
  563. }
  564. const result = await this.#cdpTarget.cdpClient.sendCommand('Page.printToPDF', cdpParams);
  565. return {
  566. result: {
  567. data: result.data,
  568. },
  569. };
  570. }
  571. }
  572. exports.BrowsingContextImpl = BrowsingContextImpl;
  573. //# sourceMappingURL=browsingContextImpl.js.map