fix: don't handle browser messages before document element is created (#19718)

* fix: don't handle browser messages before document element is created

* fix: bind ElectronApiServiceImpl later

DidCreateDocumentElement is called before the ElectronApiServiceImpl
gets bound.

* chore: add comment
This commit is contained in:
Cheng Zhao 2019-08-13 02:38:41 +09:00 committed by Samuel Attard
parent 398c5d553d
commit 04dbd5c53f
5 changed files with 90 additions and 35 deletions

View file

@ -98,38 +98,61 @@ ElectronApiServiceImpl::~ElectronApiServiceImpl() = default;
ElectronApiServiceImpl::ElectronApiServiceImpl( ElectronApiServiceImpl::ElectronApiServiceImpl(
content::RenderFrame* render_frame, content::RenderFrame* render_frame,
RendererClientBase* renderer_client, RendererClientBase* renderer_client)
mojom::ElectronRendererAssociatedRequest request)
: content::RenderFrameObserver(render_frame), : content::RenderFrameObserver(render_frame),
binding_(this), binding_(this),
render_frame_(render_frame), renderer_client_(renderer_client),
renderer_client_(renderer_client) { weak_factory_(this) {}
void ElectronApiServiceImpl::BindTo(
mojom::ElectronRendererAssociatedRequest request) {
// Note: BindTo might be called for multiple times.
if (binding_.is_bound())
binding_.Unbind();
binding_.Bind(std::move(request)); binding_.Bind(std::move(request));
binding_.set_connection_error_handler(base::BindOnce( binding_.set_connection_error_handler(
&ElectronApiServiceImpl::OnDestruct, base::Unretained(this))); base::BindOnce(&ElectronApiServiceImpl::OnConnectionError, GetWeakPtr()));
} }
// static void ElectronApiServiceImpl::DidCreateDocumentElement() {
void ElectronApiServiceImpl::CreateMojoService( document_created_ = true;
content::RenderFrame* render_frame,
RendererClientBase* renderer_client,
mojom::ElectronRendererAssociatedRequest request) {
DCHECK(render_frame);
// Owns itself. Will be deleted when the render frame is destroyed.
new ElectronApiServiceImpl(render_frame, renderer_client, std::move(request));
} }
void ElectronApiServiceImpl::OnDestruct() { void ElectronApiServiceImpl::OnDestruct() {
delete this; delete this;
} }
void ElectronApiServiceImpl::OnConnectionError() {
if (binding_.is_bound())
binding_.Unbind();
}
void ElectronApiServiceImpl::Message(bool internal, void ElectronApiServiceImpl::Message(bool internal,
bool send_to_all, bool send_to_all,
const std::string& channel, const std::string& channel,
base::Value arguments, base::Value arguments,
int32_t sender_id) { int32_t sender_id) {
blink::WebLocalFrame* frame = render_frame_->GetWebFrame(); // Don't handle browser messages before document element is created.
//
// Note: It is probably better to save the message and then replay it after
// document is ready, but current behavior has been there since the first
// day of Electron, and no one has complained so far.
//
// Reason 1:
// When we receive a message from the browser, we try to transfer it
// to a web page, and when we do that Blink creates an empty
// document element if it hasn't been created yet, and it makes our init
// script to run while `window.location` is still "about:blank".
// (See https://github.com/electron/electron/pull/1044.)
//
// Reason 2:
// The libuv message loop integration would be broken for unkown reasons.
// (See https://github.com/electron/electron/issues/19368.)
if (!document_created_)
return;
blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
if (!frame) if (!frame)
return; return;

View file

@ -7,6 +7,7 @@
#include <string> #include <string>
#include "base/memory/weak_ptr.h"
#include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h" #include "content/public/renderer/render_frame_observer.h"
#include "electron/shell/common/api/api.mojom.h" #include "electron/shell/common/api/api.mojom.h"
@ -19,10 +20,10 @@ class RendererClientBase;
class ElectronApiServiceImpl : public mojom::ElectronRenderer, class ElectronApiServiceImpl : public mojom::ElectronRenderer,
public content::RenderFrameObserver { public content::RenderFrameObserver {
public: public:
static void CreateMojoService( ElectronApiServiceImpl(content::RenderFrame* render_frame,
content::RenderFrame* render_frame, RendererClientBase* renderer_client);
RendererClientBase* renderer_client,
mojom::ElectronRendererAssociatedRequest request); void BindTo(mojom::ElectronRendererAssociatedRequest request);
void Message(bool internal, void Message(bool internal,
bool send_to_all, bool send_to_all,
@ -33,19 +34,26 @@ class ElectronApiServiceImpl : public mojom::ElectronRenderer,
void TakeHeapSnapshot(mojo::ScopedHandle file, void TakeHeapSnapshot(mojo::ScopedHandle file,
TakeHeapSnapshotCallback callback) override; TakeHeapSnapshotCallback callback) override;
base::WeakPtr<ElectronApiServiceImpl> GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
private: private:
~ElectronApiServiceImpl() override; ~ElectronApiServiceImpl() override;
ElectronApiServiceImpl(content::RenderFrame* render_frame,
RendererClientBase* renderer_client,
mojom::ElectronRendererAssociatedRequest request);
// RenderFrameObserver implementation. // RenderFrameObserver implementation.
void DidCreateDocumentElement() override;
void OnDestruct() override; void OnDestruct() override;
void OnConnectionError();
// Whether the DOM document element has been created.
bool document_created_ = false;
mojo::AssociatedBinding<mojom::ElectronRenderer> binding_; mojo::AssociatedBinding<mojom::ElectronRenderer> binding_;
content::RenderFrame* render_frame_;
RendererClientBase* renderer_client_; RendererClientBase* renderer_client_;
base::WeakPtrFactory<ElectronApiServiceImpl> weak_factory_;
DISALLOW_COPY_AND_ASSIGN(ElectronApiServiceImpl); DISALLOW_COPY_AND_ASSIGN(ElectronApiServiceImpl);
}; };

View file

@ -227,17 +227,12 @@ void RendererClientBase::RenderFrameCreated(
std::make_unique<electron::PrintRenderFrameHelperDelegate>()); std::make_unique<electron::PrintRenderFrameHelperDelegate>());
#endif #endif
// TODO(nornagon): it might be possible for an IPC message sent to this // Note: ElectronApiServiceImpl has to be created now to capture the
// service to trigger v8 context creation before the page has begun loading. // DidCreateDocumentElement event.
// However, it's unclear whether such a timing is possible to trigger, and we auto* service = new ElectronApiServiceImpl(render_frame, this);
// don't have any test to confirm it. Add a test that confirms that a
// main->renderer IPC can't cause the preload script to be executed twice. If
// it is possible to trigger the preload script before the document is ready
// through this interface, we should delay adding it to the registry until
// the document is ready.
render_frame->GetAssociatedInterfaceRegistry()->AddInterface( render_frame->GetAssociatedInterfaceRegistry()->AddInterface(
base::BindRepeating(&ElectronApiServiceImpl::CreateMojoService, base::BindRepeating(&ElectronApiServiceImpl::BindTo,
render_frame, this)); service->GetWeakPtr()));
#if BUILDFLAG(ENABLE_PDF_VIEWER) #if BUILDFLAG(ENABLE_PDF_VIEWER)
// Allow access to file scheme from pdf viewer. // Allow access to file scheme from pdf viewer.

View file

@ -3,7 +3,7 @@ import { AddressInfo } from 'net'
import * as chaiAsPromised from 'chai-as-promised' import * as chaiAsPromised from 'chai-as-promised'
import * as path from 'path' import * as path from 'path'
import * as http from 'http' import * as http from 'http'
import { BrowserWindow, webContents } from 'electron' import { BrowserWindow, ipcMain, webContents } from 'electron'
import { emittedOnce } from './events-helpers'; import { emittedOnce } from './events-helpers';
import { closeAllWindows } from './window-helpers'; import { closeAllWindows } from './window-helpers';
@ -77,6 +77,26 @@ describe('webContents module', () => {
w.webContents.send(null as any) w.webContents.send(null as any)
}).to.throw('Missing required channel argument') }).to.throw('Missing required channel argument')
}) })
it('does not block node async APIs when sent before document is ready', (done) => {
// Please reference https://github.com/electron/electron/issues/19368 if
// this test fails.
ipcMain.once('async-node-api-done', () => {
done()
})
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
sandbox: false,
contextIsolation: false
}
})
w.loadFile(path.join(fixturesPath, 'pages', 'send-after-node.html'))
setTimeout(() => {
w.webContents.send("test")
}, 50)
})
}) })
describe('webContents.executeJavaScript', () => { describe('webContents.executeJavaScript', () => {

View file

@ -0,0 +1,9 @@
<html>
<body>
<script type="text/javascript" charset="utf-8">
require('fs').readdir(process.cwd(), () => {
require('electron').ipcRenderer.send('async-node-api-done');
})
</script>
</body>
</html>