Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: promisify executeJavaScript
  • Loading branch information
miniak committed Mar 9, 2019
1 parent dd1afbd commit 17b9734
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 97 deletions.
50 changes: 28 additions & 22 deletions atom/renderer/api/atom_api_web_frame.cc
Expand Up @@ -13,6 +13,7 @@
#include "atom/common/native_mate_converters/callback.h"
#include "atom/common/native_mate_converters/gfx_converter.h"
#include "atom/common/native_mate_converters/string16_converter.h"
#include "atom/common/promise_util.h"
#include "atom/renderer/api/atom_api_spell_check_client.h"
#include "base/memory/memory_pressure_listener.h"
#include "content/public/renderer/render_frame.h"
Expand Down Expand Up @@ -93,23 +94,20 @@ class RenderFrameStatus : public content::RenderFrameObserver {

class ScriptExecutionCallback : public blink::WebScriptExecutionCallback {
public:
using CompletionCallback =
base::Callback<void(const v8::Local<v8::Value>& result)>;

explicit ScriptExecutionCallback(const CompletionCallback& callback)
: callback_(callback) {}
explicit ScriptExecutionCallback(atom::util::Promise promise)
: promise_(std::move(promise)) {}
~ScriptExecutionCallback() override {}

void Completed(
const blink::WebVector<v8::Local<v8::Value>>& result) override {
if (!callback_.is_null() && !result.empty() && !result[0].IsEmpty())
if (!result.empty() && !result[0].IsEmpty())
// Right now only single results per frame is supported.
callback_.Run(result[0]);
promise_.Resolve(result[0]);
delete this;
}

private:
CompletionCallback callback_;
atom::util::Promise promise_;

DISALLOW_COPY_AND_ASSIGN(ScriptExecutionCallback);
};
Expand Down Expand Up @@ -323,25 +321,34 @@ void InsertCSS(v8::Local<v8::Value> window, const std::string& css) {
}
}

void ExecuteJavaScript(mate::Arguments* args,
v8::Local<v8::Value> window,
const base::string16& code) {
v8::Local<v8::Promise> ExecuteJavaScript(mate::Arguments* args,
v8::Local<v8::Value> window,
const base::string16& code) {
v8::Isolate* isolate = args->isolate();
util::Promise promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();

bool has_user_gesture = false;
args->GetNext(&has_user_gesture);
ScriptExecutionCallback::CompletionCallback completion_callback;
args->GetNext(&completion_callback);
std::unique_ptr<blink::WebScriptExecutionCallback> callback(
new ScriptExecutionCallback(completion_callback));

auto callback = std::make_unique<ScriptExecutionCallback>(std::move(promise));

GetRenderFrame(window)->GetWebFrame()->RequestExecuteScriptAndReturnValue(
blink::WebScriptSource(blink::WebString::FromUTF16(code)),
has_user_gesture, callback.release());

return handle;
}

void ExecuteJavaScriptInIsolatedWorld(
v8::Local<v8::Promise> ExecuteJavaScriptInIsolatedWorld(
mate::Arguments* args,
v8::Local<v8::Value> window,
int world_id,
const std::vector<mate::Dictionary>& scripts) {
v8::Isolate* isolate = args->isolate();
util::Promise promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();

std::vector<blink::WebScriptSource> sources;

for (const auto& script : scripts) {
Expand All @@ -352,8 +359,8 @@ void ExecuteJavaScriptInIsolatedWorld(
script.Get("startLine", &start_line);

if (!script.Get("code", &code)) {
args->ThrowError("Invalid 'code'");
return;
promise.Reject("Invalid 'code'");
return handle;
}

sources.emplace_back(
Expand All @@ -368,14 +375,13 @@ void ExecuteJavaScriptInIsolatedWorld(
blink::WebLocalFrame::kSynchronous;
args->GetNext(&scriptExecutionType);

ScriptExecutionCallback::CompletionCallback completion_callback;
args->GetNext(&completion_callback);
std::unique_ptr<blink::WebScriptExecutionCallback> callback(
new ScriptExecutionCallback(completion_callback));
auto callback = std::make_unique<ScriptExecutionCallback>(std::move(promise));

GetRenderFrame(window)->GetWebFrame()->RequestExecuteScriptInIsolatedWorld(
world_id, &sources.front(), sources.size(), has_user_gesture,
scriptExecutionType, callback.release());

return handle;
}

void SetIsolatedWorldInfo(v8::Local<v8::Value> window,
Expand Down
8 changes: 4 additions & 4 deletions docs/api/promisification.md
Expand Up @@ -14,16 +14,13 @@ When a majority of affected functions are migrated, this flag will be enabled by
- [inAppPurchase.purchaseProduct(productID, quantity, callback)](https://github.com/electron/electron/blob/master/docs/api/in-app-purchase.md#purchaseProduct)
- [inAppPurchase.getProducts(productIDs, callback)](https://github.com/electron/electron/blob/master/docs/api/in-app-purchase.md#getProducts)
- [ses.getBlobData(identifier, callback)](https://github.com/electron/electron/blob/master/docs/api/session.md#getBlobData)
- [contents.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#executeJavaScript)
- [contents.print([options], [callback])](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#print)
- [webFrame.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-frame.md#executeJavaScript)
- [webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-frame.md#executeJavaScriptInIsolatedWorld)
- [webviewTag.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#executeJavaScript)

### Converted Functions

- [app.getFileIcon(path[, options], callback)](https://github.com/electron/electron/blob/master/docs/api/app.md#getFileIcon)
- [contents.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#capturePage)
- [contents.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#executeJavaScript)
- [contents.printToPDF(options, callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#printToPDF)
- [contents.savePage(fullPath, saveType, callback)](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#savePage)
- [contentTracing.getCategories(callback)](https://github.com/electron/electron/blob/master/docs/api/content-tracing.md#getCategories)
Expand All @@ -48,6 +45,9 @@ When a majority of affected functions are migrated, this flag will be enabled by
- [ses.clearAuthCache(options[, callback])](https://github.com/electron/electron/blob/master/docs/api/session.md#clearAuthCache)
- [ses.clearCache(callback)](https://github.com/electron/electron/blob/master/docs/api/session.md#clearCache)
- [shell.openExternal(url[, options, callback])](https://github.com/electron/electron/blob/master/docs/api/shell.md#openExternal)
- [webFrame.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-frame.md#executeJavaScript)
- [webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/web-frame.md#executeJavaScriptInIsolatedWorld)
- [webviewTag.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#capturePage)
- [webviewTag.executeJavaScript(code[, userGesture, callback])](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#executeJavaScript)
- [webviewTag.printToPDF(options, callback)](https://github.com/electron/electron/blob/master/docs/api/webview-tag.md#printToPDF)
- [win.capturePage([rect, ]callback)](https://github.com/electron/electron/blob/master/docs/api/browser-window.md#capturePage)
25 changes: 22 additions & 3 deletions docs/api/web-contents.md
Expand Up @@ -973,9 +973,28 @@ 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.

If the result of the executed code is a promise the callback result will be the
resolved value of the promise. We recommend that you use the returned Promise
to handle code that results in a Promise.
```js
contents.executeJavaScript('fetch("https://jsonplaceholder.typicode.com/users/1").then(resp => resp.json())', true)
.then((result) => {
console.log(result) // Will be the JSON object from the fetch call
})
```

**[Deprecated Soon](promisification.md)**

#### `contents.executeJavaScript(code[, userGesture])`

* `code` String
* `userGesture` Boolean (optional) - Default is `false`.

Returns `Promise<any>` - A promise that resolves with the result of the executed code
or is rejected if the result of the code is 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.

```js
contents.executeJavaScript('fetch("https://jsonplaceholder.typicode.com/users/1").then(resp => resp.json())', true)
Expand Down
34 changes: 33 additions & 1 deletion docs/api/web-frame.md
Expand Up @@ -117,6 +117,22 @@ 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.

**[Deprecated Soon](promisification.md)**

### `webFrame.executeJavaScript(code[, userGesture])`

* `code` String
* `userGesture` Boolean (optional) - Default is `false`.

Returns `Promise<any>` - A promise that resolves with the result of the executed code
or is rejected if the result of the code is 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.

### `webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])`

* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here.
Expand All @@ -125,7 +141,23 @@ this limitation.
* `callback` Function (optional) - Called after script has been executed.
* `result` Any

Work like `executeJavaScript` but evaluates `scripts` in an isolated context.
Returns `Promise<any>` - A promise that resolves with the result of the executed code
or is rejected if the result of the code is a rejected promise.

Works like `executeJavaScript` but evaluates `scripts` in an isolated context.

**[Deprecated Soon](promisification.md)**

### `webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture])`

* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here.
* `scripts` [WebSource[]](structures/web-source.md)
* `userGesture` Boolean (optional) - Default is `false`.

Returns `Promise<any>` - A promise that resolves with the result of the executed code
or is rejected if the result of the code is a rejected promise.

Works like `executeJavaScript` but evaluates `scripts` in an isolated context.

### `webFrame.setIsolatedWorldContentSecurityPolicy(worldId, csp)` _(Deprecated)_

Expand Down
17 changes: 17 additions & 0 deletions docs/api/webview-tag.md
Expand Up @@ -374,6 +374,23 @@ Injects CSS into the guest page.
* `callback` Function (optional) - Called after script has been executed.
* `result` Any

Returns `Promise<any>` - A promise that resolves with the result of the executed code
or is rejected if the result of the code is a rejected promise.

Evaluates `code` in page. If `userGesture` is set, it will create the user
gesture context in the page. HTML APIs like `requestFullScreen`, which require
user action, can take advantage of this option for automation.

**[Deprecated Soon](promisification.md)**

### `<webview>.executeJavaScript(code[, userGesture])`

* `code` String
* `userGesture` Boolean (optional) - Default `false`.

Returns `Promise<any>` - A promise that resolves with the result of the executed code
or is rejected if the result of the code is a rejected promise.

Evaluates `code` in page. If `userGesture` is set, it will create the user
gesture context in the page. HTML APIs like `requestFullScreen`, which require
user action, can take advantage of this option for automation.
Expand Down
37 changes: 8 additions & 29 deletions lib/browser/api/web-contents.js
Expand Up @@ -178,47 +178,25 @@ const webFrameMethods = [
'setVisualZoomLevelLimits'
]

const asyncWebFrameMethods = function (requestId, method, callback, ...args) {
return new Promise((resolve, reject) => {
ipcMainInternal.once(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, function (event, error, result) {
if (error == null) {
if (typeof callback === 'function') callback(result)
resolve(result)
} else {
reject(errorUtils.deserialize(error))
}
})
this._sendInternal('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', requestId, method, args)
})
}

for (const method of webFrameMethods) {
WebContents.prototype[method] = function (...args) {
ipcMainUtils.invoke(this, 'ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', method, ...args)
}
}

const executeJavaScript = (sender, code, hasUserGesture) => {
return ipcMainUtils.invoke(sender, 'ELECTRON_INTERNAL_RENDERER_WEB_FRAME_METHOD', 'executeJavaScript', code, hasUserGesture)
}

// Make sure WebContents::executeJavaScript would run the code only when the
// WebContents has been loaded.
WebContents.prototype.executeJavaScript = function (code, hasUserGesture, callback) {
const requestId = getNextId()

if (typeof hasUserGesture === 'function') {
// Shift.
callback = hasUserGesture
hasUserGesture = null
}

if (hasUserGesture == null) {
hasUserGesture = false
}

WebContents.prototype.executeJavaScript = function (code, hasUserGesture) {
if (this.getURL() && !this.isLoadingMainFrame()) {
return asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture)
return executeJavaScript(this, code, hasUserGesture)
} else {
return new Promise((resolve, reject) => {
this.once('did-stop-loading', () => {
asyncWebFrameMethods.call(this, requestId, 'executeJavaScript', callback, code, hasUserGesture).then(resolve).catch(reject)
executeJavaScript(this, code, hasUserGesture).then(resolve, reject)
})
})
}
Expand Down Expand Up @@ -345,6 +323,7 @@ WebContents.prototype.loadFile = function (filePath, options = {}) {
}

WebContents.prototype.capturePage = deprecate.promisify(WebContents.prototype.capturePage)
WebContents.prototype.executeJavaScript = deprecate.promisify(WebContents.prototype.executeJavaScript)
WebContents.prototype.printToPDF = deprecate.promisify(WebContents.prototype.printToPDF)
WebContents.prototype.savePage = deprecate.promisify(WebContents.prototype.savePage)

Expand Down
9 changes: 9 additions & 0 deletions lib/renderer/api/web-frame.ts
Expand Up @@ -85,6 +85,15 @@ function getWebFrame (context: Window) {
return context ? new WebFrame(context) : null
}

const promisifiedMethods = new Set<string>([
'executeJavaScript',
'executeJavaScriptInIsolatedWorld'
])

for (const method of promisifiedMethods) {
(WebFrame as any).prototype[method] = deprecate.promisify((WebFrame as any).prototype[method])
}

const _webFrame = new WebFrame(window)

export default _webFrame
18 changes: 8 additions & 10 deletions lib/renderer/security-warnings.ts
Expand Up @@ -64,16 +64,14 @@ const getIsRemoteProtocol = function () {
* @returns {boolean} Is a CSP with `unsafe-eval` set?
*/
const isUnsafeEvalEnabled = function () {
return new Promise((resolve) => {
webFrame.executeJavaScript(`(${(() => {
try {
new Function('') // eslint-disable-line no-new,no-new-func
} catch {
return false
}
return true
}).toString()})()`, false, resolve)
})
return webFrame.executeJavaScript(`(${(() => {
try {
new Function('') // eslint-disable-line no-new,no-new-func
} catch {
return false
}
return true
}).toString()})()`, false)
}

const moreInformation = `\nFor more information and help, consult
Expand Down
19 changes: 0 additions & 19 deletions lib/renderer/web-frame-init.ts
@@ -1,7 +1,5 @@
import { webFrame, WebFrame } from 'electron'
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal'
import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils'
import * as errorUtils from '@electron/internal/common/error-utils'

// All keys of WebFrame that extend Function
type WebFrameMethod = {
Expand All @@ -19,21 +17,4 @@ export const webFrameInit = () => {
// will be caught by "keyof WebFrameMethod" though.
return (webFrame[method] as any)(...args)
})

ipcRendererInternal.on('ELECTRON_INTERNAL_RENDERER_ASYNC_WEB_FRAME_METHOD', (
event, requestId: number, method: keyof WebFrameMethod, args: any[]
) => {
new Promise(resolve =>
// The TypeScript compiler cannot handle the sheer number of
// call signatures here and simply gives up. Incorrect invocations
// will be caught by "keyof WebFrameMethod" though.
(webFrame[method] as any)(...args, resolve)
).then(result => {
return [null, result]
}, error => {
return [errorUtils.serialize(error)]
}).then(responseArgs => {
event.sender.send(`ELECTRON_INTERNAL_BROWSER_ASYNC_WEB_FRAME_RESPONSE_${requestId}`, ...responseArgs)
})
})
}
16 changes: 15 additions & 1 deletion spec/api-web-contents-spec.js
Expand Up @@ -1076,7 +1076,21 @@ describe('webContents module', () => {
})

describe('webframe messages in sandboxed contents', () => {
it('responds to executeJavaScript', (done) => {
it('responds to executeJavaScript', async () => {
w.destroy()
w = new BrowserWindow({
show: false,
webPreferences: {
sandbox: true
}
})
await w.loadURL('about:blank')
const result = await w.webContents.executeJavaScript('37 + 5')
assert.strictEqual(result, 42)
})

// TODO(miniak): remove when promisification is complete
it('responds to executeJavaScript (callback)', (done) => {
w.destroy()
w = new BrowserWindow({
show: false,
Expand Down

0 comments on commit 17b9734

Please sign in to comment.