2019-12-19 16:53:24 -08:00
/ * *
* Copyright 2018 Google Inc . All rights reserved .
* Modifications 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 .
* /
2020-06-25 08:30:56 -07:00
import { BrowserBase , BrowserOptions , BrowserContextOptions } from '../browser' ;
import { assertBrowserContextIsNotOwned , BrowserContext , BrowserContextBase , validateBrowserContextOptions , verifyGeolocation } from '../browserContext' ;
2019-12-20 13:07:14 -08:00
import { Events } from '../events' ;
2020-06-29 16:37:01 -07:00
import { assert , deprecate , helper , RegisteredListener } from '../helper' ;
2019-12-20 13:07:14 -08:00
import * as network from '../network' ;
2020-03-19 16:25:12 -07:00
import { Page , PageBinding } from '../page' ;
2020-02-04 19:41:38 -08:00
import { ConnectionTransport , SlowMoTransport } from '../transport' ;
2020-03-05 17:22:57 -08:00
import * as types from '../types' ;
2020-03-09 12:32:42 -07:00
import { ConnectionEvents , FFConnection } from './ffConnection' ;
2020-02-26 12:42:20 -08:00
import { headersArray } from './ffNetworkManager' ;
2020-03-05 17:22:57 -08:00
import { FFPage } from './ffPage' ;
import { Protocol } from './protocol' ;
2020-01-07 16:13:49 -08:00
2020-04-02 17:56:14 -07:00
export class FFBrowser extends BrowserBase {
2019-12-19 16:53:24 -08:00
_connection : FFConnection ;
2020-03-09 12:32:42 -07:00
readonly _ffPages : Map < string , FFPage > ;
2020-02-24 08:53:30 -08:00
readonly _contexts : Map < string , FFBrowserContext > ;
2019-12-19 16:53:24 -08:00
private _eventListeners : RegisteredListener [ ] ;
2020-05-14 13:22:33 -07:00
static async connect ( transport : ConnectionTransport , options : BrowserOptions ) : Promise < FFBrowser > {
2020-06-16 17:11:19 -07:00
const connection = new FFConnection ( SlowMoTransport . wrap ( transport , options . slowMo ) , options . loggers ) ;
2020-05-14 13:22:33 -07:00
const browser = new FFBrowser ( connection , options ) ;
2020-05-21 15:13:16 -07:00
const promises : Promise < any > [ ] = [
connection . send ( 'Browser.enable' , { attachToDefaultContext : ! ! options . persistent } ) ,
] ;
if ( options . persistent ) {
browser . _defaultContext = new FFBrowserContext ( browser , null , options . persistent ) ;
promises . push ( ( browser . _defaultContext as FFBrowserContext ) . _initialize ( ) ) ;
}
await Promise . all ( promises ) ;
2019-12-19 16:53:24 -08:00
return browser ;
}
2020-05-14 13:22:33 -07:00
constructor ( connection : FFConnection , options : BrowserOptions ) {
super ( options ) ;
2019-12-19 16:53:24 -08:00
this . _connection = connection ;
2020-03-09 12:32:42 -07:00
this . _ffPages = new Map ( ) ;
2019-12-19 16:53:24 -08:00
this . _contexts = new Map ( ) ;
2020-06-29 16:26:32 -07:00
this . _connection . on ( ConnectionEvents . Disconnected , ( ) = > this . _didClose ( ) ) ;
2019-12-19 16:53:24 -08:00
this . _eventListeners = [
2020-03-06 16:49:48 -08:00
helper . addEventListener ( this . _connection , 'Browser.attachedToTarget' , this . _onAttachedToTarget . bind ( this ) ) ,
helper . addEventListener ( this . _connection , 'Browser.detachedFromTarget' , this . _onDetachedFromTarget . bind ( this ) ) ,
2020-04-07 15:01:42 -07:00
helper . addEventListener ( this . _connection , 'Browser.downloadCreated' , this . _onDownloadCreated . bind ( this ) ) ,
helper . addEventListener ( this . _connection , 'Browser.downloadFinished' , this . _onDownloadFinished . bind ( this ) ) ,
2019-12-19 16:53:24 -08:00
] ;
}
isConnected ( ) : boolean {
return ! this . _connection . _closed ;
}
async newContext ( options : BrowserContextOptions = { } ) : Promise < BrowserContext > {
2020-02-24 08:53:30 -08:00
options = validateBrowserContextOptions ( options ) ;
2020-05-12 15:13:48 -07:00
if ( options . isMobile )
throw new Error ( 'options.isMobile is not supported in Firefox' ) ;
const { browserContextId } = await this . _connection . send ( 'Browser.createBrowserContext' , { removeOnDetach : true } ) ;
2020-02-24 08:53:30 -08:00
const context = new FFBrowserContext ( this , browserContextId , options ) ;
2020-01-13 13:32:44 -08:00
await context . _initialize ( ) ;
2019-12-19 16:53:24 -08:00
this . _contexts . set ( browserContextId , context ) ;
return context ;
}
2020-02-10 10:41:45 -08:00
contexts ( ) : BrowserContext [ ] {
2020-02-05 12:41:55 -08:00
return Array . from ( this . _contexts . values ( ) ) ;
2019-12-19 16:53:24 -08:00
}
2020-03-06 16:49:48 -08:00
_onDetachedFromTarget ( payload : Protocol.Browser.detachedFromTargetPayload ) {
2020-03-09 12:32:42 -07:00
const ffPage = this . _ffPages . get ( payload . targetId ) ! ;
this . _ffPages . delete ( payload . targetId ) ;
ffPage . didClose ( ) ;
2019-12-19 16:53:24 -08:00
}
2020-03-13 11:33:33 -07:00
_onAttachedToTarget ( payload : Protocol.Browser.attachedToTargetPayload ) {
2020-03-06 16:49:48 -08:00
const { targetId , browserContextId , openerId , type } = payload . targetInfo ;
2020-03-09 12:32:42 -07:00
assert ( type === 'page' ) ;
2020-05-20 16:30:04 -07:00
const context = browserContextId ? this . _contexts . get ( browserContextId ) ! : this . _defaultContext as FFBrowserContext ;
2020-04-18 19:58:11 -07:00
assert ( context , ` Unknown context id: ${ browserContextId } , _defaultContext: ${ this . _defaultContext } ` ) ;
2020-03-09 12:32:42 -07:00
const session = this . _connection . createSession ( payload . sessionId , type ) ;
const opener = openerId ? this . _ffPages . get ( openerId ) ! : null ;
const ffPage = new FFPage ( session , context , opener ) ;
this . _ffPages . set ( targetId , ffPage ) ;
2020-03-13 11:33:33 -07:00
ffPage . pageOrError ( ) . then ( async ( ) = > {
2020-03-19 16:25:12 -07:00
const page = ffPage . _page ;
context . emit ( Events . BrowserContext . Page , page ) ;
2020-03-13 11:33:33 -07:00
if ( ! opener )
return ;
const openerPage = await opener . pageOrError ( ) ;
if ( openerPage instanceof Page && ! openerPage . isClosed ( ) )
2020-03-19 16:25:12 -07:00
openerPage . emit ( Events . Page . Popup , page ) ;
2020-03-13 11:33:33 -07:00
} ) ;
2020-02-06 19:01:03 -08:00
}
2020-04-07 15:01:42 -07:00
_onDownloadCreated ( payload : Protocol.Browser.downloadCreatedPayload ) {
const ffPage = this . _ffPages . get ( payload . pageTargetId ) ! ;
assert ( ffPage ) ;
if ( ! ffPage )
return ;
2020-04-29 18:36:24 -07:00
let originPage = ffPage . _initializedPage ;
// If it's a new window download, report it on the opener page.
if ( ! originPage ) {
// Resume the page creation with an error. The page will automatically close right
// after the download begins.
ffPage . _pageCallback ( new Error ( 'Starting new page download' ) ) ;
if ( ffPage . _opener )
originPage = ffPage . _opener . _initializedPage ;
}
if ( ! originPage )
return ;
2020-05-12 19:23:08 -07:00
this . _downloadCreated ( originPage , payload . uuid , payload . url , payload . suggestedFileName ) ;
2020-04-07 15:01:42 -07:00
}
_onDownloadFinished ( payload : Protocol.Browser.downloadFinishedPayload ) {
const error = payload . canceled ? 'canceled' : payload . error ;
this . _downloadFinished ( payload . uuid , error ) ;
}
2020-04-03 16:34:07 -07:00
_disconnect() {
2019-12-19 16:53:24 -08:00
helper . removeEventListeners ( this . _eventListeners ) ;
2020-02-06 12:41:43 -08:00
this . _connection . close ( ) ;
2020-04-02 16:57:12 -07:00
}
2019-12-19 16:53:24 -08:00
}
2020-03-05 17:22:57 -08:00
export class FFBrowserContext extends BrowserContextBase {
2020-02-24 08:53:30 -08:00
readonly _browser : FFBrowser ;
readonly _browserContextId : string | null ;
2020-06-25 08:30:56 -07:00
constructor ( browser : FFBrowser , browserContextId : string | null , options : types.BrowserContextOptions ) {
2020-06-29 16:26:32 -07:00
super ( browser , options , ! browserContextId ) ;
2020-02-24 08:53:30 -08:00
this . _browser = browser ;
this . _browserContextId = browserContextId ;
2020-06-05 13:50:15 -07:00
this . _authenticateProxyViaHeader ( ) ;
2020-02-24 08:53:30 -08:00
}
async _initialize() {
2020-05-21 15:13:16 -07:00
assert ( ! this . _ffPages ( ) . length ) ;
2020-05-12 15:13:48 -07:00
const browserContextId = this . _browserContextId || undefined ;
2020-05-27 22:16:54 -07:00
const promises : Promise < any > [ ] = [ super . _initialize ( ) ] ;
2020-05-21 15:13:16 -07:00
if ( this . _browser . _options . downloadsPath ) {
promises . push ( this . _browser . _connection . send ( 'Browser.setDownloadOptions' , {
2020-05-12 15:13:48 -07:00
browserContextId ,
downloadOptions : {
behavior : this._options.acceptDownloads ? 'saveToDisk' : 'cancel' ,
2020-05-14 13:22:33 -07:00
downloadsDir : this._browser._options.downloadsPath ,
2020-05-12 15:13:48 -07:00
} ,
2020-05-21 15:13:16 -07:00
} ) ) ;
}
2020-05-12 15:13:48 -07:00
if ( this . _options . viewport ) {
const viewport = {
viewportSize : { width : this._options.viewport.width , height : this._options.viewport.height } ,
deviceScaleFactor : this._options.deviceScaleFactor || 1 ,
} ;
promises . push ( this . _browser . _connection . send ( 'Browser.setDefaultViewport' , { browserContextId , viewport } ) ) ;
}
if ( this . _options . hasTouch )
promises . push ( this . _browser . _connection . send ( 'Browser.setTouchOverride' , { browserContextId , hasTouch : true } ) ) ;
if ( this . _options . userAgent )
promises . push ( this . _browser . _connection . send ( 'Browser.setUserAgentOverride' , { browserContextId , userAgent : this._options.userAgent } ) ) ;
if ( this . _options . bypassCSP )
promises . push ( this . _browser . _connection . send ( 'Browser.setBypassCSP' , { browserContextId , bypassCSP : true } ) ) ;
if ( this . _options . ignoreHTTPSErrors )
promises . push ( this . _browser . _connection . send ( 'Browser.setIgnoreHTTPSErrors' , { browserContextId , ignoreHTTPSErrors : true } ) ) ;
if ( this . _options . javaScriptEnabled === false )
promises . push ( this . _browser . _connection . send ( 'Browser.setJavaScriptDisabled' , { browserContextId , javaScriptDisabled : true } ) ) ;
if ( this . _options . locale )
promises . push ( this . _browser . _connection . send ( 'Browser.setLocaleOverride' , { browserContextId , locale : this._options.locale } ) ) ;
if ( this . _options . timezoneId )
promises . push ( this . _browser . _connection . send ( 'Browser.setTimezoneOverride' , { browserContextId , timezoneId : this._options.timezoneId } ) ) ;
2020-03-17 15:32:50 -07:00
if ( this . _options . permissions )
2020-05-12 15:13:48 -07:00
promises . push ( this . grantPermissions ( this . _options . permissions ) ) ;
2020-03-20 19:32:27 -07:00
if ( this . _options . extraHTTPHeaders || this . _options . locale )
2020-05-12 15:13:48 -07:00
promises . push ( this . setExtraHTTPHeaders ( this . _options . extraHTTPHeaders || { } ) ) ;
2020-03-06 13:50:42 -08:00
if ( this . _options . httpCredentials )
2020-05-12 15:13:48 -07:00
promises . push ( this . setHTTPCredentials ( this . _options . httpCredentials ) ) ;
2020-03-22 08:56:50 -07:00
if ( this . _options . geolocation )
2020-05-12 15:13:48 -07:00
promises . push ( this . setGeolocation ( this . _options . geolocation ) ) ;
2020-03-22 15:34:30 -07:00
if ( this . _options . offline )
2020-05-12 15:13:48 -07:00
promises . push ( this . setOffline ( this . _options . offline ) ) ;
2020-04-06 19:49:33 -07:00
if ( this . _options . colorScheme )
2020-05-12 15:13:48 -07:00
promises . push ( this . _browser . _connection . send ( 'Browser.setColorScheme' , { browserContextId , colorScheme : this._options.colorScheme } ) ) ;
await Promise . all ( promises ) ;
2020-02-24 08:53:30 -08:00
}
2020-03-09 12:32:42 -07:00
_ffPages ( ) : FFPage [ ] {
return Array . from ( this . _browser . _ffPages . values ( ) ) . filter ( ffPage = > ffPage . _browserContext === this ) ;
}
2020-02-24 08:53:30 -08:00
setDefaultNavigationTimeout ( timeout : number ) {
this . _timeoutSettings . setDefaultNavigationTimeout ( timeout ) ;
}
setDefaultTimeout ( timeout : number ) {
this . _timeoutSettings . setDefaultTimeout ( timeout ) ;
}
2020-03-13 11:33:33 -07:00
pages ( ) : Page [ ] {
2020-04-16 13:09:24 -07:00
return this . _ffPages ( ) . map ( ffPage = > ffPage . _initializedPage ) . filter ( pageOrNull = > ! ! pageOrNull ) as Page [ ] ;
2020-02-24 08:53:30 -08:00
}
async newPage ( ) : Promise < Page > {
assertBrowserContextIsNotOwned ( this ) ;
2020-03-09 12:32:42 -07:00
const { targetId } = await this . _browser . _connection . send ( 'Browser.newPage' , {
2020-02-24 08:53:30 -08:00
browserContextId : this._browserContextId || undefined
2020-04-01 22:10:56 -07:00
} ) . catch ( e = > {
if ( e . message . includes ( 'Failed to override timezone' ) )
throw new Error ( ` Invalid timezone ID: ${ this . _options . timezoneId } ` ) ;
throw e ;
2020-02-24 08:53:30 -08:00
} ) ;
2020-03-09 12:32:42 -07:00
const ffPage = this . _browser . _ffPages . get ( targetId ) ! ;
const pageOrError = await ffPage . pageOrError ( ) ;
if ( pageOrError instanceof Page ) {
if ( pageOrError . isClosed ( ) )
2020-03-05 15:18:27 -08:00
throw new Error ( 'Page has been closed.' ) ;
2020-03-09 12:32:42 -07:00
return pageOrError ;
2020-03-05 15:18:27 -08:00
}
2020-03-09 12:32:42 -07:00
throw pageOrError ;
2020-02-24 08:53:30 -08:00
}
2020-06-25 08:30:56 -07:00
async _doCookies ( urls : string [ ] ) : Promise < types.NetworkCookie [ ] > {
2020-02-24 08:53:30 -08:00
const { cookies } = await this . _browser . _connection . send ( 'Browser.getCookies' , { browserContextId : this._browserContextId || undefined } ) ;
return network . filterCookies ( cookies . map ( c = > {
const copy : any = { . . . c } ;
delete copy . size ;
2020-03-07 08:41:57 -08:00
delete copy . session ;
2020-06-25 08:30:56 -07:00
return copy as types . NetworkCookie ;
2020-02-24 08:53:30 -08:00
} ) , urls ) ;
}
2020-06-25 08:30:56 -07:00
async addCookies ( cookies : types.SetNetworkCookieParam [ ] ) {
2020-02-24 08:53:30 -08:00
await this . _browser . _connection . send ( 'Browser.setCookies' , { browserContextId : this._browserContextId || undefined , cookies : network.rewriteCookies ( cookies ) } ) ;
}
async clearCookies() {
await this . _browser . _connection . send ( 'Browser.clearCookies' , { browserContextId : this._browserContextId || undefined } ) ;
}
2020-03-17 15:32:50 -07:00
async _doGrantPermissions ( origin : string , permissions : string [ ] ) {
const webPermissionToProtocol = new Map < string , 'geo' | 'desktop-notification' | 'persistent-storage' | 'push' > ( [
2020-02-24 08:53:30 -08:00
[ 'geolocation' , 'geo' ] ,
2020-03-17 15:32:50 -07:00
[ 'persistent-storage' , 'persistent-storage' ] ,
[ 'push' , 'push' ] ,
[ 'notifications' , 'desktop-notification' ] ,
2020-02-24 08:53:30 -08:00
] ) ;
const filtered = permissions . map ( permission = > {
const protocolPermission = webPermissionToProtocol . get ( permission ) ;
if ( ! protocolPermission )
throw new Error ( 'Unknown permission: ' + permission ) ;
return protocolPermission ;
} ) ;
2020-03-17 15:32:50 -07:00
await this . _browser . _connection . send ( 'Browser.grantPermissions' , { origin : origin , browserContextId : this._browserContextId || undefined , permissions : filtered } ) ;
2020-02-24 08:53:30 -08:00
}
2020-03-17 15:32:50 -07:00
async _doClearPermissions() {
2020-02-24 08:53:30 -08:00
await this . _browser . _connection . send ( 'Browser.resetPermissions' , { browserContextId : this._browserContextId || undefined } ) ;
}
async setGeolocation ( geolocation : types.Geolocation | null ) : Promise < void > {
2020-03-20 19:17:46 -07:00
if ( geolocation )
geolocation = verifyGeolocation ( geolocation ) ;
this . _options . geolocation = geolocation || undefined ;
2020-03-22 08:56:50 -07:00
await this . _browser . _connection . send ( 'Browser.setGeolocationOverride' , { browserContextId : this._browserContextId || undefined , geolocation } ) ;
2020-02-24 08:53:30 -08:00
}
2020-06-25 08:30:56 -07:00
async setExtraHTTPHeaders ( headers : types.Headers ) : Promise < void > {
2020-02-26 12:42:20 -08:00
this . _options . extraHTTPHeaders = network . verifyHeaders ( headers ) ;
2020-03-20 19:32:27 -07:00
const allHeaders = { . . . this . _options . extraHTTPHeaders } ;
if ( this . _options . locale )
allHeaders [ 'Accept-Language' ] = this . _options . locale ;
await this . _browser . _connection . send ( 'Browser.setExtraHTTPHeaders' , { browserContextId : this._browserContextId || undefined , headers : headersArray ( allHeaders ) } ) ;
2020-02-26 12:42:20 -08:00
}
2020-03-04 17:58:12 -08:00
async setOffline ( offline : boolean ) : Promise < void > {
this . _options . offline = offline ;
2020-03-22 15:34:30 -07:00
await this . _browser . _connection . send ( 'Browser.setOnlineOverride' , { browserContextId : this._browserContextId || undefined , override : offline ? 'offline' : 'online' } ) ;
2020-03-04 17:58:12 -08:00
}
2020-03-06 13:50:42 -08:00
async setHTTPCredentials ( httpCredentials : types.Credentials | null ) : Promise < void > {
2020-06-29 16:37:01 -07:00
deprecate ( ` context.setHTTPCredentials ` , ` warning: method |context.setHTTPCredentials()| is deprecated. Instead of changing credentials, create another browser context with new credentials. ` ) ;
2020-03-06 13:50:42 -08:00
this . _options . httpCredentials = httpCredentials || undefined ;
await this . _browser . _connection . send ( 'Browser.setHTTPCredentials' , { browserContextId : this._browserContextId || undefined , credentials : httpCredentials } ) ;
}
2020-06-25 08:30:56 -07:00
async _doAddInitScript ( source : string ) {
2020-02-27 16:18:33 -08:00
await this . _browser . _connection . send ( 'Browser.addScriptToEvaluateOnNewDocument' , { browserContextId : this._browserContextId || undefined , script : source } ) ;
}
2020-05-18 14:28:06 -07:00
async _doExposeBinding ( binding : PageBinding ) {
await this . _browser . _connection . send ( 'Browser.addBinding' , { browserContextId : this._browserContextId || undefined , name : binding.name , script : binding.source } ) ;
2020-03-03 16:46:06 -08:00
}
2020-03-09 21:02:54 -07:00
async route ( url : types.URLMatch , handler : network.RouteHandler ) : Promise < void > {
this . _routes . push ( { url , handler } ) ;
2020-03-22 08:56:50 -07:00
if ( this . _routes . length === 1 )
await this . _browser . _connection . send ( 'Browser.setRequestInterception' , { browserContextId : this._browserContextId || undefined , enabled : true } ) ;
2020-03-09 21:02:54 -07:00
}
2020-04-15 19:55:22 -07:00
async unroute ( url : types.URLMatch , handler? : network.RouteHandler ) : Promise < void > {
this . _routes = this . _routes . filter ( route = > route . url !== url || ( handler && route . handler !== handler ) ) ;
if ( this . _routes . length === 0 )
await this . _browser . _connection . send ( 'Browser.setRequestInterception' , { browserContextId : this._browserContextId || undefined , enabled : false } ) ;
}
2020-06-29 16:26:32 -07:00
async _doClose() {
assert ( this . _browserContextId ) ;
2020-03-06 16:49:48 -08:00
await this . _browser . _connection . send ( 'Browser.removeBrowserContext' , { browserContextId : this._browserContextId } ) ;
2020-02-24 08:53:30 -08:00
this . _browser . _contexts . delete ( this . _browserContextId ) ;
}
}