no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / remote / marionette / evaluate.sys.mjs
blobbdcce779a1f568858a19e294b3f7c316e4beb375
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";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
11 });
13 const ARGUMENTS = "__webDriverArguments";
14 const CALLBACK = "__webDriverCallback";
15 const COMPLETE = "__webDriverComplete";
16 const DEFAULT_TIMEOUT = 10000; // ms
17 const FINISH = "finish";
19 /** @namespace */
20 export const evaluate = {};
22 /**
23  * Evaluate a script in given sandbox.
24  *
25  * The the provided `script` will be wrapped in an anonymous function
26  * with the `args` argument applied.
27  *
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]`.
34  *
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.
38  *
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.
42  *
43  * The `file` option is used in error messages to provide information
44  * on the origin script file in the local end.
45  *
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.
48  *
49  * @param {nsISandbox} sb
50  *     Sandbox the script will be evaluted in.
51  * @param {string} script
52  *     Script to evaluate.
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
65  *     DEFAULT_TIMEOUT.
66  *
67  * @returns {Promise}
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.
71  *
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.
76  */
77 evaluate.sandbox = function (
78   sb,
79   script,
80   args = [],
81   {
82     async = false,
83     file = "dummy file",
84     line = 0,
85     timeout = DEFAULT_TIMEOUT,
86   } = {}
87 ) {
88   let unloadHandler;
89   let marionetteSandbox = sandbox.create(sb.window);
91   // timeout handler
92   let scriptTimeoutID, timeoutPromise;
93   if (timeout !== null) {
94     timeoutPromise = new Promise((resolve, reject) => {
95       scriptTimeoutID = setTimeout(() => {
96         reject(
97           new lazy.error.ScriptTimeoutError(`Timed out after ${timeout} ms`)
98         );
99       }, timeout);
100     });
101   }
103   let promise = new Promise((resolve, reject) => {
104     let src = "";
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
111     if (async) {
112       sb[CALLBACK] = sb[COMPLETE];
113       src += `${ARGUMENTS}.push(rv => ${CALLBACK}(rv));`;
114     }
116     src += `(function() {
117       ${script}
118     }).apply(null, ${ARGUMENTS})`;
120     unloadHandler = sandbox.cloneInto(
121       () => reject(new lazy.error.JavaScriptError("Document was unloaded")),
122       marionetteSandbox
123     );
124     marionetteSandbox.window.addEventListener("unload", unloadHandler);
126     let promises = [
127       Cu.evalInSandbox(
128         src,
129         sb,
130         "1.8",
131         file,
132         line,
133         /* enforceFilenameRestrictions */ false
134       ),
135       timeoutPromise,
136     ];
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(
142       value => {
143         if (!async) {
144           resolve(value);
145         }
146       },
147       err => {
148         reject(err);
149       }
150     );
151   });
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])
157     .catch(err => {
158       // Only raise valid errors for both the sync and async scripts.
159       if (err instanceof lazy.error.ScriptTimeoutError) {
160         throw err;
161       }
162       throw new lazy.error.JavaScriptError(err);
163     })
164     .finally(() => {
165       clearTimeout(scriptTimeoutID);
166       marionetteSandbox.window.removeEventListener("unload", unloadHandler);
167     });
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.
180  * @returns {boolean}
181  *     True if <var>obj</var> is dead, false otherwise.
182  */
183 evaluate.isDead = function (obj, prop) {
184   try {
185     obj[prop];
186   } catch (e) {
187     if (e.message.includes("dead object")) {
188       return true;
189     }
190     throw e;
191   }
192   return false;
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.
204  */
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.
219  * @returns {Sandbox}
220  *     The augmented sandbox.
221  */
222 sandbox.augment = function (sb, adapter) {
223   function* entries(obj) {
224     for (let key of Object.keys(obj)) {
225       yield [key, obj[key]];
226     }
227   }
229   let funcs = adapter.exports || entries(adapter);
230   for (let [name, func] of funcs) {
231     sb[name] = func;
232   }
234   return sb;
238  * Creates a sandbox.
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.
246  * @returns {Sandbox}
247  *     The created sandbox.
248  */
249 sandbox.create = function (win, principal = null, opts = {}) {
250   let p = principal || win;
251   opts = Object.assign(
252     {
253       sameZoneAs: win,
254       sandboxPrototype: win,
255       wantComponents: true,
256       wantXrays: true,
257       wantGlobalProperties: ["ChromeUtils"],
258     },
259     opts
260   );
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.
271  * @returns {Sandbox}
272  *     The created sandbox.
273  */
274 sandbox.createMutable = function (win) {
275   let opts = {
276     wantComponents: false,
277     wantXrays: false,
278   };
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(
285     Ci.nsIPrincipal
286   );
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());
294   return sb;
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.
302  * @memberof evaluate
303  */
304 export class Sandboxes {
305   /**
306    * @param {function(): Window} windowFn
307    *     A function that returns the references to the current Window
308    *     object.
309    */
310   constructor(windowFn) {
311     this.windowFn_ = windowFn;
312     this.boxes_ = new Map();
313   }
315   get window_() {
316     return this.windowFn_();
317   }
319   /**
320    * Factory function for getting a sandbox by name, or failing that,
321    * creating a new one.
322    *
323    * If the sandbox' window does not match the provided window, a new one
324    * will be created.
325    *
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.
330    *
331    * @returns {Sandbox}
332    *     A used or fresh sandbox.
333    */
334   get(name = "default", fresh = false) {
335     let sb = this.boxes_.get(name);
336     if (sb) {
337       if (fresh || evaluate.isDead(sb, "window") || sb.window != this.window_) {
338         this.boxes_.delete(name);
339         return this.get(name, false);
340       }
341     } else {
342       if (name == "system") {
343         sb = sandbox.createSystemPrincipal(this.window_);
344       } else {
345         sb = sandbox.create(this.window_);
346       }
347       this.boxes_.set(name, sb);
348     }
349     return sb;
350   }
352   /** Clears cache of sandboxes. */
353   clear() {
354     this.boxes_.clear();
355   }