From faee6c5ced694490cc2ec19abedfdc152700be51 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 29 Jul 2022 14:58:53 -0400 Subject: [PATCH 1/5] feat: add WebContents.opener --- docs/api/web-contents.md | 5 ++ .../browser/api/electron_api_web_contents.cc | 5 ++ shell/browser/api/electron_api_web_contents.h | 1 + spec-main/api-web-contents-spec.ts | 47 +++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 0c7f18d55cd0e..4cb4214a5f04b 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -2079,6 +2079,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 window that opened the window, 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/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index 3340c0e47e357..39ad39a8c3729 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(); } 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..ab8e1f5aafb88 100644 --- a/spec-main/api-web-contents-spec.ts +++ b/spec-main/api-web-contents-spec.ts @@ -1281,6 +1281,53 @@ 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.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.opener).to.be.undefined(); + }); + it('can get opener with a[target=_blank]', 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.href = 'about:blank'; + a.click(); + }())`, true); + const [childWindow] = await childPromise; + expect(childWindow.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.opener).to.be.undefined(); + }); + }); + describe('render view deleted events', () => { let server: http.Server; let serverUrl: string; From 4b48b8b48d4155073f082839e8b804055efecb2b Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 29 Jul 2022 15:19:14 -0400 Subject: [PATCH 2/5] feat: add webContents.fromFrame(frame) --- docs/api/web-contents.md | 7 +++++++ shell/browser/api/electron_api_web_contents.cc | 10 ++++++++++ spec-main/api-web-contents-spec.ts | 10 ++++++++++ 3 files changed, 27 insertions(+) diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 4cb4214a5f04b..f757be147d90a 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. diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index 39ad39a8c3729..793fcaee72c97 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -4171,6 +4171,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) { @@ -4199,6 +4208,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/spec-main/api-web-contents-spec.ts b/spec-main/api-web-contents-spec.ts index ab8e1f5aafb88..e8614c763780c 100644 --- a/spec-main/api-web-contents-spec.ts +++ b/spec-main/api-web-contents-spec.ts @@ -46,6 +46,16 @@ 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 an unknown frame', () => { + expect(webContents.fromFrame(undefined)).to.be.undefined(); + }); + }); + describe('fromDevToolsTargetId()', () => { it('returns WebContents for attached DevTools target', async () => { const w = new BrowserWindow({ show: false }); From 74c5e9cb7f02e45e48ba9cb4549dcb171b65fd7b Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 29 Jul 2022 17:06:51 -0400 Subject: [PATCH 3/5] fix: unknown type name --- shell/browser/api/electron_api_web_contents.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc index 793fcaee72c97..ec6a24301111c 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -4164,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); From 37e35891ced8556df03ee4aa069dffda7ff5480c Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 29 Jul 2022 23:10:46 -0400 Subject: [PATCH 4/5] test: fix and add more fromFrame cases --- lib/browser/api/web-contents.ts | 4 ++++ spec-main/api-web-contents-spec.ts | 29 +++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) 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/spec-main/api-web-contents-spec.ts b/spec-main/api-web-contents-spec.ts index e8614c763780c..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'); @@ -51,8 +51,20 @@ describe('webContents module', () => { const contents = (webContents as any).create() as WebContents; expect(webContents.fromFrame(contents.mainFrame)).to.equal(contents); }); - it('returns undefined for an unknown frame', () => { - expect(webContents.fromFrame(undefined)).to.be.undefined(); + 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(); }); }); @@ -1299,7 +1311,7 @@ describe('webContents module', () => { const childPromise = emittedOnce(w.webContents, 'did-create-window'); w.webContents.executeJavaScript('window.open("about:blank")', true); const [childWindow] = await childPromise; - expect(childWindow.opener).to.equal(w.webContents.mainFrame); + 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 } }); @@ -1307,20 +1319,21 @@ describe('webContents module', () => { const childPromise = emittedOnce(w.webContents, 'did-create-window'); w.webContents.executeJavaScript('window.open("about:blank", undefined, "noopener")', true); const [childWindow] = await childPromise; - expect(childWindow.opener).to.be.undefined(); + expect(childWindow.webContents.opener).to.be.null(); }); - it('can get opener with a[target=_blank]', async () => { + 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.opener).to.equal(w.webContents.mainFrame); + 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 } }); @@ -1334,7 +1347,7 @@ describe('webContents module', () => { a.click(); }())`, true); const [childWindow] = await childPromise; - expect(childWindow.opener).to.be.undefined(); + expect(childWindow.webContents.opener).to.be.null(); }); }); From 78d33243a061918bdd213f4cb64ace17db6243a9 Mon Sep 17 00:00:00 2001 From: Samuel Maddock Date: Fri, 29 Jul 2022 23:12:53 -0400 Subject: [PATCH 5/5] docs: clarified terminology --- docs/api/web-contents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index f757be147d90a..37e5137340a23 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -2088,7 +2088,7 @@ A [`WebFrameMain`](web-frame-main.md) property that represents the top frame of #### `contents.opener` _Readonly_ -A [`WebFrameMain`](web-frame-main.md) property that represents the window that opened the window, either +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