Browse Source

feat: add webFrameMain API to the main process (#25464)

Samuel Maddock 4 years ago
parent
commit
704d69a8f9

+ 1 - 0
docs/README.md

@@ -145,6 +145,7 @@ These individual tutorials expand on topics discussed in the guide above.
 * [TouchBar](api/touch-bar.md)
 * [Tray](api/tray.md)
 * [webContents](api/web-contents.md)
+* [webFrameMain](api/web-frame-main.md)
 
 ### Modules for the Renderer Process (Web Page):
 

+ 4 - 0
docs/api/web-contents.md

@@ -1937,3 +1937,7 @@ A [`Debugger`](debugger.md) instance for this webContents.
 
 A `Boolean` property that determines whether or not this WebContents will throttle animations and timers
 when the page becomes backgrounded. This also affects the Page Visibility API.
+
+#### `contents.mainFrame` _Readonly_
+
+A [`WebFrameMain`](web-frame-main.md) property that represents the top frame of the page's frame hierarchy.

+ 133 - 0
docs/api/web-frame-main.md

@@ -0,0 +1,133 @@
+# webFrameMain
+
+> Control web pages and iframes.
+
+Process: [Main](../glossary.md#main-process)
+
+The `webFrameMain` module can be used to lookup frames across existing
+[`WebContents`](web-contents.md) instances. Navigation events are the common
+use case.
+
+```javascript
+const { BrowserWindow, webFrameMain } = require('electron')
+
+const win = new BrowserWindow({ width: 800, height: 1500 })
+win.loadURL('https://twitter.com')
+
+win.webContents.on(
+  'did-frame-navigate',
+  (event, url, isMainFrame, frameProcessId, frameRoutingId) => {
+    const frame = webFrameMain.fromId(frameProcessId, frameRoutingId)
+    if (frame) {
+      const code = 'document.body.innerHTML = document.body.innerHTML.replaceAll("heck", "h*ck")'
+      frame.executeJavaScript(code)
+    }
+  }
+)
+```
+
+You can also access frames of existing pages by using the `webFrame` property
+of [`WebContents`](web-contents.md).
+
+```javascript
+const { BrowserWindow } = require('electron')
+
+async function main () {
+  const win = new BrowserWindow({ width: 800, height: 600 })
+  await win.loadURL('https://reddit.com')
+
+  const youtubeEmbeds = win.webContents.mainFrame.frames.filter((frame) => {
+    try {
+      const url = new URL(frame.url)
+      return url.host === 'www.youtube.com'
+    } catch {
+      return false
+    }
+  })
+
+  console.log(youtubeEmbeds)
+}
+
+main()
+```
+
+## Methods
+
+These methods can be accessed from the `webFrameMain` module:
+
+### `webFrameMain.fromId(processId, routingId)`
+
+* `processId` Integer - An `Integer` representing the id of the process which owns the frame.
+* `routingId` Integer - An `Integer` representing the unique frame id in the
+  current renderer process. Routing IDs can be retrieved from `WebFrameMain`
+  instances (`frame.routingId`) and are also passed by frame
+  specific `WebContents` navigation events (e.g. `did-frame-navigate`).
+
+Returns `WebFrameMain` - A frame with the given process and routing IDs.
+
+## Class: WebFrameMain
+
+Process: [Main](../glossary.md#main-process)
+
+### Instance Methods
+
+#### `frame.executeJavaScript(code[, userGesture])`
+
+* `code` String
+* `userGesture` Boolean (optional) - Default is `false`.
+
+Returns `Promise<unknown>` - A promise that resolves with the result of the executed
+code or is rejected if execution throws or results in a rejected promise.
+
+Evaluates `code` in page.
+
+In the browser window some HTML APIs like `requestFullScreen` can only be
+invoked by a gesture from the user. Setting `userGesture` to `true` will remove
+this limitation.
+
+#### `frame.reload()`
+
+Returns `boolean` - Whether the reload was initiated successfully. Only results in `false` when the frame has no history.
+
+### Instance Properties
+
+#### `frame.url` _Readonly_
+
+A `string` representing the current URL of the frame.
+
+#### `frame.top` _Readonly_
+
+A `WebFrameMain | null` representing top frame in the frame hierarchy to which `frame`
+belongs.
+
+#### `frame.parent` _Readonly_
+
+A `WebFrameMain | null` representing parent frame of `frame`, the property would be
+`null` if `frame` is the top frame in the frame hierarchy.
+
+#### `frame.frames` _Readonly_
+
+A `WebFrameMain[]` collection containing the direct descendents of `frame`.
+
+#### `frame.framesInSubtree` _Readonly_
+
+A `WebFrameMain[]` collection containing every frame in the subtree of `frame`,
+including itself. This can be useful when traversing through all frames.
+
+#### `frame.frameTreeNodeId` _Readonly_
+
+An `Integer` representing the id of the frame's internal FrameTreeNode
+instance. This id is browser-global and uniquely identifies a frame that hosts
+content. The identifier is fixed at the creation of the frame and stays
+constant for the lifetime of the frame. When the frame is removed, the id is
+not used again.
+
+#### `frame.processId` _Readonly_
+
+An `Integer` representing the id of the process which owns this frame.
+
+#### `frame.routingId` _Readonly_
+
+An `Integer` representing the unique frame id in the current renderer process.
+Distinct `WebFrameMain` instances that refer to the same underlying frame will
+have the same `routingId`.

+ 2 - 0
filenames.auto.gni

@@ -66,6 +66,7 @@ auto_filenames = {
     "docs/api/touch-bar.md",
     "docs/api/tray.md",
     "docs/api/web-contents.md",
+    "docs/api/web-frame-main.md",
     "docs/api/web-frame.md",
     "docs/api/web-request.md",
     "docs/api/webview-tag.md",
@@ -224,6 +225,7 @@ auto_filenames = {
     "lib/browser/api/views/image-view.ts",
     "lib/browser/api/web-contents-view.ts",
     "lib/browser/api/web-contents.ts",
+    "lib/browser/api/web-frame-main.ts",
     "lib/browser/chrome-extension-shim.ts",
     "lib/browser/default-menu.ts",
     "lib/browser/desktop-capturer.ts",

+ 4 - 0
filenames.gni

@@ -115,6 +115,8 @@ filenames = {
     "shell/browser/api/electron_api_web_contents_mac.mm",
     "shell/browser/api/electron_api_web_contents_view.cc",
     "shell/browser/api/electron_api_web_contents_view.h",
+    "shell/browser/api/electron_api_web_frame_main.cc",
+    "shell/browser/api/electron_api_web_frame_main.h",
     "shell/browser/api/electron_api_web_request.cc",
     "shell/browser/api/electron_api_web_request.h",
     "shell/browser/api/electron_api_web_view_manager.cc",
@@ -495,6 +497,8 @@ filenames = {
     "shell/common/gin_converters/file_dialog_converter.cc",
     "shell/common/gin_converters/file_dialog_converter.h",
     "shell/common/gin_converters/file_path_converter.h",
+    "shell/common/gin_converters/frame_converter.cc",
+    "shell/common/gin_converters/frame_converter.h",
     "shell/common/gin_converters/gfx_converter.cc",
     "shell/common/gin_converters/gfx_converter.h",
     "shell/common/gin_converters/guid_converter.h",

+ 2 - 1
lib/browser/api/module-list.ts

@@ -31,7 +31,8 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
   { name: 'Tray', loader: () => require('./tray') },
   { name: 'View', loader: () => require('./view') },
   { name: 'webContents', loader: () => require('./web-contents') },
-  { name: 'WebContentsView', loader: () => require('./web-contents-view') }
+  { name: 'WebContentsView', loader: () => require('./web-contents-view') },
+  { name: 'webFrameMain', loader: () => require('./web-frame-main') }
 ];
 
 if (BUILDFLAG(ENABLE_DESKTOP_CAPTURER)) {

+ 2 - 1
lib/browser/api/module-names.ts

@@ -34,7 +34,8 @@ export const browserModuleNames = [
   'Tray',
   'View',
   'webContents',
-  'WebContentsView'
+  'WebContentsView',
+  'webFrameMain'
 ];
 
 if (BUILDFLAG(ENABLE_DESKTOP_CAPTURER)) {

+ 5 - 0
lib/browser/api/web-frame-main.ts

@@ -0,0 +1,5 @@
+const { fromId } = process._linkedBinding('electron_browser_web_frame_main');
+
+export default {
+  fromId
+};

+ 2 - 2
package.json

@@ -5,7 +5,7 @@
   "description": "Build cross platform desktop apps with JavaScript, HTML, and CSS",
   "devDependencies": {
     "@electron/docs-parser": "^0.9.1",
-    "@electron/typescript-definitions": "^8.7.9",
+    "@electron/typescript-definitions": "^8.8.0",
     "@octokit/rest": "^18.0.3",
     "@primer/octicons": "^10.0.0",
     "@types/basic-auth": "^1.1.3",
@@ -141,4 +141,4 @@
     "@types/temp": "^0.8.34",
     "aws-sdk": "^2.727.1"
   }
-}
+}

+ 11 - 0
shell/browser/api/electron_api_web_contents.cc

@@ -62,6 +62,7 @@
 #include "shell/browser/api/electron_api_browser_window.h"
 #include "shell/browser/api/electron_api_debugger.h"
 #include "shell/browser/api/electron_api_session.h"
+#include "shell/browser/api/electron_api_web_frame_main.h"
 #include "shell/browser/api/message_port.h"
 #include "shell/browser/browser.h"
 #include "shell/browser/child_web_contents_tracker.h"
@@ -87,6 +88,7 @@
 #include "shell/common/gin_converters/callback_converter.h"
 #include "shell/common/gin_converters/content_converter.h"
 #include "shell/common/gin_converters/file_path_converter.h"
+#include "shell/common/gin_converters/frame_converter.h"
 #include "shell/common/gin_converters/gfx_converter.h"
 #include "shell/common/gin_converters/gurl_converter.h"
 #include "shell/common/gin_converters/image_converter.h"
@@ -1324,6 +1326,10 @@ void WebContents::UpdateDraggableRegions(
 
 void WebContents::RenderFrameDeleted(
     content::RenderFrameHost* render_frame_host) {
+  // A WebFrameMain can outlive its RenderFrameHost so we need to mark it as
+  // disposed to prevent access to it.
+  WebFrameMain::RenderFrameDeleted(render_frame_host);
+
   // A RenderFrameHost can be destroyed before the related Mojo binding is
   // closed, which can result in Mojo calls being sent for RenderFrameHosts
   // that no longer exist. To prevent this from happening, when a
@@ -2835,6 +2841,10 @@ bool WebContents::WasInitiallyShown() {
   return initially_shown_;
 }
 
+content::RenderFrameHost* WebContents::MainFrame() {
+  return web_contents()->GetMainFrame();
+}
+
 void WebContents::GrantOriginAccess(const GURL& url) {
   content::ChildProcessSecurityPolicy::GetInstance()->GrantCommitOrigin(
       web_contents()->GetMainFrame()->GetProcess()->GetID(),
@@ -3031,6 +3041,7 @@ v8::Local<v8::ObjectTemplate> WebContents::FillObjectTemplate(
       .SetProperty("devToolsWebContents", &WebContents::DevToolsWebContents)
       .SetProperty("debugger", &WebContents::Debugger)
       .SetProperty("_initiallyShown", &WebContents::WasInitiallyShown)
+      .SetProperty("mainFrame", &WebContents::MainFrame)
       .Build();
 }
 

+ 1 - 0
shell/browser/api/electron_api_web_contents.h

@@ -397,6 +397,7 @@ class WebContents : public gin::Wrappable<WebContents>,
   v8::Local<v8::Value> DevToolsWebContents(v8::Isolate* isolate);
   v8::Local<v8::Value> Debugger(v8::Isolate* isolate);
   bool WasInitiallyShown();
+  content::RenderFrameHost* MainFrame();
 
   WebContentsZoomController* GetZoomController() { return zoom_controller_; }
 

+ 258 - 0
shell/browser/api/electron_api_web_frame_main.cc

@@ -0,0 +1,258 @@
+// Copyright (c) 2020 Samuel Maddock <[email protected]>.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/browser/api/electron_api_web_frame_main.h"
+
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "base/lazy_instance.h"
+#include "base/logging.h"
+#include "content/browser/renderer_host/frame_tree_node.h"  // nogncheck
+#include "content/public/browser/render_frame_host.h"
+#include "gin/object_template_builder.h"
+#include "shell/browser/browser.h"
+#include "shell/browser/javascript_environment.h"
+#include "shell/common/gin_converters/frame_converter.h"
+#include "shell/common/gin_converters/gurl_converter.h"
+#include "shell/common/gin_converters/value_converter.h"
+#include "shell/common/gin_helper/dictionary.h"
+#include "shell/common/gin_helper/error_thrower.h"
+#include "shell/common/gin_helper/object_template_builder.h"
+#include "shell/common/gin_helper/promise.h"
+#include "shell/common/node_includes.h"
+
+namespace electron {
+
+namespace api {
+
+typedef std::unordered_map<content::RenderFrameHost*, WebFrameMain*>
+    RenderFrameMap;
+base::LazyInstance<RenderFrameMap>::DestructorAtExit g_render_frame_map =
+    LAZY_INSTANCE_INITIALIZER;
+
+WebFrameMain* FromRenderFrameHost(content::RenderFrameHost* rfh) {
+  auto frame_map = g_render_frame_map.Get();
+  auto iter = frame_map.find(rfh);
+  auto* web_frame = iter == frame_map.end() ? nullptr : iter->second;
+  return web_frame;
+}
+
+gin::WrapperInfo WebFrameMain::kWrapperInfo = {gin::kEmbedderNativeGin};
+
+WebFrameMain::WebFrameMain(content::RenderFrameHost* rfh) : render_frame_(rfh) {
+  g_render_frame_map.Get().emplace(rfh, this);
+}
+
+WebFrameMain::~WebFrameMain() {
+  MarkRenderFrameDisposed();
+}
+
+void WebFrameMain::MarkRenderFrameDisposed() {
+  g_render_frame_map.Get().erase(render_frame_);
+  render_frame_disposed_ = true;
+}
+
+bool WebFrameMain::CheckRenderFrame() const {
+  if (render_frame_disposed_) {
+    v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+    v8::Locker locker(isolate);
+    v8::HandleScope scope(isolate);
+    gin_helper::ErrorThrower(isolate).ThrowError(
+        "Render frame was disposed before WebFrameMain could be accessed");
+    return false;
+  }
+  return true;
+}
+
+v8::Local<v8::Promise> WebFrameMain::ExecuteJavaScript(
+    gin::Arguments* args,
+    const base::string16& code) {
+  gin_helper::Promise<base::Value> promise(args->isolate());
+  v8::Local<v8::Promise> handle = promise.GetHandle();
+
+  // Optional userGesture parameter
+  bool user_gesture;
+  if (!args->PeekNext().IsEmpty()) {
+    if (args->PeekNext()->IsBoolean()) {
+      args->GetNext(&user_gesture);
+    } else {
+      args->ThrowTypeError("userGesture must be a boolean");
+      return handle;
+    }
+  } else {
+    user_gesture = false;
+  }
+
+  if (render_frame_disposed_) {
+    promise.RejectWithErrorMessage(
+        "Render frame was disposed before WebFrameMain could be accessed");
+    return handle;
+  }
+
+  if (user_gesture) {
+    auto* ftn = content::FrameTreeNode::From(render_frame_);
+    ftn->UpdateUserActivationState(
+        blink::mojom::UserActivationUpdateType::kNotifyActivation,
+        blink::mojom::UserActivationNotificationType::kTest);
+  }
+
+  render_frame_->ExecuteJavaScriptForTests(
+      code, base::BindOnce([](gin_helper::Promise<base::Value> promise,
+                              base::Value value) { promise.Resolve(value); },
+                           std::move(promise)));
+
+  return handle;
+}
+
+bool WebFrameMain::Reload(v8::Isolate* isolate) {
+  if (!CheckRenderFrame())
+    return false;
+  return render_frame_->Reload();
+}
+
+int WebFrameMain::FrameTreeNodeID(v8::Isolate* isolate) const {
+  if (!CheckRenderFrame())
+    return -1;
+  return render_frame_->GetFrameTreeNodeId();
+}
+
+int WebFrameMain::ProcessID(v8::Isolate* isolate) const {
+  if (!CheckRenderFrame())
+    return -1;
+  return render_frame_->GetProcess()->GetID();
+}
+
+int WebFrameMain::RoutingID(v8::Isolate* isolate) const {
+  if (!CheckRenderFrame())
+    return -1;
+  return render_frame_->GetRoutingID();
+}
+
+GURL WebFrameMain::URL(v8::Isolate* isolate) const {
+  if (!CheckRenderFrame())
+    return GURL::EmptyGURL();
+  return render_frame_->GetLastCommittedURL();
+}
+
+content::RenderFrameHost* WebFrameMain::Top(v8::Isolate* isolate) const {
+  if (!CheckRenderFrame())
+    return nullptr;
+  return render_frame_->GetMainFrame();
+}
+
+content::RenderFrameHost* WebFrameMain::Parent(v8::Isolate* isolate) const {
+  if (!CheckRenderFrame())
+    return nullptr;
+  return render_frame_->GetParent();
+}
+
+std::vector<content::RenderFrameHost*> WebFrameMain::Frames(
+    v8::Isolate* isolate) const {
+  std::vector<content::RenderFrameHost*> frame_hosts;
+  if (!CheckRenderFrame())
+    return frame_hosts;
+
+  for (auto* rfh : render_frame_->GetFramesInSubtree()) {
+    if (rfh->GetParent() == render_frame_)
+      frame_hosts.push_back(rfh);
+  }
+
+  return frame_hosts;
+}
+
+std::vector<content::RenderFrameHost*> WebFrameMain::FramesInSubtree(
+    v8::Isolate* isolate) const {
+  std::vector<content::RenderFrameHost*> frame_hosts;
+  if (!CheckRenderFrame())
+    return frame_hosts;
+
+  for (auto* rfh : render_frame_->GetFramesInSubtree()) {
+    frame_hosts.push_back(rfh);
+  }
+
+  return frame_hosts;
+}
+
+// static
+gin::Handle<WebFrameMain> WebFrameMain::From(v8::Isolate* isolate,
+                                             content::RenderFrameHost* rfh) {
+  if (rfh == nullptr)
+    return gin::Handle<WebFrameMain>();
+  auto* web_frame = FromRenderFrameHost(rfh);
+  auto handle = gin::CreateHandle(
+      isolate, web_frame == nullptr ? new WebFrameMain(rfh) : web_frame);
+  return handle;
+}
+
+// static
+gin::Handle<WebFrameMain> WebFrameMain::FromID(v8::Isolate* isolate,
+                                               int render_process_id,
+                                               int render_frame_id) {
+  auto* rfh =
+      content::RenderFrameHost::FromID(render_process_id, render_frame_id);
+  return From(isolate, rfh);
+}
+
+// static
+void WebFrameMain::RenderFrameDeleted(content::RenderFrameHost* rfh) {
+  auto* web_frame = FromRenderFrameHost(rfh);
+  if (web_frame)
+    web_frame->MarkRenderFrameDisposed();
+}
+
+gin::ObjectTemplateBuilder WebFrameMain::GetObjectTemplateBuilder(
+    v8::Isolate* isolate) {
+  return gin::Wrappable<WebFrameMain>::GetObjectTemplateBuilder(isolate)
+      .SetMethod("executeJavaScript", &WebFrameMain::ExecuteJavaScript)
+      .SetMethod("reload", &WebFrameMain::Reload)
+      .SetProperty("frameTreeNodeId", &WebFrameMain::FrameTreeNodeID)
+      .SetProperty("processId", &WebFrameMain::ProcessID)
+      .SetProperty("routingId", &WebFrameMain::RoutingID)
+      .SetProperty("url", &WebFrameMain::URL)
+      .SetProperty("top", &WebFrameMain::Top)
+      .SetProperty("parent", &WebFrameMain::Parent)
+      .SetProperty("frames", &WebFrameMain::Frames)
+      .SetProperty("framesInSubtree", &WebFrameMain::FramesInSubtree);
+}
+
+const char* WebFrameMain::GetTypeName() {
+  return "WebFrameMain";
+}
+
+}  // namespace api
+
+}  // namespace electron
+
+namespace {
+
+using electron::api::WebFrameMain;
+
+v8::Local<v8::Value> FromID(gin_helper::ErrorThrower thrower,
+                            int render_process_id,
+                            int render_frame_id) {
+  if (!electron::Browser::Get()->is_ready()) {
+    thrower.ThrowError("WebFrameMain is available only after app ready");
+    return v8::Null(thrower.isolate());
+  }
+
+  return WebFrameMain::FromID(thrower.isolate(), render_process_id,
+                              render_frame_id)
+      .ToV8();
+}
+
+void Initialize(v8::Local<v8::Object> exports,
+                v8::Local<v8::Value> unused,
+                v8::Local<v8::Context> context,
+                void* priv) {
+  v8::Isolate* isolate = context->GetIsolate();
+  gin_helper::Dictionary dict(isolate, exports);
+  dict.SetMethod("fromId", &FromID);
+}
+
+}  // namespace
+
+NODE_LINKED_MODULE_CONTEXT_AWARE(electron_browser_web_frame_main, Initialize)

+ 94 - 0
shell/browser/api/electron_api_web_frame_main.h

@@ -0,0 +1,94 @@
+// Copyright (c) 2020 Samuel Maddock <[email protected]>.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef SHELL_BROWSER_API_ELECTRON_API_WEB_FRAME_MAIN_H_
+#define SHELL_BROWSER_API_ELECTRON_API_WEB_FRAME_MAIN_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "gin/handle.h"
+#include "gin/wrappable.h"
+
+class GURL;
+
+namespace content {
+class RenderFrameHost;
+}
+
+namespace gin {
+class Arguments;
+}
+
+namespace gin_helper {
+class Dictionary;
+}
+
+namespace electron {
+
+namespace api {
+
+// Bindings for accessing frames from the main process.
+class WebFrameMain : public gin::Wrappable<WebFrameMain> {
+ public:
+  static gin::Handle<WebFrameMain> FromID(v8::Isolate* isolate,
+                                          int render_process_id,
+                                          int render_frame_id);
+  static gin::Handle<WebFrameMain> From(
+      v8::Isolate* isolate,
+      content::RenderFrameHost* render_frame_host);
+
+  // Called to mark any RenderFrameHost as disposed by any WebFrameMain that
+  // may be holding a weak reference.
+  static void RenderFrameDeleted(content::RenderFrameHost* rfh);
+
+  // Mark RenderFrameHost as disposed and to no longer access it. This can
+  // occur upon frame navigation.
+  void MarkRenderFrameDisposed();
+
+  // gin::Wrappable
+  static gin::WrapperInfo kWrapperInfo;
+  gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
+      v8::Isolate* isolate) override;
+  const char* GetTypeName() override;
+
+ protected:
+  explicit WebFrameMain(content::RenderFrameHost* render_frame);
+  ~WebFrameMain() override;
+
+ private:
+  // WebFrameMain can outlive its RenderFrameHost pointer so we need to check
+  // whether its been disposed of prior to accessing it.
+  bool CheckRenderFrame() const;
+
+  v8::Local<v8::Promise> ExecuteJavaScript(gin::Arguments* args,
+                                           const base::string16& code);
+  bool Reload(v8::Isolate* isolate);
+
+  int FrameTreeNodeID(v8::Isolate* isolate) const;
+  int ProcessID(v8::Isolate* isolate) const;
+  int RoutingID(v8::Isolate* isolate) const;
+  GURL URL(v8::Isolate* isolate) const;
+
+  content::RenderFrameHost* Top(v8::Isolate* isolate) const;
+  content::RenderFrameHost* Parent(v8::Isolate* isolate) const;
+  std::vector<content::RenderFrameHost*> Frames(v8::Isolate* isolate) const;
+  std::vector<content::RenderFrameHost*> FramesInSubtree(
+      v8::Isolate* isolate) const;
+
+  content::RenderFrameHost* render_frame_ = nullptr;
+
+  // Whether the RenderFrameHost has been removed and that it should no longer
+  // be accessed.
+  bool render_frame_disposed_ = false;
+
+  DISALLOW_COPY_AND_ASSIGN(WebFrameMain);
+};
+
+}  // namespace api
+
+}  // namespace electron
+
+#endif  // SHELL_BROWSER_API_ELECTRON_API_WEB_FRAME_MAIN_H_

+ 28 - 0
shell/common/gin_converters/frame_converter.cc

@@ -0,0 +1,28 @@
+// Copyright (c) 2020 Samuel Maddock <[email protected]>.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/common/gin_converters/frame_converter.h"
+
+#include <string>
+#include <vector>
+
+#include "content/public/browser/render_frame_host.h"
+#include "shell/browser/api/electron_api_web_frame_main.h"
+#include "shell/common/gin_converters/blink_converter.h"
+#include "shell/common/gin_converters/callback_converter.h"
+#include "shell/common/gin_converters/gurl_converter.h"
+#include "shell/common/gin_helper/dictionary.h"
+
+namespace gin {
+
+// static
+v8::Local<v8::Value> Converter<content::RenderFrameHost*>::ToV8(
+    v8::Isolate* isolate,
+    content::RenderFrameHost* val) {
+  if (!val)
+    return v8::Null(isolate);
+  return electron::api::WebFrameMain::From(isolate, val).ToV8();
+}
+
+}  // namespace gin

+ 26 - 0
shell/common/gin_converters/frame_converter.h

@@ -0,0 +1,26 @@
+// Copyright (c) 2020 Samuel Maddock <[email protected]>.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef SHELL_COMMON_GIN_CONVERTERS_FRAME_CONVERTER_H_
+#define SHELL_COMMON_GIN_CONVERTERS_FRAME_CONVERTER_H_
+
+#include <utility>
+
+#include "gin/converter.h"
+
+namespace content {
+class RenderFrameHost;
+}  // namespace content
+
+namespace gin {
+
+template <>
+struct Converter<content::RenderFrameHost*> {
+  static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
+                                   content::RenderFrameHost* val);
+};
+
+}  // namespace gin
+
+#endif  // SHELL_COMMON_GIN_CONVERTERS_FRAME_CONVERTER_H_

+ 1 - 0
shell/common/node_bindings.cc

@@ -63,6 +63,7 @@
   V(electron_browser_view)               \
   V(electron_browser_web_contents)       \
   V(electron_browser_web_contents_view)  \
+  V(electron_browser_web_frame_main)     \
   V(electron_browser_web_view_manager)   \
   V(electron_browser_window)             \
   V(electron_common_asar)                \

+ 200 - 0
spec-main/api-web-frame-main-spec.ts

@@ -0,0 +1,200 @@
+import { expect } from 'chai';
+import * as http from 'http';
+import * as path from 'path';
+import * as url from 'url';
+import { BrowserWindow, WebFrameMain, webFrameMain } from 'electron/main';
+import { closeAllWindows } from './window-helpers';
+import { emittedOnce } from './events-helpers';
+import { AddressInfo } from 'net';
+
+describe('webFrameMain module', () => {
+  const fixtures = path.resolve(__dirname, '..', 'spec-main', 'fixtures');
+  const subframesPath = path.join(fixtures, 'sub-frames');
+
+  const fileUrl = (filename: string) => url.pathToFileURL(path.join(subframesPath, filename)).href;
+
+  afterEach(closeAllWindows);
+
+  describe('WebFrame traversal APIs', () => {
+    let w: BrowserWindow;
+    let webFrame: WebFrameMain;
+
+    beforeEach(async () => {
+      w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
+      await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
+      webFrame = w.webContents.mainFrame;
+    });
+
+    it('can access top frame', () => {
+      expect(webFrame.top).to.equal(webFrame);
+    });
+
+    it('has no parent on top frame', () => {
+      expect(webFrame.parent).to.be.null();
+    });
+
+    it('can access immediate frame descendents', () => {
+      const { frames } = webFrame;
+      expect(frames).to.have.lengthOf(1);
+      const subframe = frames[0];
+      expect(subframe).not.to.equal(webFrame);
+      expect(subframe.parent).to.equal(webFrame);
+    });
+
+    it('can access deeply nested frames', () => {
+      const subframe = webFrame.frames[0];
+      expect(subframe).not.to.equal(webFrame);
+      expect(subframe.parent).to.equal(webFrame);
+      const nestedSubframe = subframe.frames[0];
+      expect(nestedSubframe).not.to.equal(webFrame);
+      expect(nestedSubframe).not.to.equal(subframe);
+      expect(nestedSubframe.parent).to.equal(subframe);
+    });
+
+    it('can traverse all frames in root', () => {
+      const urls = webFrame.framesInSubtree.map(frame => frame.url);
+      expect(urls).to.deep.equal([
+        fileUrl('frame-with-frame-container.html'),
+        fileUrl('frame-with-frame.html'),
+        fileUrl('frame.html')
+      ]);
+    });
+
+    it('can traverse all frames in subtree', () => {
+      const urls = webFrame.frames[0].framesInSubtree.map(frame => frame.url);
+      expect(urls).to.deep.equal([
+        fileUrl('frame-with-frame.html'),
+        fileUrl('frame.html')
+      ]);
+    });
+
+    describe('cross-origin', () => {
+      type Server = { server: http.Server, url: string }
+
+      /** Creates an HTTP server whose handler embeds the given iframe src. */
+      const createServer = () => new Promise<Server>(resolve => {
+        const server = http.createServer((req, res) => {
+          const params = new URLSearchParams(url.parse(req.url || '').search || '');
+          if (params.has('frameSrc')) {
+            res.end(`<iframe src="${params.get('frameSrc')}"></iframe>`);
+          } else {
+            res.end('');
+          }
+        });
+        server.listen(0, '127.0.0.1', () => {
+          const url = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`;
+          resolve({ server, url });
+        });
+      });
+
+      let serverA = null as unknown as Server;
+      let serverB = null as unknown as Server;
+
+      before(async () => {
+        serverA = await createServer();
+        serverB = await createServer();
+      });
+
+      after(() => {
+        serverA.server.close();
+        serverB.server.close();
+      });
+
+      it('can access cross-origin frames', async () => {
+        await w.loadURL(`${serverA.url}?frameSrc=${serverB.url}`);
+        webFrame = w.webContents.mainFrame;
+        expect(webFrame.url.startsWith(serverA.url)).to.be.true();
+        expect(webFrame.frames[0].url).to.equal(serverB.url);
+      });
+    });
+  });
+
+  describe('WebFrame.url', () => {
+    it('should report correct address for each subframe', async () => {
+      const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
+      await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
+      const webFrame = w.webContents.mainFrame;
+
+      expect(webFrame.url).to.equal(fileUrl('frame-with-frame-container.html'));
+      expect(webFrame.frames[0].url).to.equal(fileUrl('frame-with-frame.html'));
+      expect(webFrame.frames[0].frames[0].url).to.equal(fileUrl('frame.html'));
+    });
+  });
+
+  describe('WebFrame IDs', () => {
+    it('has properties for various identifiers', async () => {
+      const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
+      await w.loadFile(path.join(subframesPath, 'frame.html'));
+      const webFrame = w.webContents.mainFrame;
+      expect(webFrame).to.haveOwnProperty('frameTreeNodeId');
+      expect(webFrame).to.haveOwnProperty('processId');
+      expect(webFrame).to.haveOwnProperty('routingId');
+    });
+  });
+
+  describe('WebFrame.executeJavaScript', () => {
+    it('can inject code into any subframe', async () => {
+      const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
+      await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
+      const webFrame = w.webContents.mainFrame;
+
+      const getUrl = (frame: WebFrameMain) => frame.executeJavaScript('location.href');
+      expect(await getUrl(webFrame)).to.equal(fileUrl('frame-with-frame-container.html'));
+      expect(await getUrl(webFrame.frames[0])).to.equal(fileUrl('frame-with-frame.html'));
+      expect(await getUrl(webFrame.frames[0].frames[0])).to.equal(fileUrl('frame.html'));
+    });
+  });
+
+  describe('WebFrame.reload', () => {
+    it('reloads a frame', async () => {
+      const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
+      await w.loadFile(path.join(subframesPath, 'frame.html'));
+      const webFrame = w.webContents.mainFrame;
+
+      await webFrame.executeJavaScript('window.TEMP = 1', false);
+      expect(webFrame.reload()).to.be.true();
+      await emittedOnce(w.webContents, 'dom-ready');
+      expect(await webFrame.executeJavaScript('window.TEMP', false)).to.be.null();
+    });
+  });
+
+  describe('disposed WebFrames', () => {
+    let w: BrowserWindow;
+    let webFrame: WebFrameMain;
+
+    before(async () => {
+      w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
+      await w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
+      webFrame = w.webContents.mainFrame;
+      w.destroy();
+      // Wait for WebContents, and thus RenderFrameHost, to be destroyed.
+      await new Promise(resolve => setTimeout(resolve, 0));
+    });
+
+    it('throws upon accessing properties', () => {
+      expect(() => webFrame.url).to.throw();
+    });
+  });
+
+  it('webFrameMain.fromId can find each frame from navigation events', (done) => {
+    const w = new BrowserWindow({ show: false, webPreferences: { contextIsolation: true } });
+
+    w.loadFile(path.join(subframesPath, 'frame-with-frame-container.html'));
+
+    let eventCount = 0;
+    w.webContents.on('did-frame-finish-load', (event, isMainFrame, frameProcessId, frameRoutingId) => {
+      const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
+      expect(frame).not.to.be.null();
+      expect(frame?.processId).to.be.equal(frameProcessId);
+      expect(frame?.routingId).to.be.equal(frameRoutingId);
+      expect(frame?.top === frame).to.be.equal(isMainFrame);
+
+      eventCount++;
+
+      // frame-with-frame-container.html, frame-with-frame.html, frame.html
+      if (eventCount === 3) {
+        done();
+      }
+    });
+  });
+});

+ 4 - 4
yarn.lock

@@ -33,10 +33,10 @@
     ora "^4.0.3"
     pretty-ms "^5.1.0"
 
-"@electron/typescript-definitions@^8.7.9":
-  version "8.7.9"
-  resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.7.9.tgz#6fe8856341e9ff77af803a9094be92759518c926"
-  integrity sha512-fiJr1KDR1auWTBfggMTRK/ouhHZV2iVumitkkNIA7NKONlVPLtcYf6/JgkWDla+y4CUTzM7M7R5AVSE0f/RuYA==
+"@electron/typescript-definitions@^8.8.0":
+  version "8.8.0"
+  resolved "https://registry.yarnpkg.com/@electron/typescript-definitions/-/typescript-definitions-8.8.0.tgz#3af8989507af50b3b06b23833a45a5631ab31d3f"
+  integrity sha512-HXcLOzI6zNFTzye3R/aSuqBAiVkUWVnogHwRe4mEdS4nodOqKZQxaB5tzPU2qZ4mS5cpVykBW4s6qAItuptoCA==
   dependencies:
     "@types/node" "^11.13.7"
     chalk "^2.4.2"