Browse Source

feat: add support for will-navigate and will-fail-load custom error pages

Samuel Attard 4 years ago
parent
commit
b2625db2ba

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

@@ -57,6 +57,25 @@ Process: [Main](../glossary.md#main-process)
 Emitted when the navigation is done, i.e. the spinner of the tab has stopped
 spinning, and the `onload` event was dispatched.
 
+#### Event: 'will-fail-load'
+
+Returns:
+
+* `event` Event
+* `url` String
+* `isInPlace` Boolean
+* `isMainFrame` Boolean
+* `frameProcessId` Integer
+* `frameRoutingId` Integer
+* `errorCode` Integer
+* `errorDescription` String
+
+This event will be emitted after `did-start-loading` and always before the
+`did-fail-load` event for the same navigation.
+
+Settings `event.returnValue` to an HTML string will result in a custom error page being
+displayed using that HTML.
+
 #### Event: 'did-fail-load'
 
 Returns:
@@ -243,6 +262,9 @@ this purpose.
 
 Calling `event.preventDefault()` will prevent the navigation.
 
+Settings `event.returnValue` to an HTML string will result in a custom error page being
+displayed using that HTML and the navigation being cancelled.
+
 #### Event: 'did-start-navigation'
 
 Returns:

+ 17 - 0
lib/browser/api/web-contents.ts

@@ -574,6 +574,23 @@ WebContents.prototype._init = function () {
     ipcMain.emit(channel, event, message);
   });
 
