browsingContextProcessor.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.BrowsingContextProcessor = void 0;
  4. const protocol_js_1 = require("../../../protocol/protocol.js");
  5. const log_js_1 = require("../../../utils/log.js");
  6. const InputStateManager_js_1 = require("../input/InputStateManager.js");
  7. const ActionDispatcher_js_1 = require("../input/ActionDispatcher.js");
  8. const PreloadScriptStorage_js_1 = require("./PreloadScriptStorage.js");
  9. const browsingContextImpl_js_1 = require("./browsingContextImpl.js");
  10. const cdpTarget_js_1 = require("./cdpTarget.js");
  11. const bidiPreloadScript_1 = require("./bidiPreloadScript");
  12. class BrowsingContextProcessor {
  13. #browsingContextStorage;
  14. #cdpConnection;
  15. #eventManager;
  16. #logger;
  17. #realmStorage;
  18. #selfTargetId;
  19. #preloadScriptStorage = new PreloadScriptStorage_js_1.PreloadScriptStorage();
  20. #inputStateManager = new InputStateManager_js_1.InputStateManager();
  21. constructor(cdpConnection, selfTargetId, eventManager, browsingContextStorage, realmStorage, logger) {
  22. this.#cdpConnection = cdpConnection;
  23. this.#selfTargetId = selfTargetId;
  24. this.#eventManager = eventManager;
  25. this.#browsingContextStorage = browsingContextStorage;
  26. this.#realmStorage = realmStorage;
  27. this.#logger = logger;
  28. this.#setEventListeners(this.#cdpConnection.browserClient());
  29. }
  30. /**
  31. * This method is called for each CDP session, since this class is responsible
  32. * for creating and destroying all targets and browsing contexts.
  33. */
  34. #setEventListeners(cdpClient) {
  35. cdpClient.on('Target.attachedToTarget', (params) => {
  36. this.#handleAttachedToTargetEvent(params, cdpClient);
  37. });
  38. cdpClient.on('Target.detachedFromTarget', (params) => {
  39. this.#handleDetachedFromTargetEvent(params);
  40. });
  41. cdpClient.on('Target.targetInfoChanged', (params) => {
  42. this.#handleTargetInfoChangedEvent(params);
  43. });
  44. cdpClient.on('Page.frameAttached', (params) => {
  45. this.#handleFrameAttachedEvent(params);
  46. });
  47. cdpClient.on('Page.frameDetached', (params) => {
  48. this.#handleFrameDetachedEvent(params);
  49. });
  50. }
  51. #handleFrameAttachedEvent(params) {
  52. const parentBrowsingContext = this.#browsingContextStorage.findContext(params.parentFrameId);
  53. if (parentBrowsingContext !== undefined) {
  54. browsingContextImpl_js_1.BrowsingContextImpl.create(parentBrowsingContext.cdpTarget, this.#realmStorage, params.frameId, params.parentFrameId, this.#eventManager, this.#browsingContextStorage, this.#logger);
  55. }
  56. }
  57. #handleFrameDetachedEvent(params) {
  58. // In case of OOPiF no need in deleting BrowsingContext.
  59. if (params.reason === 'swap') {
  60. return;
  61. }
  62. this.#browsingContextStorage.findContext(params.frameId)?.delete();
  63. }
  64. #handleAttachedToTargetEvent(params, parentSessionCdpClient) {
  65. const { sessionId, targetInfo } = params;
  66. const targetCdpClient = this.#cdpConnection.getCdpClient(sessionId);
  67. if (!this.#isValidTarget(targetInfo)) {
  68. // DevTools or some other not supported by BiDi target. Just release
  69. // debugger and ignore them.
  70. targetCdpClient
  71. .sendCommand('Runtime.runIfWaitingForDebugger')
  72. .then(() => parentSessionCdpClient.sendCommand('Target.detachFromTarget', params))
  73. .catch((error) => this.#logger?.(log_js_1.LogType.system, error));
  74. return;
  75. }
  76. this.#logger?.(log_js_1.LogType.browsingContexts, 'AttachedToTarget event received:', JSON.stringify(params, null, 2));
  77. this.#setEventListeners(targetCdpClient);
  78. const maybeContext = this.#browsingContextStorage.findContext(targetInfo.targetId);
  79. const cdpTarget = cdpTarget_js_1.CdpTarget.create(targetInfo.targetId, maybeContext?.parentId ?? null, targetCdpClient, sessionId, this.#realmStorage, this.#eventManager, this.#preloadScriptStorage);
  80. if (maybeContext) {
  81. // OOPiF.
  82. maybeContext.updateCdpTarget(cdpTarget);
  83. }
  84. else {
  85. // New context.
  86. browsingContextImpl_js_1.BrowsingContextImpl.create(cdpTarget, this.#realmStorage, targetInfo.targetId, null, this.#eventManager, this.#browsingContextStorage, this.#logger);
  87. }
  88. }
  89. #handleDetachedFromTargetEvent(params) {
  90. // XXX: params.targetId is deprecated. Update this class to track using
  91. // params.sessionId instead.
  92. // https://github.com/GoogleChromeLabs/chromium-bidi/issues/60
  93. const contextId = params.targetId;
  94. this.#browsingContextStorage.findContext(contextId)?.delete();
  95. this.#preloadScriptStorage
  96. .findPreloadScripts({ targetId: contextId })
  97. .map((preloadScript) => preloadScript.cdpTargetIsGone(contextId));
  98. }
  99. #handleTargetInfoChangedEvent(params) {
  100. const contextId = params.targetInfo.targetId;
  101. this.#browsingContextStorage
  102. .findContext(contextId)
  103. ?.onTargetInfoChanged(params);
  104. }
  105. async #getRealm(target) {
  106. if ('realm' in target) {
  107. return this.#realmStorage.getRealm({
  108. realmId: target.realm,
  109. });
  110. }
  111. const context = this.#browsingContextStorage.getContext(target.context);
  112. return context.getOrCreateSandbox(target.sandbox);
  113. }
  114. process_browsingContext_getTree(params) {
  115. const resultContexts = params.root === undefined
  116. ? this.#browsingContextStorage.getTopLevelContexts()
  117. : [this.#browsingContextStorage.getContext(params.root)];
  118. return {
  119. result: {
  120. contexts: resultContexts.map((c) => c.serializeToBidiValue(params.maxDepth ?? Number.MAX_VALUE)),
  121. },
  122. };
  123. }
  124. async process_browsingContext_create(params) {
  125. const browserCdpClient = this.#cdpConnection.browserClient();
  126. let referenceContext;
  127. if (params.referenceContext !== undefined) {
  128. referenceContext = this.#browsingContextStorage.getContext(params.referenceContext);
  129. if (!referenceContext.isTopLevelContext()) {
  130. throw new protocol_js_1.Message.InvalidArgumentException(`referenceContext should be a top-level context`);
  131. }
  132. }
  133. let result;
  134. switch (params.type) {
  135. case 'tab':
  136. result = await browserCdpClient.sendCommand('Target.createTarget', {
  137. url: 'about:blank',
  138. newWindow: false,
  139. });
  140. break;
  141. case 'window':
  142. result = await browserCdpClient.sendCommand('Target.createTarget', {
  143. url: 'about:blank',
  144. newWindow: true,
  145. });
  146. break;
  147. }
  148. // Wait for the new tab to be loaded to avoid race conditions in the
  149. // `browsingContext` events, when the `browsingContext.domContentLoaded` and
  150. // `browsingContext.load` events from the initial `about:blank` navigation
  151. // are emitted after the next navigation is started.
  152. // Details: https://github.com/web-platform-tests/wpt/issues/35846
  153. const contextId = result.targetId;
  154. const context = this.#browsingContextStorage.getContext(contextId);
  155. await context.awaitLoaded();
  156. return {
  157. result: {
  158. context: context.id,
  159. },
  160. };
  161. }
  162. process_browsingContext_navigate(params) {
  163. const context = this.#browsingContextStorage.getContext(params.context);
  164. return context.navigate(params.url, params.wait ?? 'none');
  165. }
  166. process_browsingContext_reload(params) {
  167. const context = this.#browsingContextStorage.getContext(params.context);
  168. return context.reload(params.ignoreCache ?? false, params.wait ?? 'none');
  169. }
  170. async process_browsingContext_captureScreenshot(params) {
  171. const context = this.#browsingContextStorage.getContext(params.context);
  172. return context.captureScreenshot();
  173. }
  174. async process_browsingContext_print(params) {
  175. const context = this.#browsingContextStorage.getContext(params.context);
  176. return context.print(params);
  177. }
  178. async process_script_addPreloadScript(params) {
  179. const preloadScript = new bidiPreloadScript_1.BidiPreloadScript(params);
  180. this.#preloadScriptStorage.addPreloadScript(preloadScript);
  181. const cdpTargets = new Set(
  182. // TODO: The unique target can be in a non-top-level browsing context.
  183. // We need all the targets.
  184. // To get them, we can walk through all the contexts and collect their targets into the set.
  185. params.context === undefined || params.context === null
  186. ? this.#browsingContextStorage
  187. .getTopLevelContexts()
  188. .map((context) => context.cdpTarget)
  189. : [this.#browsingContextStorage.getContext(params.context).cdpTarget]);
  190. await preloadScript.initInTargets(cdpTargets);
  191. return {
  192. result: {
  193. script: preloadScript.id,
  194. },
  195. };
  196. }
  197. async process_script_removePreloadScript(params) {
  198. const bidiId = params.script;
  199. const scripts = this.#preloadScriptStorage.findPreloadScripts({
  200. id: bidiId,
  201. });
  202. if (scripts.length === 0) {
  203. throw new protocol_js_1.Message.NoSuchScriptException(`No preload script with BiDi ID '${bidiId}'`);
  204. }
  205. await Promise.all(scripts.map((script) => script.remove()));
  206. this.#preloadScriptStorage.removeBiDiPreloadScripts({
  207. id: bidiId,
  208. });
  209. return { result: {} };
  210. }
  211. async process_script_evaluate(params) {
  212. const realm = await this.#getRealm(params.target);
  213. return realm.scriptEvaluate(params.expression, params.awaitPromise, params.resultOwnership ?? 'none', params.serializationOptions ?? {});
  214. }
  215. process_script_getRealms(params) {
  216. if (params.context !== undefined) {
  217. // Make sure the context is known.
  218. this.#browsingContextStorage.getContext(params.context);
  219. }
  220. const realms = this.#realmStorage
  221. .findRealms({
  222. browsingContextId: params.context,
  223. type: params.type,
  224. })
  225. .map((realm) => realm.toBiDi());
  226. return { result: { realms } };
  227. }
  228. async process_script_callFunction(params) {
  229. const realm = await this.#getRealm(params.target);
  230. return realm.callFunction(params.functionDeclaration, params.this ?? {
  231. type: 'undefined',
  232. }, // `this` is `undefined` by default.
  233. params.arguments ?? [], // `arguments` is `[]` by default.
  234. params.awaitPromise, params.resultOwnership ?? 'none', params.serializationOptions ?? {});
  235. }
  236. async process_script_disown(params) {
  237. const realm = await this.#getRealm(params.target);
  238. await Promise.all(params.handles.map(async (h) => realm.disown(h)));
  239. return { result: {} };
  240. }
  241. async process_input_performActions(params) {
  242. const context = this.#browsingContextStorage.getContext(params.context);
  243. const inputState = this.#inputStateManager.get(context.top);
  244. const actionsByTick = this.#getActionsByTick(params, inputState);
  245. const dispatcher = new ActionDispatcher_js_1.ActionDispatcher(inputState, context, await ActionDispatcher_js_1.ActionDispatcher.isMacOS(context).catch(() => false));
  246. await dispatcher.dispatchActions(actionsByTick);
  247. return { result: {} };
  248. }
  249. #getActionsByTick(params, inputState) {
  250. const actionsByTick = [];
  251. for (const action of params.actions) {
  252. switch (action.type) {
  253. case protocol_js_1.Input.SourceActionsType.Pointer: {
  254. action.parameters ??= { pointerType: protocol_js_1.Input.PointerType.Mouse };
  255. action.parameters.pointerType ??= protocol_js_1.Input.PointerType.Mouse;
  256. const source = inputState.getOrCreate(action.id, protocol_js_1.Input.SourceActionsType.Pointer, action.parameters.pointerType);
  257. if (source.subtype !== action.parameters.pointerType) {
  258. throw new protocol_js_1.Message.InvalidArgumentException(`Expected input source ${action.id} to be ${source.subtype}; got ${action.parameters.pointerType}.`);
  259. }
  260. break;
  261. }
  262. default:
  263. inputState.getOrCreate(action.id, action.type);
  264. }
  265. const actions = action.actions.map((item) => ({
  266. id: action.id,
  267. action: item,
  268. }));
  269. for (let i = 0; i < actions.length; i++) {
  270. if (actionsByTick.length === i) {
  271. actionsByTick.push([]);
  272. }
  273. actionsByTick[i].push(actions[i]);
  274. }
  275. }
  276. return actionsByTick;
  277. }
  278. async process_input_releaseActions(params) {
  279. const context = this.#browsingContextStorage.getContext(params.context);
  280. const topContext = context.top;
  281. const inputState = this.#inputStateManager.get(topContext);
  282. const dispatcher = new ActionDispatcher_js_1.ActionDispatcher(inputState, context, await ActionDispatcher_js_1.ActionDispatcher.isMacOS(context).catch(() => false));
  283. await dispatcher.dispatchTickActions(inputState.cancelList.reverse());
  284. this.#inputStateManager.delete(topContext);
  285. return { result: {} };
  286. }
  287. async process_browsingContext_setViewport(params) {
  288. const context = this.#browsingContextStorage.getContext(params.context);
  289. if (!context.isTopLevelContext()) {
  290. throw new protocol_js_1.Message.InvalidArgumentException('Emulating viewport is only supported on the top-level context');
  291. }
  292. await context.setViewport(params.viewport);
  293. return { result: {} };
  294. }
  295. async process_browsingContext_close(commandParams) {
  296. const browserCdpClient = this.#cdpConnection.browserClient();
  297. const context = this.#browsingContextStorage.getContext(commandParams.context);
  298. if (!context.isTopLevelContext()) {
  299. throw new protocol_js_1.Message.InvalidArgumentException('A top-level browsing context cannot be closed.');
  300. }
  301. const detachedFromTargetPromise = new Promise((resolve) => {
  302. const onContextDestroyed = (eventParams) => {
  303. if (eventParams.targetId === commandParams.context) {
  304. browserCdpClient.off('Target.detachedFromTarget', onContextDestroyed);
  305. resolve();
  306. }
  307. };
  308. browserCdpClient.on('Target.detachedFromTarget', onContextDestroyed);
  309. });
  310. await browserCdpClient.sendCommand('Target.closeTarget', {
  311. targetId: commandParams.context,
  312. });
  313. // Sometimes CDP command finishes before `detachedFromTarget` event,
  314. // sometimes after. Wait for the CDP command to be finished, and then wait
  315. // for `detachedFromTarget` if it hasn't emitted.
  316. await detachedFromTargetPromise;
  317. return { result: {} };
  318. }
  319. #isValidTarget(target) {
  320. if (target.targetId === this.#selfTargetId) {
  321. return false;
  322. }
  323. return ['page', 'iframe'].includes(target.type);
  324. }
  325. async process_cdp_sendCommand(params) {
  326. const client = params.session
  327. ? this.#cdpConnection.getCdpClient(params.session)
  328. : this.#cdpConnection.browserClient();
  329. const sendCdpCommandResult = await client.sendCommand(params.method, params.params);
  330. return {
  331. result: sendCdpCommandResult,
  332. session: params.session,
  333. };
  334. }
  335. process_cdp_getSession(params) {
  336. const context = params.context;
  337. const sessionId = this.#browsingContextStorage.getContext(context).cdpTarget.cdpSessionId;
  338. if (sessionId === undefined) {
  339. return { result: { session: null } };
  340. }
  341. return { result: { session: sessionId } };
  342. }
  343. }
  344. exports.BrowsingContextProcessor = BrowsingContextProcessor;
  345. //# sourceMappingURL=browsingContextProcessor.js.map