diff --git a/docs/api/web-frame-main.md b/docs/api/web-frame-main.md index 8ce004b6e9..3d3b87b354 100644 --- a/docs/api/web-frame-main.md +++ b/docs/api/web-frame-main.md @@ -169,6 +169,16 @@ convenient when `nodeIntegrationInSubFrames` is not enabled. A `string` representing the current URL of the frame. +#### `frame.origin` _Readonly_ + +A `string` representing the current origin of the frame, serialized according +to [RFC 6454](https://www.rfc-editor.org/rfc/rfc6454). This may be different +from the URL. For instance, if the frame is a child window opened to +`about:blank`, then `frame.origin` will return the parent frame's origin, while +`frame.url` will return the empty string. Pages without a scheme/host/port +triple origin will have the serialized origin of `"null"` (that is, the string +containing the letters n, u, l, l). + #### `frame.top` _Readonly_ A `WebFrameMain | null` representing top frame in the frame hierarchy to which `frame` diff --git a/shell/browser/api/electron_api_web_frame_main.cc b/shell/browser/api/electron_api_web_frame_main.cc index 178d3e30bd..0c92c632f3 100644 --- a/shell/browser/api/electron_api_web_frame_main.cc +++ b/shell/browser/api/electron_api_web_frame_main.cc @@ -296,6 +296,12 @@ GURL WebFrameMain::URL() const { return render_frame_->GetLastCommittedURL(); } +std::string WebFrameMain::Origin() const { + if (!CheckRenderFrame()) + return std::string(); + return render_frame_->GetLastCommittedOrigin().Serialize(); +} + blink::mojom::PageVisibilityState WebFrameMain::VisibilityState() const { if (!CheckRenderFrame()) return blink::mojom::PageVisibilityState::kHidden; @@ -397,6 +403,7 @@ v8::Local WebFrameMain::FillObjectTemplate( .SetProperty("processId", &WebFrameMain::ProcessID) .SetProperty("routingId", &WebFrameMain::RoutingID) .SetProperty("url", &WebFrameMain::URL) + .SetProperty("origin", &WebFrameMain::Origin) .SetProperty("visibilityState", &WebFrameMain::VisibilityState) .SetProperty("top", &WebFrameMain::Top) .SetProperty("parent", &WebFrameMain::Parent) diff --git a/shell/browser/api/electron_api_web_frame_main.h b/shell/browser/api/electron_api_web_frame_main.h index e8f1715ab8..32e5e351d5 100644 --- a/shell/browser/api/electron_api_web_frame_main.h +++ b/shell/browser/api/electron_api_web_frame_main.h @@ -109,6 +109,7 @@ class WebFrameMain : public gin::Wrappable, int ProcessID() const; int RoutingID() const; GURL URL() const; + std::string Origin() const; blink::mojom::PageVisibilityState VisibilityState() const; content::RenderFrameHost* Top() const; diff --git a/spec/api-web-frame-main-spec.ts b/spec/api-web-frame-main-spec.ts index 8c3e0e37e1..414fc2f721 100644 --- a/spec/api-web-frame-main-spec.ts +++ b/spec/api-web-frame-main-spec.ts @@ -2,11 +2,11 @@ import { expect } from 'chai'; import * as http from 'http'; import * as path from 'path'; import * as url from 'url'; -import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain } from 'electron/main'; +import { BrowserWindow, WebFrameMain, webFrameMain, ipcMain, app, WebContents } from 'electron/main'; import { closeAllWindows } from './window-helpers'; import { emittedOnce, emittedNTimes } from './events-helpers'; import { AddressInfo } from 'net'; -import { ifit, waitUntil } from './spec-helpers'; +import { defer, ifit, waitUntil } from './spec-helpers'; describe('webFrameMain module', () => { const fixtures = path.resolve(__dirname, 'fixtures'); @@ -39,7 +39,7 @@ describe('webFrameMain module', () => { let webFrame: WebFrameMain; beforeEach(async () => { - w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + w = new BrowserWindow({ show: false }); await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')); webFrame = w.webContents.mainFrame; }); @@ -88,8 +88,8 @@ describe('webFrameMain module', () => { }); describe('cross-origin', () => { - let serverA = null as unknown as Server; - let serverB = null as unknown as Server; + let serverA: Server; + let serverB: Server; before(async () => { serverA = await createServer(); @@ -112,7 +112,7 @@ describe('webFrameMain module', () => { describe('WebFrame.url', () => { it('should report correct address for each subframe', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + const w = new BrowserWindow({ show: false }); await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')); const webFrame = w.webContents.mainFrame; @@ -122,9 +122,59 @@ describe('webFrameMain module', () => { }); }); + describe('WebFrame.origin', () => { + it('should be null for a fresh WebContents', () => { + const w = new BrowserWindow({ show: false }); + expect(w.webContents.mainFrame.origin).to.equal('null'); + }); + + it('should be file:// for file frames', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadFile(path.join(fixtures, 'pages', 'blank.html')); + expect(w.webContents.mainFrame.origin).to.equal('file://'); + }); + + it('should be http:// for an http frame', async () => { + const w = new BrowserWindow({ show: false }); + const s = await createServer(); + defer(() => s.server.close()); + await w.loadURL(s.url); + expect(w.webContents.mainFrame.origin).to.equal(s.url.replace(/\/$/, '')); + }); + + it('should show parent origin when child page is about:blank', async () => { + const w = new BrowserWindow({ show: false }); + await w.loadFile(path.join(fixtures, 'pages', 'blank.html')); + const webContentsCreated: Promise<[unknown, WebContents]> = emittedOnce(app, 'web-contents-created') as any; + expect(w.webContents.mainFrame.origin).to.equal('file://'); + await w.webContents.executeJavaScript('window.open("", null, "show=false"), null'); + const [, childWebContents] = await webContentsCreated; + expect(childWebContents.mainFrame.origin).to.equal('file://'); + }); + + it('should show parent frame\'s origin when about:blank child window opened through cross-origin subframe', async () => { + const w = new BrowserWindow({ show: false }); + const serverA = await createServer(); + const serverB = await createServer(); + defer(() => { + serverA.server.close(); + serverB.server.close(); + }); + await w.loadURL(serverA.url + '?frameSrc=' + encodeURIComponent(serverB.url)); + const { mainFrame } = w.webContents; + expect(mainFrame.origin).to.equal(serverA.url.replace(/\/$/, '')); + const [childFrame] = mainFrame.frames; + expect(childFrame.origin).to.equal(serverB.url.replace(/\/$/, '')); + const webContentsCreated: Promise<[unknown, WebContents]> = emittedOnce(app, 'web-contents-created') as any; + await childFrame.executeJavaScript('window.open("", null, "show=false"), null'); + const [, childWebContents] = await webContentsCreated; + expect(childWebContents.mainFrame.origin).to.equal(childFrame.origin); + }); + }); + describe('WebFrame IDs', () => { it('has properties for various identifiers', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + const w = new BrowserWindow({ show: false }); await w.loadFile(path.join(subframesPath, 'frame.html')); const webFrame = w.webContents.mainFrame; expect(webFrame).to.have.ownProperty('url').that.is.a('string'); @@ -154,7 +204,7 @@ describe('webFrameMain module', () => { describe('WebFrame.executeJavaScript', () => { it('can inject code into any subframe', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + const w = new BrowserWindow({ show: false }); await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html')); const webFrame = w.webContents.mainFrame; @@ -165,7 +215,7 @@ describe('webFrameMain module', () => { }); it('can resolve promise', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + const w = new BrowserWindow({ show: false }); await w.loadFile(path.join(subframesPath, 'frame.html')); const webFrame = w.webContents.mainFrame; const p = () => webFrame.executeJavaScript('new Promise(resolve => setTimeout(resolve(42), 2000));'); @@ -174,7 +224,7 @@ describe('webFrameMain module', () => { }); it('can reject with error', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + const w = new BrowserWindow({ show: false }); await w.loadFile(path.join(subframesPath, 'frame.html')); const webFrame = w.webContents.mainFrame; const p = () => webFrame.executeJavaScript('new Promise((r,e) => setTimeout(e("error!"), 500));'); @@ -195,7 +245,7 @@ describe('webFrameMain module', () => { }); it('can reject when script execution fails', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + const w = new BrowserWindow({ show: false }); await w.loadFile(path.join(subframesPath, 'frame.html')); const webFrame = w.webContents.mainFrame; const p = () => webFrame.executeJavaScript('console.log(test)'); @@ -205,7 +255,7 @@ describe('webFrameMain module', () => { describe('WebFrame.reload', () => { it('reloads a frame', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + const w = new BrowserWindow({ show: false }); await w.loadFile(path.join(subframesPath, 'frame.html')); const webFrame = w.webContents.mainFrame; @@ -238,7 +288,7 @@ describe('webFrameMain module', () => { let w: BrowserWindow; beforeEach(async () => { - w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + w = new BrowserWindow({ show: false }); }); // TODO(jkleinsc) fix this flaky test on linux @@ -301,7 +351,7 @@ describe('webFrameMain module', () => { }); it('can find each frame from navigation events', async () => { - const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } }); + const w = new BrowserWindow({ show: false }); // frame-with-frame-container.html, frame-with-frame.html, frame.html const didFrameFinishLoad = emittedNTimes(w.webContents, 'did-frame-finish-load', 3);