From 643a166fb5475ffd32bcfb0b3ca5080635cfdc50 Mon Sep 17 00:00:00 2001 From: Pavel Date: Tue, 10 Mar 2020 10:11:41 -0700 Subject: [PATCH] feat(permissions): make origin optional --- docs/api.md | 20 +++++++++------ package.json | 3 ++- src/browserContext.ts | 21 +++++++++++++--- src/chromium/crBrowser.ts | 9 ++++--- src/firefox/ffBrowser.ts | 17 +++++++------ src/webkit/wkBrowser.ts | 7 +++--- test/geolocation.spec.js | 2 +- test/permissions.spec.js | 52 +++++++++++++++++++++++++++++++-------- 8 files changed, 92 insertions(+), 39 deletions(-) diff --git a/docs/api.md b/docs/api.md index 2e1255a95ac6c5..08b497910b863b 100644 --- a/docs/api.md +++ b/docs/api.md @@ -208,7 +208,7 @@ Indicates that the browser is connected. - `longitude` <[number]> Longitude between -180 and 180. - `accuracy` <[number]> Optional non-negative accuracy value. - `locale` Specify user locale, for example `en-GB`, `de-DE`, etc. Locale will affect `navigator.language` value, `Accept-Language` request header value as well as number and date formatting rules. - - `permissions` <[Object]> A map from origin keys to permissions values. See [browserContext.setPermissions](#browsercontextsetpermissionsorigin-permissions) for more details. + - `permissions` <[Array]<[string]>> A list of permissions to grant to all pages in this context. See [browserContext.grantPermissions](#browsercontextgrantpermissionspermissions-optoins) for more details. - `extraHTTPHeaders` <[Object]> An object containing additional HTTP headers to be sent with every request. All header values must be strings. - `offline` <[boolean]> Whether to emulate network being offline. Defaults to `false`. - `httpCredentials` <[Object]> Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). @@ -299,7 +299,7 @@ await context.close(); - [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation) - [browserContext.setHTTPCredentials(httpCredentials)](#browsercontextsethttpcredentialshttpcredentials) - [browserContext.setOffline(offline)](#browsercontextsetofflineoffline) -- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) +- [browserContext.grantPermissions(permissions[], options)](#browsercontextsetpermissionspermissions-options) - [browserContext.waitForEvent(event[, optionsOrPredicate])](#browsercontextwaitforeventevent-optionsorpredicate) @@ -381,7 +381,7 @@ Clears all permission overrides for the browser context. ```js const context = await browser.newContext(); -context.setPermissions('https://example.com', ['clipboard-read']); +context.grantPermissions(['clipboard-read']); // do stuff .. context.clearPermissions(); ``` @@ -544,9 +544,9 @@ To disable authentication, pass `null`. - `offline` <[boolean]> Whether to emulate network being offline for the browser context. - returns: <[Promise]> -#### browserContext.setPermissions(origin, permissions[]) -- `origin` <[string]> The [origin] to grant permissions to, e.g. "https://example.com". -- `permissions` <[Array]<[string]>> An array of permissions to grant. All permissions that are not listed here will be automatically denied. Permissions can be one of the following values: +#### browserContext.grantPermissions(permissions[], options) +- `permissions` <[Array]<[string]>> A permission or an array of permissions to grant. Permissions can be one of the following values: + - `'*'` - `'geolocation'` - `'midi'` - `'midi-sysex'` (system-exclusive midi) @@ -563,8 +563,12 @@ To disable authentication, pass `null`. - `'clipboard-read'` - `'clipboard-write'` - `'payment-handler'` +- `options` <[Object]> + - `origin` <[string]> The [origin] to grant permissions to, e.g. "https://example.com". - returns: <[Promise]> +Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if specified. + #### browserContext.waitForEvent(event[, optionsOrPredicate]) - `event` <[string]> Event name, same one would pass into `browserContext.on(event)`. - `optionsOrPredicate` <[Function]|[Object]> Either a predicate that receives an event or an options object. @@ -577,7 +581,7 @@ is fired. ```js const context = await browser.newContext(); -await context.setPermissions('https://html5demos.com', ['geolocation']); +await context.grantPermissions(['geolocation']); ``` ### class: Page @@ -3793,7 +3797,7 @@ const backgroundPage = await backroundPageTarget.page(); - [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation) - [browserContext.setHTTPCredentials(httpCredentials)](#browsercontextsethttpcredentialshttpcredentials) - [browserContext.setOffline(offline)](#browsercontextsetofflineoffline) -- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) +- [browserContext.grantPermissions(permissions[], options)](#browsercontextsetpermissionspermissions-origin) - [browserContext.waitForEvent(event[, optionsOrPredicate])](#browsercontextwaitforeventevent-optionsorpredicate) diff --git a/package.json b/package.json index 8291d918e22e46..36c29d32cdb009 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "main": "index.js", "playwright": { "chromium_revision": "750417", - "firefox_revision": "1042", + "firefox_revision": "1043", "webkit_revision": "1179" }, "scripts": { @@ -46,6 +46,7 @@ "extract-zip": "^1.6.6", "https-proxy-agent": "^3.0.0", "jpeg-js": "^0.3.6", + "playwright": "^0.11.1-next.1583993157193", "pngjs": "^3.4.0", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", diff --git a/src/browserContext.ts b/src/browserContext.ts index 5fd6db1367bf19..8abc4d4ab57f78 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -32,7 +32,7 @@ export type BrowserContextOptions = { locale?: string, timezoneId?: string, geolocation?: types.Geolocation, - permissions?: { [key: string]: string[] }, + permissions?: string[], extraHTTPHeaders?: network.Headers, offline?: boolean, httpCredentials?: types.Credentials, @@ -46,7 +46,7 @@ export interface BrowserContext { cookies(urls?: string | string[]): Promise; addCookies(cookies: network.SetNetworkCookieParam[]): Promise; clearCookies(): Promise; - setPermissions(origin: string, permissions: string[]): Promise; + grantPermissions(permissions: string[], options?: { origin?: string }): Promise; clearPermissions(): Promise; setGeolocation(geolocation: types.Geolocation | null): Promise; setExtraHTTPHeaders(headers: network.Headers): Promise; @@ -67,6 +67,7 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement _closed = false; private readonly _closePromise: Promise; private _closePromiseFulfill: ((error: Error) => void) | undefined; + private _permissions = new Map(); constructor(options: BrowserContextOptions) { super(); @@ -92,8 +93,7 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement abstract cookies(...urls: string[]): Promise; abstract addCookies(cookies: network.SetNetworkCookieParam[]): Promise; abstract clearCookies(): Promise; - abstract setPermissions(origin: string, permissions: string[]): Promise; - abstract clearPermissions(): Promise; + abstract _doGrantPermissions(origin: string, permissions: string[]): Promise; abstract setGeolocation(geolocation: types.Geolocation | null): Promise; abstract setHTTPCredentials(httpCredentials: types.Credentials | null): Promise; abstract setExtraHTTPHeaders(headers: network.Headers): Promise; @@ -103,6 +103,19 @@ export abstract class BrowserContextBase extends platform.EventEmitter implement abstract route(url: types.URLMatch, handler: network.RouteHandler): Promise; abstract close(): Promise; + async grantPermissions(permissions: string[], options?: { origin?: string }) { + const origin = options && options.origin || '*'; + const existing = new Set(this._permissions.get(origin) || []); + permissions.forEach(p => existing.add(p)); + const list = [...existing.values()]; + this._permissions.set(origin, list); + await this._doGrantPermissions(origin, list); + } + + async clearPermissions() { + this._permissions.clear(); + } + setDefaultNavigationTimeout(timeout: number) { this._timeoutSettings.setDefaultNavigationTimeout(timeout); } diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index d764d705083f0c..188cfbdbdf6437 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -252,8 +252,8 @@ export class CRBrowserContext extends BrowserContextBase { } async _initialize() { - const entries = Object.entries(this._options.permissions || {}); - await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); + if (this._options.permissions) + await this.grantPermissions(this._options.permissions); if (this._options.geolocation) await this.setGeolocation(this._options.geolocation); if (this._options.offline) @@ -302,7 +302,7 @@ export class CRBrowserContext extends BrowserContextBase { await this._browser._session.send('Storage.clearCookies', { browserContextId: this._browserContextId || undefined }); } - async setPermissions(origin: string, permissions: string[]): Promise { + async _doGrantPermissions(origin: string, permissions: string[]) { const webPermissionToProtocol = new Map([ ['geolocation', 'geolocation'], ['midi', 'midi'], @@ -327,10 +327,11 @@ export class CRBrowserContext extends BrowserContextBase { throw new Error('Unknown permission: ' + permission); return protocolPermission; }); - await this._browser._session.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId || undefined, permissions: filtered }); + await this._browser._session.send('Browser.grantPermissions', { origin: origin === '*' ? undefined : origin, browserContextId: this._browserContextId || undefined, permissions: filtered }); } async clearPermissions() { + super.clearPermissions(); await this._browser._session.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined }); } diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index f51563cc0b16a5..88bc37607d0adf 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -166,8 +166,8 @@ export class FFBrowserContext extends BrowserContextBase { } async _initialize() { - const entries = Object.entries(this._options.permissions || {}); - await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); + if (this._options.permissions) + await this.grantPermissions(this._options.permissions); if (this._options.geolocation) await this.setGeolocation(this._options.geolocation); if (this._options.extraHTTPHeaders) @@ -227,12 +227,12 @@ export class FFBrowserContext extends BrowserContextBase { await this._browser._connection.send('Browser.clearCookies', { browserContextId: this._browserContextId || undefined }); } - async setPermissions(origin: string, permissions: string[]): Promise { - const webPermissionToProtocol = new Map([ + async _doGrantPermissions(origin: string, permissions: string[]) { + const webPermissionToProtocol = new Map([ ['geolocation', 'geo'], - ['microphone', 'microphone'], - ['camera', 'camera'], - ['notifications', 'desktop-notifications'], + ['persistent-storage', 'persistent-storage'], + ['push', 'push'], + ['notifications', 'desktop-notification'], ]); const filtered = permissions.map(permission => { const protocolPermission = webPermissionToProtocol.get(permission); @@ -240,10 +240,11 @@ export class FFBrowserContext extends BrowserContextBase { throw new Error('Unknown permission: ' + permission); return protocolPermission; }); - await this._browser._connection.send('Browser.grantPermissions', {origin, browserContextId: this._browserContextId || undefined, permissions: filtered}); + await this._browser._connection.send('Browser.grantPermissions', { origin: origin, browserContextId: this._browserContextId || undefined, permissions: filtered}); } async clearPermissions() { + super.clearPermissions(); await this._browser._connection.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined }); } diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 59a700a4cafc87..0518fbac745021 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -190,8 +190,8 @@ export class WKBrowserContext extends BrowserContextBase { await this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId: this._browserContextId, ignore: true }); if (this._options.locale) await this._browser._browserSession.send('Playwright.setLanguages', { browserContextId: this._browserContextId, languages: [this._options.locale] }); - const entries = Object.entries(this._options.permissions || {}); - await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1]))); + if (this._options.permissions) + this.grantPermissions(this._options.permissions); if (this._options.geolocation) await this.setGeolocation(this._options.geolocation); if (this._options.offline) @@ -244,7 +244,7 @@ export class WKBrowserContext extends BrowserContextBase { await this._browser._browserSession.send('Playwright.deleteAllCookies', { browserContextId: this._browserContextId }); } - async setPermissions(origin: string, permissions: string[]): Promise { + async _doGrantPermissions(origin: string, permissions: string[]) { const webPermissionToProtocol = new Map([ ['geolocation', 'geolocation'], ]); @@ -258,6 +258,7 @@ export class WKBrowserContext extends BrowserContextBase { } async clearPermissions() { + super.clearPermissions(); await this._browser._browserSession.send('Playwright.resetPermissions', { browserContextId: this._browserContextId }); } diff --git a/test/geolocation.spec.js b/test/geolocation.spec.js index 337cc51b9419e7..216231fc434d37 100644 --- a/test/geolocation.spec.js +++ b/test/geolocation.spec.js @@ -25,7 +25,7 @@ module.exports.describe = function ({ testRunner, expect, FFOX, WEBKIT }) { describe.fail(FFOX)('Overrides.setGeolocation', function() { it('should work', async({page, server, context}) => { - await context.setPermissions(server.PREFIX, ['geolocation']); + await context.grantPermissions(['geolocation']); await page.goto(server.EMPTY_PAGE); await context.setGeolocation({longitude: 10, latitude: 10}); const geolocation = await page.evaluate(() => new Promise(resolve => navigator.geolocation.getCurrentPosition(position => { diff --git a/test/permissions.spec.js b/test/permissions.spec.js index af38b1c09f96cb..e88d39df0af8a0 100644 --- a/test/permissions.spec.js +++ b/test/permissions.spec.js @@ -21,7 +21,7 @@ const path = require('path'); /** * @type {PageTestSuite} */ -module.exports.describe = function({testRunner, expect, WEBKIT}) { +module.exports.fdescribe = function({testRunner, expect, WEBKIT, FFOX}) { const {describe, xdescribe, fdescribe} = testRunner; const {it, fit, xit, dit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; @@ -38,23 +38,55 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) { }); it('should deny permission when not listed', async({page, server, context}) => { await page.goto(server.EMPTY_PAGE); - await context.setPermissions(server.EMPTY_PAGE, []); + await context.grantPermissions([], { origin: server.EMPTY_PAGE }); expect(await getPermission(page, 'geolocation')).toBe('denied'); }); it('should fail when bad permission is given', async({page, server, context}) => { await page.goto(server.EMPTY_PAGE); let error = {}; - await context.setPermissions(server.EMPTY_PAGE, ['foo']).catch(e => error = e); + await context.grantPermissions(['foo']).catch(e => error = e, { origin: server.EMPTY_PAGE }); expect(error.message).toBe('Unknown permission: foo'); }); - it('should grant permission when listed', async({page, server, context}) => { + it('should grant geolocation permission when listed', async({page, server, context}) => { await page.goto(server.EMPTY_PAGE); - await context.setPermissions(server.EMPTY_PAGE, ['geolocation']); + await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await getPermission(page, 'geolocation')).toBe('granted'); }); + it.fail(FFOX)('should grant notifications permission when listed', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.grantPermissions(['notifications'], { origin: server.EMPTY_PAGE }); + expect(await getPermission(page, 'notifications')).toBe('granted'); + }); + it.fail(FFOX)('should accumulate when adding', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.grantPermissions(['geolocation']); + await context.grantPermissions(['notifications']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + expect(await getPermission(page, 'notifications')).toBe('granted'); + }); + it.fail(FFOX)('should clear permissions', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.grantPermissions(['geolocation']); + await context.clearPermissions(); + await context.grantPermissions(['notifications']); + expect(await getPermission(page, 'geolocation')).not.toBe('granted'); + expect(await getPermission(page, 'notifications')).toBe('granted'); + }); + it('should grant permission when listed for all domains', async({page, server, context}) => { + await page.goto(server.EMPTY_PAGE); + await context.grantPermissions(['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + }); + it('should grant permission when creating context', async({server, browser}) => { + const context = await browser.newContext({ permissions: ['geolocation'] }); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + await context.close(); + }); it('should reset permissions', async({page, server, context}) => { await page.goto(server.EMPTY_PAGE); - await context.setPermissions(server.EMPTY_PAGE, ['geolocation']); + await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await getPermission(page, 'geolocation')).toBe('granted'); await context.clearPermissions(); expect(await getPermission(page, 'geolocation')).toBe('prompt'); @@ -71,9 +103,9 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) { }); }); expect(await page.evaluate(() => window['events'])).toEqual(['prompt']); - await context.setPermissions(server.EMPTY_PAGE, []); + await context.grantPermissions([], { origin: server.EMPTY_PAGE }); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied']); - await context.setPermissions(server.EMPTY_PAGE, ['geolocation']); + await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted']); await context.clearPermissions(); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted', 'prompt']); @@ -86,8 +118,8 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) { expect(await getPermission(page, 'geolocation')).toBe('prompt'); expect(await getPermission(otherPage, 'geolocation')).toBe('prompt'); - await context.setPermissions(server.EMPTY_PAGE, []); - await otherContext.setPermissions(server.EMPTY_PAGE, ['geolocation']); + await context.grantPermissions([], { origin: server.EMPTY_PAGE }); + await otherContext.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await getPermission(page, 'geolocation')).toBe('denied'); expect(await getPermission(otherPage, 'geolocation')).toBe('granted');