From 4da6df44303b3f2f3c493ac5684eab301e68ca6a 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/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 0de4fd03545c5..b384e306b21be 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -2029,6 +2029,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 49a690976e398..7b6134a108d0a 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -3433,6 +3433,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) @@ -4043,6 +4047,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 9cec3ff614f85..ecae5a35ec34e 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/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts index 98251ac79035f..db378d9aa3819 100644 --- a/spec/api-web-contents-spec.ts +++ b/spec/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 fb532a08e240a06027393438ac14e072d1a8581b 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/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 b384e306b21be..96d29e562bb5b 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 7b6134a108d0a..0d881af5e0d4c 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -4169,6 +4169,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) { @@ -4197,6 +4206,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/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts index db378d9aa3819..f544d5ee88514 100644 --- a/spec/api-web-contents-spec.ts +++ b/spec/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 23de4c70d3f16ddb751384fa489f32a860311843 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 0d881af5e0d4c..87a31771f29f1 100644 --- a/shell/browser/api/electron_api_web_contents.cc +++ b/shell/browser/api/electron_api_web_contents.cc @@ -4162,6 +4162,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 05fc80a7446e5eff3bc0f1a769e36115d4df523c 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/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 9491594dcc5da..63fc39e33f5c6 100644 --- a/lib/browser/api/web-contents.ts +++ b/lib/browser/api/web-contents.ts @@ -830,6 +830,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/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts index f544d5ee88514..32edb36ed655e 100644 --- a/spec/api-web-contents-spec.ts +++ b/spec/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, '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 0c7c22acf8e211512834993b9b606cf1a9772180 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 96d29e562bb5b..39ecdc98cec54 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -2038,7 +2038,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