diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 0c7f18d55cd0e..37e5137340a23 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -45,6 +45,13 @@ returns `null`. Returns `WebContents` | undefined - A WebContents instance with the given ID, or `undefined` if there is no WebContents associated with the given ID. +### `webContents.fromFrame(frame)` + +* `frame` WebFrameMain + +Returns `WebContents` | undefined - A WebContents instance with the given WebFrameMain, or +`undefined` if there is no WebContents associated with the given WebFrameMain. + ### `webContents.fromDevToolsTargetId(targetId)` * `targetId` string - The Chrome DevTools Protocol [TargetID](https://chromedevtools.github.io/devtools-protocol/tot/Target/#type-TargetID) associated with the WebContents instance. @@ -2079,6 +2086,11 @@ when the page becomes backgrounded. This also affects the Page Visibility API. A [`WebFrameMain`](web-frame-main.md) property that represents the top frame of the page's frame hierarchy. +#### `contents.opener` _Readonly_ + +A [`WebFrameMain`](web-frame-main.md) property that represents the frame that opened this WebContents, either +with open(), or by navigating a link with a target attribute. + [keyboardevent]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter [SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm diff --git a/lib/browser/api/web-contents.ts b/lib/browser/api/web-contents.ts index a0369fc734a14..5b9131fcec003 100644 --- a/lib/browser/api/web-contents.ts +++ b/lib/browser/api/web-contents.ts @@ -838,6 +838,10 @@ export function fromId (id: string) { return binding.fromId(id); } +export function fromFrame (frame: Electron.WebFrameMain) { + return binding.fromFrame(frame); +} + export function fromDevToolsTargetId (targetId: string) { return binding.fromDevToolsTargetId(targetId); } diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index 3340c0e47e357..ec6a24301111c 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -3435,6 +3435,10 @@ content::RenderFrameHost* WebContents::MainFrame() { return web_contents()->GetPrimaryMainFrame(); } +content::RenderFrameHost* WebContents::Opener() { + return web_contents()->GetOpener(); +} + void WebContents::NotifyUserActivation() { content::RenderFrameHost* frame = web_contents()->GetPrimaryMainFrame(); if (frame) @@ -4045,6 +4049,7 @@ v8::Local WebContents::FillObjectTemplate( .SetProperty("devToolsWebContents", &WebContents::DevToolsWebContents) .SetProperty("debugger", &WebContents::Debugger) .SetProperty("mainFrame", &WebContents::MainFrame) + .SetProperty("opener", &WebContents::Opener) .Build(); } @@ -4159,6 +4164,7 @@ namespace { using electron::api::GetAllWebContents; using electron::api::WebContents; +using electron::api::WebFrameMain; gin::Handle WebContentsFromID(v8::Isolate* isolate, int32_t id) { WebContents* contents = WebContents::FromID(id); @@ -4166,6 +4172,15 @@ gin::Handle WebContentsFromID(v8::Isolate* isolate, int32_t id) { : gin::Handle(); } +gin::Handle WebContentsFromFrame(v8::Isolate* isolate, + WebFrameMain* web_frame) { + content::RenderFrameHost* rfh = web_frame->render_frame_host(); + content::WebContents* source = content::WebContents::FromRenderFrameHost(rfh); + WebContents* contents = WebContents::From(source); + return contents ? gin::CreateHandle(isolate, contents) + : gin::Handle(); +} + gin::Handle WebContentsFromDevToolsTargetID( v8::Isolate* isolate, std::string target_id) { @@ -4194,6 +4209,7 @@ void Initialize(v8::Local exports, gin_helper::Dictionary dict(isolate, exports); dict.Set("WebContents", WebContents::GetConstructor(context)); dict.SetMethod("fromId", &WebContentsFromID); + dict.SetMethod("fromFrame", &WebContentsFromFrame); dict.SetMethod("fromDevToolsTargetId", &WebContentsFromDevToolsTargetID); dict.SetMethod("getAllWebContents", &GetAllWebContentsAsV8); } diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h index 111984a20c37d..1d7bc0ccb7e34 100644 --- a/shell/browser/api/electron_api_web_contents.h +++ b/shell/browser/api/electron_api_web_contents.h @@ -332,6 +332,7 @@ class WebContents : public ExclusiveAccessContext, v8::Local DevToolsWebContents(v8::Isolate* isolate); v8::Local Debugger(v8::Isolate* isolate); content::RenderFrameHost* MainFrame(); + content::RenderFrameHost* Opener(); WebContentsZoomController* GetZoomController() { return zoom_controller_; } diff --git a/spec-main/api-web-contents-spec.ts b/spec-main/api-web-contents-spec.ts index 6b326e8008e64..f19fc67d49c70 100644 --- a/spec-main/api-web-contents-spec.ts +++ b/spec-main/api-web-contents-spec.ts @@ -6,7 +6,7 @@ import * as http from 'http'; import { BrowserWindow, ipcMain, webContents, session, WebContents, app, BrowserView } from 'electron/main'; import { emittedOnce } from './events-helpers'; import { closeAllWindows } from './window-helpers'; -import { ifdescribe, delay, defer } from './spec-helpers'; +import { ifdescribe, delay, defer, waitUntil } from './spec-helpers'; const pdfjs = require('pdfjs-dist'); const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures'); @@ -46,6 +46,28 @@ describe('webContents module', () => { }); }); + describe('fromFrame()', () => { + it('returns WebContents for mainFrame', () => { + const contents = (webContents as any).create() as WebContents; + expect(webContents.fromFrame(contents.mainFrame)).to.equal(contents); + }); + it('returns undefined for disposed frame', async () => { + const contents = (webContents as any).create() as WebContents; + const { mainFrame } = contents; + contents.destroy(); + await waitUntil(() => typeof webContents.fromFrame(mainFrame) === 'undefined'); + }); + it('throws when passing invalid argument', async () => { + let errored = false; + try { + webContents.fromFrame({} as any); + } catch { + errored = true; + } + expect(errored).to.be.true(); + }); + }); + describe('fromDevToolsTargetId()', () => { it('returns WebContents for attached DevTools target', async () => { const w = new BrowserWindow({ show: false }); @@ -1281,6 +1303,54 @@ describe('webContents module', () => { }); }); + describe('opener api', () => { + afterEach(closeAllWindows); + it('can get opener with window.open()', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadURL('about:blank'); + const childPromise = emittedOnce(w.webContents, 'did-create-window'); + w.webContents.executeJavaScript('window.open("about:blank")', true); + const [childWindow] = await childPromise; + expect(childWindow.webContents.opener).to.equal(w.webContents.mainFrame); + }); + it('has no opener when using "noopener"', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadURL('about:blank'); + const childPromise = emittedOnce(w.webContents, 'did-create-window'); + w.webContents.executeJavaScript('window.open("about:blank", undefined, "noopener")', true); + const [childWindow] = await childPromise; + expect(childWindow.webContents.opener).to.be.null(); + }); + it('can get opener with a[target=_blank][rel=opener]', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadURL('about:blank'); + const childPromise = emittedOnce(w.webContents, 'did-create-window'); + w.webContents.executeJavaScript(`(function() { + const a = document.createElement('a'); + a.target = '_blank'; + a.rel = 'opener'; + a.href = 'about:blank'; + a.click(); + }())`, true); + const [childWindow] = await childPromise; + expect(childWindow.webContents.opener).to.equal(w.webContents.mainFrame); + }); + it('has no opener with a[target=_blank][rel=noopener]', async () => { + const w = new BrowserWindow({ show: false, webPreferences: { sandbox: true } }); + await w.loadURL('about:blank'); + const childPromise = emittedOnce(w.webContents, 'did-create-window'); + w.webContents.executeJavaScript(`(function() { + const a = document.createElement('a'); + a.target = '_blank'; + a.rel = 'noopener'; + a.href = 'about:blank'; + a.click(); + }())`, true); + const [childWindow] = await childPromise; + expect(childWindow.webContents.opener).to.be.null(); + }); + }); + describe('render view deleted events', () => { let server: http.Server; let serverUrl: string;