BrowserContext.ts (10391B)
1 /** 2 * @license 3 * Copyright 2022 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; 8 9 import type {Permission} from '../api/Browser.js'; 10 import {WEB_PERMISSION_TO_PROTOCOL_PERMISSION} from '../api/Browser.js'; 11 import type {BrowserContextEvents} from '../api/BrowserContext.js'; 12 import {BrowserContext, BrowserContextEvent} from '../api/BrowserContext.js'; 13 import {PageEvent, type Page} from '../api/Page.js'; 14 import type {Target} from '../api/Target.js'; 15 import type {Cookie, CookieData} from '../common/Cookie.js'; 16 import {EventEmitter} from '../common/EventEmitter.js'; 17 import {debugError} from '../common/util.js'; 18 import type {Viewport} from '../common/Viewport.js'; 19 import {assert} from '../util/assert.js'; 20 import {bubble} from '../util/decorators.js'; 21 22 import type {BidiBrowser} from './Browser.js'; 23 import type {BrowsingContext} from './core/BrowsingContext.js'; 24 import {UserContext} from './core/UserContext.js'; 25 import type {BidiFrame} from './Frame.js'; 26 import { 27 BidiPage, 28 bidiToPuppeteerCookie, 29 cdpSpecificCookiePropertiesFromPuppeteerToBidi, 30 convertCookiesExpiryCdpToBiDi, 31 convertCookiesPartitionKeyFromPuppeteerToBiDi, 32 convertCookiesSameSiteCdpToBiDi, 33 } from './Page.js'; 34 import {BidiWorkerTarget} from './Target.js'; 35 import {BidiFrameTarget, BidiPageTarget} from './Target.js'; 36 import type {BidiWebWorker} from './WebWorker.js'; 37 38 /** 39 * @internal 40 */ 41 export interface BidiBrowserContextOptions { 42 defaultViewport: Viewport | null; 43 } 44 45 /** 46 * @internal 47 */ 48 export class BidiBrowserContext extends BrowserContext { 49 static from( 50 browser: BidiBrowser, 51 userContext: UserContext, 52 options: BidiBrowserContextOptions, 53 ): BidiBrowserContext { 54 const context = new BidiBrowserContext(browser, userContext, options); 55 context.#initialize(); 56 return context; 57 } 58 59 @bubble() 60 accessor trustedEmitter = new EventEmitter<BrowserContextEvents>(); 61 62 readonly #browser: BidiBrowser; 63 readonly #defaultViewport: Viewport | null; 64 // This is public because of cookies. 65 readonly userContext: UserContext; 66 readonly #pages = new WeakMap<BrowsingContext, BidiPage>(); 67 readonly #targets = new Map< 68 BidiPage, 69 [ 70 BidiPageTarget, 71 Map<BidiFrame | BidiWebWorker, BidiFrameTarget | BidiWorkerTarget>, 72 ] 73 >(); 74 75 #overrides: Array<{origin: string; permission: Permission}> = []; 76 77 private constructor( 78 browser: BidiBrowser, 79 userContext: UserContext, 80 options: BidiBrowserContextOptions, 81 ) { 82 super(); 83 this.#browser = browser; 84 this.userContext = userContext; 85 this.#defaultViewport = options.defaultViewport; 86 } 87 88 #initialize() { 89 // Create targets for existing browsing contexts. 90 for (const browsingContext of this.userContext.browsingContexts) { 91 this.#createPage(browsingContext); 92 } 93 94 this.userContext.on('browsingcontext', ({browsingContext}) => { 95 const page = this.#createPage(browsingContext); 96 97 // We need to wait for the DOMContentLoaded as the 98 // browsingContext still may be navigating from the about:blank 99 if (browsingContext.originalOpener) { 100 for (const context of this.userContext.browsingContexts) { 101 if (context.id === browsingContext.originalOpener) { 102 this.#pages 103 .get(context)! 104 .trustedEmitter.emit(PageEvent.Popup, page); 105 } 106 } 107 } 108 }); 109 this.userContext.on('closed', () => { 110 this.trustedEmitter.removeAllListeners(); 111 }); 112 } 113 114 #createPage(browsingContext: BrowsingContext): BidiPage { 115 const page = BidiPage.from(this, browsingContext); 116 this.#pages.set(browsingContext, page); 117 page.trustedEmitter.on(PageEvent.Close, () => { 118 this.#pages.delete(browsingContext); 119 }); 120 121 // -- Target stuff starts here -- 122 const pageTarget = new BidiPageTarget(page); 123 const pageTargets = new Map(); 124 this.#targets.set(page, [pageTarget, pageTargets]); 125 126 page.trustedEmitter.on(PageEvent.FrameAttached, frame => { 127 const bidiFrame = frame as BidiFrame; 128 const target = new BidiFrameTarget(bidiFrame); 129 pageTargets.set(bidiFrame, target); 130 this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target); 131 }); 132 page.trustedEmitter.on(PageEvent.FrameNavigated, frame => { 133 const bidiFrame = frame as BidiFrame; 134 const target = pageTargets.get(bidiFrame); 135 // If there is no target, then this is the page's frame. 136 if (target === undefined) { 137 this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, pageTarget); 138 } else { 139 this.trustedEmitter.emit(BrowserContextEvent.TargetChanged, target); 140 } 141 }); 142 page.trustedEmitter.on(PageEvent.FrameDetached, frame => { 143 const bidiFrame = frame as BidiFrame; 144 const target = pageTargets.get(bidiFrame); 145 if (target === undefined) { 146 return; 147 } 148 pageTargets.delete(bidiFrame); 149 this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target); 150 }); 151 152 page.trustedEmitter.on(PageEvent.WorkerCreated, worker => { 153 const bidiWorker = worker as BidiWebWorker; 154 const target = new BidiWorkerTarget(bidiWorker); 155 pageTargets.set(bidiWorker, target); 156 this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, target); 157 }); 158 page.trustedEmitter.on(PageEvent.WorkerDestroyed, worker => { 159 const bidiWorker = worker as BidiWebWorker; 160 const target = pageTargets.get(bidiWorker); 161 if (target === undefined) { 162 return; 163 } 164 pageTargets.delete(worker); 165 this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, target); 166 }); 167 168 page.trustedEmitter.on(PageEvent.Close, () => { 169 this.#targets.delete(page); 170 this.trustedEmitter.emit(BrowserContextEvent.TargetDestroyed, pageTarget); 171 }); 172 this.trustedEmitter.emit(BrowserContextEvent.TargetCreated, pageTarget); 173 // -- Target stuff ends here -- 174 175 return page; 176 } 177 178 override targets(): Target[] { 179 return [...this.#targets.values()].flatMap(([target, frames]) => { 180 return [target, ...frames.values()]; 181 }); 182 } 183 184 override async newPage(): Promise<Page> { 185 using _guard = await this.waitForScreenshotOperations(); 186 187 const context = await this.userContext.createBrowsingContext( 188 Bidi.BrowsingContext.CreateType.Tab, 189 ); 190 const page = this.#pages.get(context)!; 191 if (!page) { 192 throw new Error('Page is not found'); 193 } 194 if (this.#defaultViewport) { 195 try { 196 await page.setViewport(this.#defaultViewport); 197 } catch { 198 // No support for setViewport in Firefox. 199 } 200 } 201 202 return page; 203 } 204 205 override async close(): Promise<void> { 206 assert( 207 this.userContext.id !== UserContext.DEFAULT, 208 'Default BrowserContext cannot be closed!', 209 ); 210 211 try { 212 await this.userContext.remove(); 213 } catch (error) { 214 debugError(error); 215 } 216 217 this.#targets.clear(); 218 } 219 220 override browser(): BidiBrowser { 221 return this.#browser; 222 } 223 224 override async pages(): Promise<BidiPage[]> { 225 return [...this.userContext.browsingContexts].map(context => { 226 return this.#pages.get(context)!; 227 }); 228 } 229 230 override async overridePermissions( 231 origin: string, 232 permissions: Permission[], 233 ): Promise<void> { 234 const permissionsSet = new Set( 235 permissions.map(permission => { 236 const protocolPermission = 237 WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission); 238 if (!protocolPermission) { 239 throw new Error('Unknown permission: ' + permission); 240 } 241 return permission; 242 }), 243 ); 244 await Promise.all( 245 Array.from(WEB_PERMISSION_TO_PROTOCOL_PERMISSION.keys()).map( 246 permission => { 247 const result = this.userContext.setPermissions( 248 origin, 249 { 250 name: permission, 251 }, 252 permissionsSet.has(permission) 253 ? Bidi.Permissions.PermissionState.Granted 254 : Bidi.Permissions.PermissionState.Denied, 255 ); 256 this.#overrides.push({origin, permission}); 257 // TODO: some permissions are outdated and setting them to denied does 258 // not work. 259 if (!permissionsSet.has(permission)) { 260 return result.catch(debugError); 261 } 262 return result; 263 }, 264 ), 265 ); 266 } 267 268 override async clearPermissionOverrides(): Promise<void> { 269 const promises = this.#overrides.map(({permission, origin}) => { 270 return this.userContext 271 .setPermissions( 272 origin, 273 { 274 name: permission, 275 }, 276 Bidi.Permissions.PermissionState.Prompt, 277 ) 278 .catch(debugError); 279 }); 280 this.#overrides = []; 281 await Promise.all(promises); 282 } 283 284 override get id(): string | undefined { 285 if (this.userContext.id === UserContext.DEFAULT) { 286 return undefined; 287 } 288 return this.userContext.id; 289 } 290 291 override async cookies(): Promise<Cookie[]> { 292 const cookies = await this.userContext.getCookies(); 293 return cookies.map(cookie => { 294 return bidiToPuppeteerCookie(cookie, true); 295 }); 296 } 297 298 override async setCookie(...cookies: CookieData[]): Promise<void> { 299 await Promise.all( 300 cookies.map(async cookie => { 301 const bidiCookie: Bidi.Storage.PartialCookie = { 302 domain: cookie.domain, 303 name: cookie.name, 304 value: { 305 type: 'string', 306 value: cookie.value, 307 }, 308 ...(cookie.path !== undefined ? {path: cookie.path} : {}), 309 ...(cookie.httpOnly !== undefined ? {httpOnly: cookie.httpOnly} : {}), 310 ...(cookie.secure !== undefined ? {secure: cookie.secure} : {}), 311 ...(cookie.sameSite !== undefined 312 ? {sameSite: convertCookiesSameSiteCdpToBiDi(cookie.sameSite)} 313 : {}), 314 ...{expiry: convertCookiesExpiryCdpToBiDi(cookie.expires)}, 315 // Chrome-specific properties. 316 ...cdpSpecificCookiePropertiesFromPuppeteerToBidi( 317 cookie, 318 'sameParty', 319 'sourceScheme', 320 'priority', 321 'url', 322 ), 323 }; 324 return await this.userContext.setCookie( 325 bidiCookie, 326 convertCookiesPartitionKeyFromPuppeteerToBiDi(cookie.partitionKey), 327 ); 328 }), 329 ); 330 } 331 }