diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /testing/marionette/evaluate.js | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/marionette/evaluate.js')
-rw-r--r-- | testing/marionette/evaluate.js | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/testing/marionette/evaluate.js b/testing/marionette/evaluate.js new file mode 100644 index 000000000..38a80eb39 --- /dev/null +++ b/testing/marionette/evaluate.js @@ -0,0 +1,494 @@ +/* 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/. */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +Cu.import("chrome://marionette/content/error.js"); + +const logger = Log.repository.getLogger("Marionette"); + +this.EXPORTED_SYMBOLS = ["evaluate", "sandbox", "Sandboxes"]; + +const ARGUMENTS = "__webDriverArguments"; +const CALLBACK = "__webDriverCallback"; +const COMPLETE = "__webDriverComplete"; +const DEFAULT_TIMEOUT = 10000; // ms +const FINISH = "finish"; +const MARIONETTE_SCRIPT_FINISHED = "marionetteScriptFinished"; +const ELEMENT_KEY = "element"; +const W3C_ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf"; + +this.evaluate = {}; + +/** + * Evaluate a script in given sandbox. + * + * If the option {@code directInject} is not specified, the script will + * be executed as a function with the {@code args} argument applied. + * + * The arguments provided by the {@code args} argument are exposed through + * the {@code arguments} object available in the script context, and if + * the script is executed asynchronously with the {@code async} + * option, an additional last argument that is synonymous to the + * {@code marionetteScriptFinished} global is appended, and can be + * accessed through {@code arguments[arguments.length - 1]}. + * + * The {@code 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 ScriptTimeoutError to occur. + * + * The {@code async} option indicates that the script will not return + * until the {@code marionetteScriptFinished} global callback is invoked, + * which is analogous to the last argument of the {@code arguments} + * object. + * + * The option {@code directInject} causes the script to be evaluated + * without being wrapped in a function and the provided arguments will + * be disregarded. This will cause such things as root scope return + * statements to throw errors because they are not used inside a function. + * + * The {@code filename} option is used in error messages to provide + * information on the origin script file in the local end. + * + * The {@code line} option is used in error messages, along with + * {@code filename}, to provide the line number in the origin script + * file on the local end. + * + * @param {nsISandbox) sb + * The sandbox the script will be evaluted in. + * @param {string} script + * The script to evaluate. + * @param {Array.<?>=} args + * A sequence of arguments to call the script with. + * @param {Object.<string, ?>=} opts + * Dictionary of options: + * + * async (boolean) + * Indicates if the script should return immediately or wait + * for the callback be invoked before returning. + * debug (boolean) + * Attaches an {@code onerror} event listener. + * directInject (boolean) + * Evaluates the script without wrapping it in a function. + * filename (string) + * File location of the program in the client. + * line (number) + * Line number of the program in the client. + * sandboxName (string) + * Name of the sandbox. Elevated system privileges, equivalent + * to chrome space, will be given if it is "system". + * timeout (boolean) + * 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 Error was thrown whilst evaluating the script. + * @throws ScriptTimeoutError + * If the script was interrupted due to script timeout. + */ +evaluate.sandbox = function (sb, script, args = [], opts = {}) { + let scriptTimeoutID, timeoutHandler, unloadHandler; + + let promise = new Promise((resolve, reject) => { + let src = ""; + sb[COMPLETE] = resolve; + timeoutHandler = () => reject(new ScriptTimeoutError("Timed out")); + unloadHandler = () => reject( + new JavaScriptError("Document was unloaded during execution")); + + // wrap in function + if (!opts.directInject) { + if (opts.async) { + sb[CALLBACK] = sb[COMPLETE]; + } + sb[ARGUMENTS] = sandbox.cloneInto(args, sb); + + // callback function made private + // so that introspection is possible + // on the arguments object + if (opts.async) { + sb[CALLBACK] = sb[COMPLETE]; + src += `${ARGUMENTS}.push(rv => ${CALLBACK}(rv));`; + } + + src += `(function() { ${script} }).apply(null, ${ARGUMENTS})`; + + // marionetteScriptFinished is not WebDriver conformant, + // hence it is only exposed to immutable sandboxes + if (opts.sandboxName) { + sb[MARIONETTE_SCRIPT_FINISHED] = sb[CALLBACK]; + } + } + + // onerror is not hooked on by default because of the inability to + // differentiate content errors from chrome errors. + // + // see bug 1128760 for more details + if (opts.debug) { + sb.window.onerror = (msg, url, line) => { + let err = new JavaScriptError(`${msg} at ${url}:${line}`); + reject(err); + }; + } + + // timeout and unload handlers + scriptTimeoutID = setTimeout(timeoutHandler, opts.timeout || DEFAULT_TIMEOUT); + sb.window.onunload = sandbox.cloneInto(unloadHandler, sb); + + let res; + try { + res = Cu.evalInSandbox(src, sb, "1.8", opts.filename || "dummy file", 0); + } catch (e) { + let err = new JavaScriptError( + e, + "execute_script", + opts.filename, + opts.line, + script); + reject(err); + } + + if (!opts.async) { + resolve(res); + } + }); + + return promise.then(res => { + clearTimeout(scriptTimeoutID); + sb.window.removeEventListener("unload", unloadHandler); + return res; + }); +}; + +this.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 |Components.utils.cloneInto|, |obj| may contain functions + * and DOM elemnets. + */ +sandbox.cloneInto = function (obj, sb) { + return Cu.cloneInto(obj, sb, {cloneFunctions: true, wrapReflectors: true}); +}; + +/** + * Augment given sandbox by an adapter that has an {@code 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 {@code 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} window + * 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 (window, principal = null, opts = {}) { + let p = principal || window; + opts = Object.assign({ + sameZoneAs: window, + sandboxPrototype: window, + wantComponents: true, + wantXrays: true, + }, opts); + return new Cu.Sandbox(p, opts); +}; + +/** + * Creates a mutable sandbox, where changes to the global scope + * will have lasting side-effects. + * + * @param {Window} window + * The DOM Window object. + * + * @return {Sandbox} + * The created sandbox. + */ +sandbox.createMutable = function (window) { + let opts = { + wantComponents: false, + wantXrays: false, + }; + return sandbox.create(window, null, opts); +}; + +sandbox.createSystemPrincipal = function (window) { + let principal = Cc["@mozilla.org/systemprincipal;1"] + .createInstance(Ci.nsIPrincipal); + return sandbox.create(window, principal); +}; + +sandbox.createSimpleTest = function (window, harness) { + let sb = sandbox.create(window); + 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. + */ +this.Sandboxes = class { + /** + * @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 + * 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 || 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(); + } +}; + +/** + * Stores scripts imported from the local end through the + * {@code GeckoDriver#importScript} command. + * + * Imported scripts are prepended to the script that is evaluated + * on each call to {@code GeckoDriver#executeScript}, + * {@code GeckoDriver#executeAsyncScript}, and + * {@code GeckoDriver#executeJSScript}. + * + * Usage: + * + * let importedScripts = new evaluate.ScriptStorage(); + * importedScripts.add(firstScript); + * importedScripts.add(secondScript); + * + * let scriptToEval = importedScripts.concat(script); + * // firstScript and secondScript are prepended to script + * + */ +evaluate.ScriptStorage = class extends Set { + + /** + * Produce a string of all stored scripts. + * + * The stored scripts are concatenated into a string, with optional + * additional scripts then appended. + * + * @param {...string} addional + * Optional scripts to include. + * + * @return {string} + * Concatenated string consisting of stored scripts and additional + * scripts, in that order. + */ + concat(...additional) { + let rv = ""; + for (let s of this) { + rv = s + rv; + } + for (let s of additional) { + rv = rv + s; + } + return rv; + } + + toJson() { + return Array.from(this); + } + +}; + +/** + * Service that enables the script storage service to be queried from + * content space. + * + * The storage can back multiple |ScriptStorage|, each typically belonging + * to a |Context|. Since imported scripts' scope are global and not + * scoped to the current browsing context, all imported scripts are stored + * in chrome space and fetched by content space as needed. + * + * Usage in chrome space: + * + * let service = new evaluate.ScriptStorageService( + * [Context.CHROME, Context.CONTENT]); + * let storage = service.for(Context.CHROME); + * let scriptToEval = storage.concat(script); + * + */ +evaluate.ScriptStorageService = class extends Map { + + /** + * Create the service. + * + * An optional array of names for script storages to initially create + * can be provided. + * + * @param {Array.<string>=} initialStorages + * List of names of the script storages to create initially. + */ + constructor(initialStorages = []) { + super(initialStorages.map(name => [name, new evaluate.ScriptStorage()])); + } + + /** + * Retrieve the scripts associated with the given context. + * + * @param {Context} context + * Context to retrieve the scripts from. + * + * @return {ScriptStorage} + * Scrips associated with given |context|. + */ + for(context) { + return this.get(context); + } + + processMessage(msg) { + switch (msg.name) { + case "Marionette:getImportedScripts": + let storage = this.for.apply(this, msg.json); + return storage.toJson(); + + default: + throw new TypeError("Unknown message: " + msg.name); + } + } + + // TODO(ato): The idea of services in chrome space + // can be generalised at some later time (see cookies.js:38). + receiveMessage(msg) { + try { + return this.processMessage(msg); + } catch (e) { + logger.error(e); + } + } + +}; + +evaluate.ScriptStorageService.prototype.QueryInterface = + XPCOMUtils.generateQI([ + Ci.nsIMessageListener, + Ci.nsISupportsWeakReference, + ]); + +/** + * Bridges the script storage in chrome space, to make it possible to + * retrieve a {@code ScriptStorage} associated with a given + * {@code Context} from content space. + * + * Usage in content space: + * + * let client = new evaluate.ScriptStorageServiceClient(chromeProxy); + * let storage = client.for(Context.CONTENT); + * let scriptToEval = storage.concat(script); + * + */ +evaluate.ScriptStorageServiceClient = class { + + /** + * @param {proxy.SyncChromeSender} chromeProxy + * Proxy for communicating with chrome space. + */ + constructor(chromeProxy) { + this.chrome = chromeProxy; + } + + /** + * Retrieve scripts associated with the given context. + * + * @param {Context} context + * Context to retrieve scripts from. + * + * @return {ScriptStorage} + * Scripts associated with given |context|. + */ + for(context) { + let scripts = this.chrome.getImportedScripts(context)[0]; + return new evaluate.ScriptStorage(scripts); + } + +}; |