evaluate.sys.mjs (10142B)
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/. */ 4 5 import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 11 }); 12 13 const ARGUMENTS = "__webDriverArguments"; 14 const CALLBACK = "__webDriverCallback"; 15 const COMPLETE = "__webDriverComplete"; 16 const DEFAULT_TIMEOUT = 10000; // ms 17 const FINISH = "finish"; 18 19 /** @namespace */ 20 export const evaluate = {}; 21 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 evaluated 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); 90 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 } 102 103 let promise = new Promise((resolve, reject) => { 104 let src = ""; 105 sb[COMPLETE] = resolve; 106 sb[ARGUMENTS] = sandbox.cloneInto(args, sb); 107 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 } 115 116 src += `(function() { 117 ${script} 118 }).apply(null, ${ARGUMENTS})`; 119 120 unloadHandler = sandbox.cloneInto( 121 () => reject(new lazy.error.JavaScriptError("Document was unloaded")), 122 marionetteSandbox 123 ); 124 marionetteSandbox.window.addEventListener("unload", unloadHandler); 125 126 let promises = [ 127 Cu.evalInSandbox( 128 src, 129 sb, 130 "1.8", 131 file, 132 line, 133 /* enforceFilenameRestrictions */ false 134 ), 135 timeoutPromise, 136 ]; 137 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 }); 152 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 reused 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 }); 168 }; 169 170 /** 171 * `Cu.isDeadWrapper` does not return true for a dead sandbox that 172 * was associated with and extension popup. This provides a way to 173 * still test for a dead object. 174 * 175 * @param {object} obj 176 * A potentially dead object. 177 * @param {string} prop 178 * Name of a property on the object. 179 * 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; 193 }; 194 195 export const sandbox = {}; 196 197 /** 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. 201 * 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 }); 207 }; 208 209 /** 210 * Augment given sandbox by an adapter that has an `exports` map 211 * property, or a normal map, of function names and function references. 212 * 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. 218 * 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 } 228 229 let funcs = adapter.exports || entries(adapter); 230 for (let [name, func] of funcs) { 231 sb[name] = func; 232 } 233 234 return sb; 235 }; 236 237 /** 238 * Creates a sandbox. 239 * 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. 245 * 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); 262 }; 263 264 /** 265 * Creates a mutable sandbox, where changes to the global scope 266 * will have lasting side-effects. 267 * 268 * @param {Window} win 269 * The DOM Window object. 270 * 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)); 281 }; 282 283 sandbox.createSystemPrincipal = function (win) { 284 let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance( 285 Ci.nsIPrincipal 286 ); 287 return sandbox.create(win, principal); 288 }; 289 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; 295 }; 296 297 /** 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. 301 * 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 } 314 315 get window_() { 316 return this.windowFn_(); 317 } 318 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 } 351 352 /** Clears cache of sandboxes. */ 353 clear() { 354 this.boxes_.clear(); 355 } 356 }