+  const handleCustomErrorPageEvent = (eventName: string) => {
+    this.on(`-${eventName}` as any, function (this: any, event: any, ...args: any[]) {
+      Object.defineProperty(event, 'returnValue', {
+        set: (value) => {
+          if (typeof value !== 'string') throw new TypeError(`event.returnValue must be set to a string, was set to a "${typeof value}"`);
+          if (value.length === 0) throw new Error('event.returnValue must be a non-empty string, an empty string was provided');
+          event.preventDefault();
+          event.sendReply(value);
+        },
+        get: () => {}
+      });
+      this.emit(eventName, event, ...args);
+    });
+  };
+  handleCustomErrorPageEvent('will-navigate');
+  handleCustomErrorPageEvent('will-fail-load');
+
   // Handle context menu action request from pepper plugin.
   this.on('pepper-context-menu' as any, function (event: any, params: {x: number, y: number, menu: Array<(MenuItemConstructorOptions) | (MenuItem)>}, callback: () => void) {
     // Access Menu via electron.Menu to prevent circular require.

+ 28 - 10
shell/browser/api/electron_api_web_contents.cc

@@ -1470,7 +1470,8 @@ void WebContents::DidStopLoading() {
 
 bool WebContents::EmitNavigationEvent(
     const std::string& event,
-    content::NavigationHandle* navigation_handle) {
+    content::NavigationHandle* navigation_handle,
+    gin_helper::Event::ValueCallback callback) {
   bool is_main_frame = navigation_handle->IsInMainFrame();
   int frame_tree_node_id = navigation_handle->GetFrameTreeNodeId();
   content::FrameTreeNode* frame_tree_node =
@@ -1490,8 +1491,18 @@ bool WebContents::EmitNavigationEvent(
   }
   bool is_same_document = navigation_handle->IsSameDocument();
   auto url = navigation_handle->GetURL();
-  return Emit(event, url, is_same_document, is_main_frame, frame_process_id,
-              frame_routing_id);
+  int code = navigation_handle->GetNetErrorCode();
+  auto description = net::ErrorToShortString(code);
+  return EmitWithSender(event, nullptr, std::move(callback), url,
+                        is_same_document, is_main_frame, frame_process_id,
+                        frame_routing_id, code, description);
+}
+
+bool WebContents::EmitNavigationEvent(
+    const std::string& event,
+    content::NavigationHandle* navigation_handle) {
+  return EmitNavigationEvent(event, navigation_handle,
+                             gin_helper::Event::ValueCallback());
 }
 
 void WebContents::BindElectronBrowser(
@@ -1513,8 +1524,9 @@ void WebContents::Message(bool internal,
   TRACE_EVENT1("electron", "WebContents::Message", "channel", channel);
   // webContents.emit('-ipc-message', new Event(), internal, channel,
   // arguments);
-  EmitWithSender("-ipc-message", receivers_.current_context(), InvokeCallback(),
-                 internal, channel, std::move(arguments));
+  EmitWithSender("-ipc-message", receivers_.current_context(),
+                 gin_helper::Event::ValueCallback(), internal, channel,
+                 std::move(arguments));
 }
 
 void WebContents::Invoke(bool internal,
@@ -1524,7 +1536,9 @@ void WebContents::Invoke(bool internal,
   TRACE_EVENT1("electron", "WebContents::Invoke", "channel", channel);
   // webContents.emit('-ipc-invoke', new Event(), internal, channel, arguments);
   EmitWithSender("-ipc-invoke", receivers_.current_context(),
-                 std::move(callback), internal, channel, std::move(arguments));
+                 gin_helper::Event::AdaptInvokeCallbackToValueCallback(
+                     std::move(callback)),
+                 internal, channel, std::move(arguments));
 }
 
 void WebContents::OnFirstNonEmptyLayout() {
@@ -1541,8 +1555,9 @@ void WebContents::ReceivePostMessage(const std::string& channel,
       MessagePort::EntanglePorts(isolate, std::move(message.ports));
   v8::Local<v8::Value> message_value =
       electron::DeserializeV8Value(isolate, message);
-  EmitWithSender("-ipc-ports", receivers_.current_context(), InvokeCallback(),
-                 false, channel, message_value, std::move(wrapped_ports));
+  EmitWithSender("-ipc-ports", receivers_.current_context(),
+                 gin_helper::Event::ValueCallback(), false, channel,
+                 message_value, std::move(wrapped_ports));
 }
 
 void WebContents::PostMessage(const std::string& channel,
@@ -1586,7 +1601,9 @@ void WebContents::MessageSync(bool internal,
   // webContents.emit('-ipc-message-sync', new Event(sender, message), internal,
   // channel, arguments);
   EmitWithSender("-ipc-message-sync", receivers_.current_context(),
-                 std::move(callback), internal, channel, std::move(arguments));
+                 gin_helper::Event::AdaptInvokeCallbackToValueCallback(
+                     std::move(callback)),
+                 internal, channel, std::move(arguments));
 }
 
 void WebContents::MessageTo(bool internal,
@@ -1608,7 +1625,8 @@ void WebContents::MessageHost(const std::string& channel,
   TRACE_EVENT1("electron", "WebContents::MessageHost", "channel", channel);
   // webContents.emit('ipc-message-host', new Event(), channel, args);
   EmitWithSender("ipc-message-host", receivers_.current_context(),
-                 InvokeCallback(), channel, std::move(arguments));
+                 gin_helper::Event::ValueCallback(), channel,
+                 std::move(arguments));
 }
 
 void WebContents::UpdateDraggableRegions(

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

@@ -30,6 +30,7 @@
 #include "mojo/public/cpp/bindings/receiver_set.h"
 #include "printing/buildflags/buildflags.h"
 #include "services/service_manager/public/cpp/binder_registry.h"
+#include "shell/browser/api/event.h"
 #include "shell/browser/api/frame_subscriber.h"
 #include "shell/browser/api/save_page_handler.h"
 #include "shell/browser/event_emitter_mixin.h"
@@ -408,11 +409,15 @@ class WebContents : public gin::Wrappable<WebContents>,
   bool EmitNavigationEvent(const std::string& event,
                            content::NavigationHandle* navigation_handle);
 
+  bool EmitNavigationEvent(const std::string& event,
+                           content::NavigationHandle* navigation_handle,
+                           gin_helper::Event::ValueCallback callback);
+
   // this.emit(name, new Event(sender, message), args...);
   template <typename... Args>
   bool EmitWithSender(base::StringPiece name,
                       content::RenderFrameHost* sender,
-                      electron::mojom::ElectronBrowser::InvokeCallback callback,
+                      gin_helper::Event::ValueCallback callback,
                       Args&&... args) {
     DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
     v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();

+ 26 - 8
shell/browser/api/event.cc

@@ -14,6 +14,22 @@
 
 namespace gin_helper {
 
+namespace {
+
+bool InvokeCallbackAdapter(Event::InvokeCallback callback,
+                           v8::Local<v8::Value> result) {
+  v8::Isolate* isolate = electron::JavascriptEnvironment::GetIsolate();
+  blink::CloneableMessage message;
+  if (!gin::ConvertFromV8(isolate, result, &message)) {
+    return false;
+  }
+
+  std::move(callback).Run(std::move(message));
+  return true;
+}
+
+}  // namespace
+
 gin::WrapperInfo Event::kWrapperInfo = {gin::kEmbedderNativeGin};
 
 Event::Event() {}
@@ -29,7 +45,7 @@ Event::~Event() {
   }
 }
 
-void Event::SetCallback(InvokeCallback callback) {
+void Event::SetCallback(ValueCallback callback) {
   DCHECK(!callback_);
   callback_ = std::move(callback);
 }
@@ -45,13 +61,7 @@ bool Event::SendReply(v8::Isolate* isolate, v8::Local<v8::Value> result) {
   if (!callback_)
     return false;
 
-  blink::CloneableMessage message;
-  if (!gin::ConvertFromV8(isolate, result, &message)) {
-    return false;
-  }
-
-  std::move(callback_).Run(std::move(message));
-  return true;
+  return std::move(callback_).Run(result);
 }
 
 gin::ObjectTemplateBuilder Event::GetObjectTemplateBuilder(
@@ -70,4 +80,12 @@ gin::Handle<Event> Event::Create(v8::Isolate* isolate) {
   return gin::CreateHandle(isolate, new Event());
 }
 
+Event::ValueCallback Event::AdaptInvokeCallbackToValueCallback(
+    Event::InvokeCallback callback) {
+  if (!callback)
+    return ValueCallback();
+
+  return base::BindOnce(InvokeCallbackAdapter, std::move(callback));
+}
+
 }  // namespace gin_helper

+ 7 - 3
shell/browser/api/event.h

@@ -18,17 +18,21 @@ namespace gin_helper {
 class Event : public gin::Wrappable<Event> {
  public:
   using InvokeCallback = electron::mojom::ElectronBrowser::InvokeCallback;
+  using ValueCallback = base::OnceCallback<bool(v8::Local<v8::Value>)>;
 
   static gin::WrapperInfo kWrapperInfo;
 
   static gin::Handle<Event> Create(v8::Isolate* isolate);
 
-  // Pass the callback to be invoked.
-  void SetCallback(InvokeCallback callback);
+  static ValueCallback AdaptInvokeCallbackToValueCallback(
+      InvokeCallback callback);
 
   // event.PreventDefault().
   void PreventDefault(v8::Isolate* isolate);
 
+  // Pass the callback to be invoked.
+  void SetCallback(ValueCallback callback);
+
   // event.sendReply(value), used for replying to synchronous messages and
   // `invoke` calls.
   bool SendReply(v8::Isolate* isolate, v8::Local<v8::Value> result);
@@ -44,7 +48,7 @@ class Event : public gin::Wrappable<Event> {
 
  private:
   // Replyer for the synchronous messages.
-  InvokeCallback callback_;
+  ValueCallback callback_;
 
   DISALLOW_COPY_AND_ASSIGN(Event);
 };

+ 50 - 3
shell/browser/electron_navigation_throttle.cc

@@ -4,12 +4,32 @@
 
 #include "shell/browser/electron_navigation_throttle.h"
 
+#include <memory>
+#include <string>
+
 #include "content/public/browser/navigation_handle.h"
 #include "shell/browser/api/electron_api_web_contents.h"
 #include "shell/browser/javascript_environment.h"
 
 namespace electron {
 
+namespace {
+
+bool HandleEventReturnValue(std::shared_ptr<bool> is_delay_called,
+                            std::shared_ptr<std::string> error_html,
+                            v8::Local<v8::Value> val) {
+  if (*is_delay_called)
+    return true;
+
+  v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
+  std::string provided_html;
+  if (gin::ConvertFromV8(isolate, val, &provided_html))
+    *error_html = provided_html;
+  return true;
+}
+
+}  // namespace
+
 ElectronNavigationThrottle::ElectronNavigationThrottle(
     content::NavigationHandle* navigation_handle)
     : content::NavigationThrottle(navigation_handle) {}
@@ -21,7 +41,9 @@ const char* ElectronNavigationThrottle::GetNameForLogging() {
 }
 
 content::NavigationThrottle::ThrottleCheckResult
-ElectronNavigationThrottle::WillStartRequest() {
+ElectronNavigationThrottle::DelegateEventToWebContents(
+    const std::string& event_name,
+    net::Error error_code) {
   auto* handle = navigation_handle();
   auto* contents = handle->GetWebContents();
   if (!contents) {
@@ -37,10 +59,35 @@ ElectronNavigationThrottle::WillStartRequest() {
     return PROCEED;
   }
 
-  if (handle->IsRendererInitiated() && handle->IsInMainFrame() &&
-      api_contents->EmitNavigationEvent("will-navigate", handle)) {
+  std::shared_ptr<bool> is_delay_called(new bool(false));
+  std::shared_ptr<std::string> error_html(new std::string);
+  if (api_contents->EmitNavigationEvent(
+          event_name, handle,
+          base::BindOnce(&HandleEventReturnValue, is_delay_called,
+                         error_html))) {
+    *is_delay_called = true;
+
+    if (!error_html->empty()) {
+      return content::NavigationThrottle::ThrottleCheckResult(
+          CANCEL, error_code, *error_html);
+    }
     return CANCEL;
   }
+  *is_delay_called = true;
+  return PROCEED;
+}
+
+content::NavigationThrottle::ThrottleCheckResult
+ElectronNavigationThrottle::WillFailRequest() {
+  return DelegateEventToWebContents("-will-fail-load",
+                                    navigation_handle()->GetNetErrorCode());
+}
+
+content::NavigationThrottle::ThrottleCheckResult
+ElectronNavigationThrottle::WillStartRequest() {
+  auto* handle = navigation_handle();
+  if (handle->IsRendererInitiated() && handle->IsInMainFrame())
+    return DelegateEventToWebContents("-will-navigate", net::ERR_FAILED);
   return PROCEED;
 }
 

+ 8 - 0
shell/browser/electron_navigation_throttle.h

@@ -7,6 +7,8 @@
 
 #include "content/public/browser/navigation_throttle.h"
 
+#include <string>
+
 namespace electron {
 
 class ElectronNavigationThrottle : public content::NavigationThrottle {
@@ -16,12 +18,18 @@ class ElectronNavigationThrottle : public content::NavigationThrottle {
 
   ElectronNavigationThrottle::ThrottleCheckResult WillStartRequest() override;
 
+  ElectronNavigationThrottle::ThrottleCheckResult WillFailRequest() override;
+
   ElectronNavigationThrottle::ThrottleCheckResult WillRedirectRequest()
       override;
 
   const char* GetNameForLogging() override;
 
  private:
+  content::NavigationThrottle::ThrottleCheckResult DelegateEventToWebContents(
+      const std::string& event_name,
+      net::Error error_code);
+
   DISALLOW_COPY_AND_ASSIGN(ElectronNavigationThrottle);
 };
 

+ 5 - 6
shell/common/gin_helper/event_emitter.cc

@@ -49,13 +49,12 @@ v8::Local<v8::Object> CreateEvent(v8::Isolate* isolate,
   return event;
 }
 
-v8::Local<v8::Object> CreateNativeEvent(
-    v8::Isolate* isolate,
-    v8::Local<v8::Object> sender,
-    content::RenderFrameHost* frame,
-    electron::mojom::ElectronBrowser::MessageSyncCallback callback) {
+v8::Local<v8::Object> CreateNativeEvent(v8::Isolate* isolate,
+                                        v8::Local<v8::Object> sender,
+                                        content::RenderFrameHost* frame,
+                                        Event::ValueCallback callback) {
   v8::Local<v8::Object> event;
-  if (frame && callback) {
+  if (callback) {
     gin::Handle<Event> native_event = Event::Create(isolate);
     native_event->SetCallback(std::move(callback));
     event = v8::Local<v8::Object>::Cast(native_event.ToV8());

+ 1 - 1
shell/common/gin_helper/event_emitter.h

@@ -29,7 +29,7 @@ v8::Local<v8::Object> CreateNativeEvent(
     v8::Isolate* isolate,
     v8::Local<v8::Object> sender,
     content::RenderFrameHost* frame,
-    electron::mojom::ElectronBrowser::MessageSyncCallback callback);
+    base::OnceCallback<bool(v8::Local<v8::Value>)> callback);
 
 }  // namespace internal