1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
13 const ARGUMENTS = "__webDriverArguments";
14 const CALLBACK = "__webDriverCallback";
15 const COMPLETE = "__webDriverComplete";
16 const DEFAULT_TIMEOUT = 10000; // ms
17 const FINISH = "finish";
20 export const evaluate = {};
23 * Evaluate a script in given sandbox.
25 * The the provided `script` will be wrapped in an anonymous function
26 * with the `args` argument applied.
28 * The arguments provided by the `args<` argument are exposed
29 * through the `arguments` object available in the script context,
30 * and if the script is executed asynchronously with the `async`
31 * option, an additional last argument that is synonymous to the
32 * name `resolve` is appended, and can be accessed
33 * through `arguments[arguments.length - 1]`.
35 * The `timeout` option specifies the duration for how long the
36 * script should be allowed to run before it is interrupted and aborted.
37 * An interrupted script will cause a {@link ScriptTimeoutError} to occur.
39 * The `async` option indicates that the script will not return
40 * until the `resolve` callback is invoked,
41 * which is analogous to the last argument of the `arguments` object.
43 * The `file` option is used in error messages to provide information
44 * on the origin script file in the local end.
46 * The `line` option is used in error messages, along with `filename`,
47 * to provide the line number in the origin script file on the local end.
49 * @param {nsISandbox} sb
50 * Sandbox the script will be evaluted in.
51 * @param {string} script
53 * @param {Array.<?>=} args
54 * A sequence of arguments to call the script with.
55 * @param {object=} options
56 * @param {boolean=} options.async
57 * Indicates if the script should return immediately or wait for
58 * the callback to be invoked before returning. Defaults to false.
59 * @param {string=} options.file
60 * File location of the program in the client. Defaults to "dummy file".
61 * @param {number=} options.line
62 * Line number of the program in the client. Defaults to 0.
63 * @param {number=} options.timeout
64 * Duration in milliseconds before interrupting the script. Defaults to
68 * A promise that when resolved will give you the return value from
69 * the script. Note that the return value requires serialisation before
70 * it can be sent to the client.
72 * @throws {JavaScriptError}
73 * If an {@link Error} was thrown whilst evaluating the script.
74 * @throws {ScriptTimeoutError}
75 * If the script was interrupted due to script timeout.
77 evaluate.sandbox = function (
85 timeout = DEFAULT_TIMEOUT,
89 let marionetteSandbox = sandbox.create(sb.window);
92 let scriptTimeoutID, timeoutPromise;
93 if (timeout !== null) {
94 timeoutPromise = new Promise((resolve, reject) => {
95 scriptTimeoutID = setTimeout(() => {
97 new lazy.error.ScriptTimeoutError(`Timed out after ${timeout} ms`)
103 let promise = new Promise((resolve, reject) => {
105 sb[COMPLETE] = resolve;
106 sb[ARGUMENTS] = sandbox.cloneInto(args, sb);
108 // callback function made private
109 // so that introspection is possible
110 // on the arguments object
112 sb[CALLBACK] = sb[COMPLETE];
113 src += `${ARGUMENTS}.push(rv => ${CALLBACK}(rv));`;
116 src += `(function() {
118 }).apply(null, ${ARGUMENTS})`;
120 unloadHandler = sandbox.cloneInto(
121 () => reject(new lazy.error.JavaScriptError("Document was unloaded")),
124 marionetteSandbox.window.addEventListener("unload", unloadHandler);
133 /* enforceFilenameRestrictions */ false
138 // Wait for the immediate result of calling evalInSandbox, or a timeout.
139 // Only resolve the promise if the scriptPromise was resolved and is not
140 // async, because the latter has to call resolve() itself.
141 Promise.race(promises).then(
153 // This block is mainly for async scripts, which escape the inner promise
154 // when calling resolve() on their own. The timeout promise will be re-used
155 // to break out after the initially setup timeout.
156 return Promise.race([promise, timeoutPromise])
158 // Only raise valid errors for both the sync and async scripts.
159 if (err instanceof lazy.error.ScriptTimeoutError) {
162 throw new lazy.error.JavaScriptError(err);
165 clearTimeout(scriptTimeoutID);
166 marionetteSandbox.window.removeEventListener("unload", unloadHandler);
171 * `Cu.isDeadWrapper` does not return true for a dead sandbox that
172 * was assosciated with and extension popup. This provides a way to
173 * still test for a dead object.
175 * @param {object} obj
176 * A potentially dead object.
177 * @param {string} prop
178 * Name of a property on the object.
181 * True if <var>obj</var> is dead, false otherwise.
183 evaluate.isDead = function (obj, prop) {
187 if (e.message.includes("dead object")) {
195 export const sandbox = {};
198 * Provides a safe way to take an object defined in a privileged scope and
199 * create a structured clone of it in a less-privileged scope. It returns
200 * a reference to the clone.
202 * Unlike for {@link Components.utils.cloneInto}, `obj` may contain
203 * functions and DOM elements.
205 sandbox.cloneInto = function (obj, sb) {
206 return Cu.cloneInto(obj, sb, { cloneFunctions: true, wrapReflectors: true });
210 * Augment given sandbox by an adapter that has an `exports` map
211 * property, or a normal map, of function names and function references.
213 * @param {Sandbox} sb
214 * The sandbox to augment.
215 * @param {object} adapter
216 * Object that holds an `exports` property, or a map, of function
217 * names and function references.
220 * The augmented sandbox.
222 sandbox.augment = function (sb, adapter) {
223 function* entries(obj) {
224 for (let key of Object.keys(obj)) {
225 yield [key, obj[key]];
229 let funcs = adapter.exports || entries(adapter);
230 for (let [name, func] of funcs) {
240 * @param {Window} win
241 * The DOM Window object.
242 * @param {nsIPrincipal=} principal
243 * An optional, custom principal to prefer over the Window. Useful if
244 * you need elevated security permissions.
247 * The created sandbox.
249 sandbox.create = function (win, principal = null, opts = {}) {
250 let p = principal || win;
251 opts = Object.assign(
254 sandboxPrototype: win,
255 wantComponents: true,
257 wantGlobalProperties: ["ChromeUtils"],
261 return new Cu.Sandbox(p, opts);
265 * Creates a mutable sandbox, where changes to the global scope
266 * will have lasting side-effects.
268 * @param {Window} win
269 * The DOM Window object.
272 * The created sandbox.
274 sandbox.createMutable = function (win) {
276 wantComponents: false,
279 // Note: We waive Xrays here to match potentially-accidental old behavior.
280 return Cu.waiveXrays(sandbox.create(win, null, opts));
283 sandbox.createSystemPrincipal = function (win) {
284 let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
287 return sandbox.create(win, principal);
290 sandbox.createSimpleTest = function (win, harness) {
291 let sb = sandbox.create(win);
292 sb = sandbox.augment(sb, harness);
293 sb[FINISH] = () => sb[COMPLETE](harness.generate_results());
298 * Sandbox storage. When the user requests a sandbox by a specific name,
299 * if one exists in the storage this will be used as long as its window
300 * reference is still valid.
304 export class Sandboxes {
306 * @param {function(): Window} windowFn
307 * A function that returns the references to the current Window
310 constructor(windowFn) {
311 this.windowFn_ = windowFn;
312 this.boxes_ = new Map();
316 return this.windowFn_();
320 * Factory function for getting a sandbox by name, or failing that,
321 * creating a new one.
323 * If the sandbox' window does not match the provided window, a new one
326 * @param {string} name
327 * The name of the sandbox to get or create.
328 * @param {boolean=} [fresh=false] fresh
329 * Remove old sandbox by name first, if it exists.
332 * A used or fresh sandbox.
334 get(name = "default", fresh = false) {
335 let sb = this.boxes_.get(name);
337 if (fresh || evaluate.isDead(sb, "window") || sb.window != this.window_) {
338 this.boxes_.delete(name);
339 return this.get(name, false);
342 if (name == "system") {
343 sb = sandbox.createSystemPrincipal(this.window_);
345 sb = sandbox.create(this.window_);
347 this.boxes_.set(name, sb);
352 /** Clears cache of sandboxes. */