Repository: electron/electron (language: cpp)

Issue:
Allow us to discover what window opened the current window
### Preflight Checklist
<!-- Please ensure you've completed the following steps by replacing [ ] with [x]-->

* [x] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/master/CONTRIBUTING.md) for this project.
* [x] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/master/CODE_OF_CONDUCT.md) that this project adheres to.
* [x] I have searched the issue tracker for a feature request that matches the one I want to file, without success.

### Problem Description
When a window is opened, I would like to know what window opened it.

For example if the window with ID 1 triggered a `window.open` call, I'd like to know that the window with ID 2 was opened by window 1.

Currently, the `referrer` information passed to the `new-window` handler only contains a URL property (which in my case is always `''` for some reason).

### Proposed Solution
Can the `referrer` argument get an additional property that tells us the ID of the window that's making the `window.open` call?
(I'm not sure how this will fit into the new `setWindowOpenOverride` mechanism)

Alternatively, maybe the `BrowserWindow` class can have an `openerWindowId` type of property that gives us this information. That would maybe be preferable.

### Alternatives Considered
I can work around it by passing in the window ID like so:

```
// ... inside of the browser-window-created handler
window.webContents.addListener("new-window", function (): void {
	[].push.call(arguments, window.id);
	newWindowHandler.apply(null, arguments);
});
```


Discussion / hints:
If I'm understanding correctly, you can already do that by combining the browser-window-created and new-window event
Example:
```
app.on('browser-window-created', (event, win) => {
    win.webContents.on('new-window', () => {
        console.log('${win.id} opened a new window`)
    }
}
```
You can also get the id of the new window by creating it yourself.
Opening new windows by default is not a good idea.
@Plastikmensch Yes, that's solution I proposed at the end. But I would prefer a simple property that would tell me the opener window rather than doing what I did.

In my situation, I do want to allow windows to open windows by default.

Fix patch:
diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md
index 0de4fd03545c5..39ecdc98cec54 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.
@@ -2029,6 +2036,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 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/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc
index 49a690976e398..87a31771f29f1 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<v8::ObjectTemplate> WebContents::FillObjectTemplate(
       .SetProperty("devToolsWebContents", &WebContents::DevToolsWebContents)
       .SetProperty("debugger", &WebContents::Debugger)
       .SetProperty("mainFrame", &WebContents::MainFrame)
+      .SetProperty("opener", &WebContents::Opener)
       .Build();
 }
 
@@ -4157,6 +4162,7 @@ namespace {
 
 using electron::api::GetAllWebContents;
 using electron::api::WebContents;
+using electron::api::WebFrameMain;
 
 gin::Handle<WebContents> WebContentsFromID(v8::Isolate* isolate, int32_t id) {
   WebContents* contents = WebContents::FromID(id);
@@ -4164,6 +4170,15 @@ gin::Handle<WebContents> WebContentsFromID(v8::Isolate* isolate, int32_t id) {
                   : gin::Handle<WebContents>();
 }
 
+gin::Handle<WebContents> 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<WebContents>();
+}
+
 gin::Handle<WebContents> WebContentsFromDevToolsTargetID(
     v8::Isolate* isolate,
     std::string target_id) {
@@ -4192,6 +4207,7 @@ void Initialize(v8::Local<v8::Object> 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 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<v8::Value> DevToolsWebContents(v8::Isolate* isolate);
   v8::Local<v8::Value> 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..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');
@@ -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;


Test patch: