summaryrefslogtreecommitdiffstats
path: root/remote/marionette/evaluate.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--remote/marionette/evaluate.sys.mjs354
1 files changed, 354 insertions, 0 deletions
diff --git a/remote/marionette/evaluate.sys.mjs b/remote/marionette/evaluate.sys.mjs
new file mode 100644
index 0000000000..a43908e7ad
--- /dev/null
+++ b/remote/marionette/evaluate.sys.mjs
@@ -0,0 +1,354 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
+});
+
+const ARGUMENTS = "__webDriverArguments";
+const CALLBACK = "__webDriverCallback";
+const COMPLETE = "__webDriverComplete";
+const DEFAULT_TIMEOUT = 10000; // ms
+const FINISH = "finish";
+
+/** @namespace */
+export const evaluate = {};
+
+/**
+ * Evaluate a script in given sandbox.
+ *
+ * The the provided `script` will be wrapped in an anonymous function
+ * with the `args` argument applied.
+ *
+ * The arguments provided by the `args<` argument are exposed
+ * through the `arguments` object available in the script context,
+ * and if the script is executed asynchronously with the `async`
+ * option, an additional last argument that is synonymous to the
+ * name `resolve` is appended, and can be accessed
+ * through `arguments[arguments.length - 1]`.
+ *
+ * The `timeout` option specifies the duration for how long the
+ * script should be allowed to run before it is interrupted and aborted.
+ * An interrupted script will cause a {@link ScriptTimeoutError} to occur.
+ *
+ * The `async` option indicates that the script will not return
+ * until the `resolve` callback is invoked,
+ * which is analogous to the last argument of the `arguments` object.
+ *
+ * The `file` option is used in error messages to provide information
+ * on the origin script file in the local end.
+ *
+ * The `line` option is used in error messages, along with `filename`,
+ * to provide the line number in the origin script file on the local end.
+ *
+ * @param {nsISandbox} sb
+ * Sandbox the script will be evaluted in.
+ * @param {string} script
+ * Script to evaluate.
+ * @param {Array.<?>=} args
+ * A sequence of arguments to call the script with.
+ * @param {boolean=} [async=false] async
+ * Indicates if the script should return immediately or wait for
+ * the callback to be invoked before returning.
+ * @param {string=} [file="dummy file"] file
+ * File location of the program in the client.
+ * @param {number=} [line=0] line
+ * Line number of th eprogram in the client.
+ * @param {number=} [timeout=DEFAULT_TIMEOUT] timeout
+ * Duration in milliseconds before interrupting the script.
+ *
+ * @return {Promise}
+ * A promise that when resolved will give you the return value from
+ * the script. Note that the return value requires serialisation before
+ * it can be sent to the client.
+ *
+ * @throws {JavaScriptError}
+ * If an {@link Error} was thrown whilst evaluating the script.
+ * @throws {ScriptTimeoutError}
+ * If the script was interrupted due to script timeout.
+ */
+evaluate.sandbox = function(
+ sb,
+ script,
+ args = [],
+ {
+ async = false,
+ file = "dummy file",
+ line = 0,
+ timeout = DEFAULT_TIMEOUT,
+ } = {}
+) {
+ let unloadHandler;
+ let marionetteSandbox = sandbox.create(sb.window);
+
+ // timeout handler
+ let scriptTimeoutID, timeoutPromise;
+ if (timeout !== null) {
+ timeoutPromise = new Promise((resolve, reject) => {
+ scriptTimeoutID = setTimeout(() => {
+ reject(
+ new lazy.error.ScriptTimeoutError(`Timed out after ${timeout} ms`)
+ );
+ }, timeout);
+ });
+ }
+
+ let promise = new Promise((resolve, reject) => {
+ let src = "";
+ sb[COMPLETE] = resolve;
+ sb[ARGUMENTS] = sandbox.cloneInto(args, sb);
+
+ // callback function made private
+ // so that introspection is possible
+ // on the arguments object
+ if (async) {
+ sb[CALLBACK] = sb[COMPLETE];
+ src += `${ARGUMENTS}.push(rv => ${CALLBACK}(rv));`;
+ }
+
+ src += `(function() {
+ ${script}
+ }).apply(null, ${ARGUMENTS})`;
+
+ unloadHandler = sandbox.cloneInto(
+ () => reject(new lazy.error.JavaScriptError("Document was unloaded")),
+ marionetteSandbox
+ );
+ marionetteSandbox.window.addEventListener("unload", unloadHandler);
+
+ let promises = [
+ Cu.evalInSandbox(
+ src,
+ sb,
+ "1.8",
+ file,
+ line,
+ /* enforceFilenameRestrictions */ false
+ ),
+ timeoutPromise,
+ ];
+
+ // Wait for the immediate result of calling evalInSandbox, or a timeout.
+ // Only resolve the promise if the scriptPromise was resolved and is not
+ // async, because the latter has to call resolve() itself.
+ Promise.race(promises).then(
+ value => {
+ if (!async) {
+ resolve(value);
+ }
+ },
+ err => {
+ reject(err);
+ }
+ );
+ });
+
+ // This block is mainly for async scripts, which escape the inner promise
+ // when calling resolve() on their own. The timeout promise will be re-used
+ // to break out after the initially setup timeout.
+ return Promise.race([promise, timeoutPromise])
+ .catch(err => {
+ // Only raise valid errors for both the sync and async scripts.
+ if (err instanceof lazy.error.ScriptTimeoutError) {
+ throw err;
+ }
+ throw new lazy.error.JavaScriptError(err);
+ })
+ .finally(() => {
+ clearTimeout(scriptTimeoutID);
+ marionetteSandbox.window.removeEventListener("unload", unloadHandler);
+ });
+};
+
+/**
+ * `Cu.isDeadWrapper` does not return true for a dead sandbox that
+ * was assosciated with and extension popup. This provides a way to
+ * still test for a dead object.
+ *
+ * @param {Object} obj
+ * A potentially dead object.
+ * @param {string} prop
+ * Name of a property on the object.
+ *
+ * @returns {boolean}
+ * True if <var>obj</var> is dead, false otherwise.
+ */
+evaluate.isDead = function(obj, prop) {
+ try {
+ obj[prop];
+ } catch (e) {
+ if (e.message.includes("dead object")) {
+ return true;
+ }
+ throw e;
+ }
+ return false;
+};
+
+export const sandbox = {};
+
+/**
+ * Provides a safe way to take an object defined in a privileged scope and
+ * create a structured clone of it in a less-privileged scope. It returns
+ * a reference to the clone.
+ *
+ * Unlike for {@link Components.utils.cloneInto}, `obj` may contain
+ * functions and DOM elements.
+ */
+sandbox.cloneInto = function(obj, sb) {
+ return Cu.cloneInto(obj, sb, { cloneFunctions: true, wrapReflectors: true });
+};
+
+/**
+ * Augment given sandbox by an adapter that has an `exports` map
+ * property, or a normal map, of function names and function references.
+ *
+ * @param {Sandbox} sb
+ * The sandbox to augment.
+ * @param {Object} adapter
+ * Object that holds an `exports` property, or a map, of function
+ * names and function references.
+ *
+ * @return {Sandbox}
+ * The augmented sandbox.
+ */
+sandbox.augment = function(sb, adapter) {
+ function* entries(obj) {
+ for (let key of Object.keys(obj)) {
+ yield [key, obj[key]];
+ }
+ }
+
+ let funcs = adapter.exports || entries(adapter);
+ for (let [name, func] of funcs) {
+ sb[name] = func;
+ }
+
+ return sb;
+};
+
+/**
+ * Creates a sandbox.
+ *
+ * @param {Window} win
+ * The DOM Window object.
+ * @param {nsIPrincipal=} principal
+ * An optional, custom principal to prefer over the Window. Useful if
+ * you need elevated security permissions.
+ *
+ * @return {Sandbox}
+ * The created sandbox.
+ */
+sandbox.create = function(win, principal = null, opts = {}) {
+ let p = principal || win;
+ opts = Object.assign(
+ {
+ sameZoneAs: win,
+ sandboxPrototype: win,
+ wantComponents: true,
+ wantXrays: true,
+ wantGlobalProperties: ["ChromeUtils"],
+ },
+ opts
+ );
+ return new Cu.Sandbox(p, opts);
+};
+
+/**
+ * Creates a mutable sandbox, where changes to the global scope
+ * will have lasting side-effects.
+ *
+ * @param {Window} win
+ * The DOM Window object.
+ *
+ * @return {Sandbox}
+ * The created sandbox.
+ */
+sandbox.createMutable = function(win) {
+ let opts = {
+ wantComponents: false,
+ wantXrays: false,
+ };
+ // Note: We waive Xrays here to match potentially-accidental old behavior.
+ return Cu.waiveXrays(sandbox.create(win, null, opts));
+};
+
+sandbox.createSystemPrincipal = function(win) {
+ let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+ );
+ return sandbox.create(win, principal);
+};
+
+sandbox.createSimpleTest = function(win, harness) {
+ let sb = sandbox.create(win);
+ sb = sandbox.augment(sb, harness);
+ sb[FINISH] = () => sb[COMPLETE](harness.generate_results());
+ return sb;
+};
+
+/**
+ * Sandbox storage. When the user requests a sandbox by a specific name,
+ * if one exists in the storage this will be used as long as its window
+ * reference is still valid.
+ *
+ * @memberof evaluate
+ */
+export class Sandboxes {
+ /**
+ * @param {function(): Window} windowFn
+ * A function that returns the references to the current Window
+ * object.
+ */
+ constructor(windowFn) {
+ this.windowFn_ = windowFn;
+ this.boxes_ = new Map();
+ }
+
+ get window_() {
+ return this.windowFn_();
+ }
+
+ /**
+ * Factory function for getting a sandbox by name, or failing that,
+ * creating a new one.
+ *
+ * If the sandbox' window does not match the provided window, a new one
+ * will be created.
+ *
+ * @param {string} name
+ * The name of the sandbox to get or create.
+ * @param {boolean=} [fresh=false] fresh
+ * Remove old sandbox by name first, if it exists.
+ *
+ * @return {Sandbox}
+ * A used or fresh sandbox.
+ */
+ get(name = "default", fresh = false) {
+ let sb = this.boxes_.get(name);
+ if (sb) {
+ if (fresh || evaluate.isDead(sb, "window") || sb.window != this.window_) {
+ this.boxes_.delete(name);
+ return this.get(name, false);
+ }
+ } else {
+ if (name == "system") {
+ sb = sandbox.createSystemPrincipal(this.window_);
+ } else {
+ sb = sandbox.create(this.window_);
+ }
+ this.boxes_.set(name, sb);
+ }
+ return sb;
+ }
+
+ /** Clears cache of sandboxes. */
+ clear() {
+ this.boxes_.clear();
+ }
+}