evaluate.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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. "use strict";
  5. const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
  6. Cu.import("resource://gre/modules/Log.jsm");
  7. Cu.import("resource://gre/modules/NetUtil.jsm");
  8. Cu.import("resource://gre/modules/Timer.jsm");
  9. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  10. Cu.import("chrome://marionette/content/error.js");
  11. const logger = Log.repository.getLogger("Marionette");
  12. this.EXPORTED_SYMBOLS = ["evaluate", "sandbox", "Sandboxes"];
  13. const ARGUMENTS = "__webDriverArguments";
  14. const CALLBACK = "__webDriverCallback";
  15. const COMPLETE = "__webDriverComplete";
  16. const DEFAULT_TIMEOUT = 10000; // ms
  17. const FINISH = "finish";
  18. const MARIONETTE_SCRIPT_FINISHED = "marionetteScriptFinished";
  19. const ELEMENT_KEY = "element";
  20. const W3C_ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf";
  21. this.evaluate = {};
  22. /**
  23. * Evaluate a script in given sandbox.
  24. *
  25. * If the option {@code directInject} is not specified, the script will
  26. * be executed as a function with the {@code args} argument applied.
  27. *
  28. * The arguments provided by the {@code args} argument are exposed through
  29. * the {@code arguments} object available in the script context, and if
  30. * the script is executed asynchronously with the {@code async}
  31. * option, an additional last argument that is synonymous to the
  32. * {@code marionetteScriptFinished} global is appended, and can be
  33. * accessed through {@code arguments[arguments.length - 1]}.
  34. *
  35. * The {@code 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 ScriptTimeoutError to occur.
  38. *
  39. * The {@code async} option indicates that the script will not return
  40. * until the {@code marionetteScriptFinished} global callback is invoked,
  41. * which is analogous to the last argument of the {@code arguments}
  42. * object.
  43. *
  44. * The option {@code directInject} causes the script to be evaluated
  45. * without being wrapped in a function and the provided arguments will
  46. * be disregarded. This will cause such things as root scope return
  47. * statements to throw errors because they are not used inside a function.
  48. *
  49. * The {@code filename} option is used in error messages to provide
  50. * information on the origin script file in the local end.
  51. *
  52. * The {@code line} option is used in error messages, along with
  53. * {@code filename}, to provide the line number in the origin script
  54. * file on the local end.
  55. *
  56. * @param {nsISandbox) sb
  57. * The sandbox the script will be evaluted in.
  58. * @param {string} script
  59. * The script to evaluate.
  60. * @param {Array.<?>=} args
  61. * A sequence of arguments to call the script with.
  62. * @param {Object.<string, ?>=} opts
  63. * Dictionary of options:
  64. *
  65. * async (boolean)
  66. * Indicates if the script should return immediately or wait
  67. * for the callback be invoked before returning.
  68. * debug (boolean)
  69. * Attaches an {@code onerror} event listener.
  70. * directInject (boolean)
  71. * Evaluates the script without wrapping it in a function.
  72. * filename (string)
  73. * File location of the program in the client.
  74. * line (number)
  75. * Line number of the program in the client.
  76. * sandboxName (string)
  77. * Name of the sandbox. Elevated system privileges, equivalent
  78. * to chrome space, will be given if it is "system".
  79. * timeout (boolean)
  80. * Duration in milliseconds before interrupting the script.
  81. *
  82. * @return {Promise}
  83. * A promise that when resolved will give you the return value from
  84. * the script. Note that the return value requires serialisation before
  85. * it can be sent to the client.
  86. *
  87. * @throws JavaScriptError
  88. * If an Error was thrown whilst evaluating the script.
  89. * @throws ScriptTimeoutError
  90. * If the script was interrupted due to script timeout.
  91. */
  92. evaluate.sandbox = function (sb, script, args = [], opts = {}) {
  93. let scriptTimeoutID, timeoutHandler, unloadHandler;
  94. let promise = new Promise((resolve, reject) => {
  95. let src = "";
  96. sb[COMPLETE] = resolve;
  97. timeoutHandler = () => reject(new ScriptTimeoutError("Timed out"));
  98. unloadHandler = () => reject(
  99. new JavaScriptError("Document was unloaded during execution"));
  100. // wrap in function
  101. if (!opts.directInject) {
  102. if (opts.async) {
  103. sb[CALLBACK] = sb[COMPLETE];
  104. }
  105. sb[ARGUMENTS] = sandbox.cloneInto(args, sb);
  106. // callback function made private
  107. // so that introspection is possible
  108. // on the arguments object
  109. if (opts.async) {
  110. sb[CALLBACK] = sb[COMPLETE];
  111. src += `${ARGUMENTS}.push(rv => ${CALLBACK}(rv));`;
  112. }
  113. src += `(function() { ${script} }).apply(null, ${ARGUMENTS})`;
  114. // marionetteScriptFinished is not WebDriver conformant,
  115. // hence it is only exposed to immutable sandboxes
  116. if (opts.sandboxName) {
  117. sb[MARIONETTE_SCRIPT_FINISHED] = sb[CALLBACK];
  118. }
  119. }
  120. // onerror is not hooked on by default because of the inability to
  121. // differentiate content errors from chrome errors.
  122. //
  123. // see bug 1128760 for more details
  124. if (opts.debug) {
  125. sb.window.onerror = (msg, url, line) => {
  126. let err = new JavaScriptError(`${msg} at ${url}:${line}`);
  127. reject(err);
  128. };
  129. }
  130. // timeout and unload handlers
  131. scriptTimeoutID = setTimeout(timeoutHandler, opts.timeout || DEFAULT_TIMEOUT);
  132. sb.window.onunload = sandbox.cloneInto(unloadHandler, sb);
  133. let res;
  134. try {
  135. res = Cu.evalInSandbox(src, sb, "1.8", opts.filename || "dummy file", 0);
  136. } catch (e) {
  137. let err = new JavaScriptError(
  138. e,
  139. "execute_script",
  140. opts.filename,
  141. opts.line,
  142. script);
  143. reject(err);
  144. }
  145. if (!opts.async) {
  146. resolve(res);
  147. }
  148. });
  149. return promise.then(res => {
  150. clearTimeout(scriptTimeoutID);
  151. sb.window.removeEventListener("unload", unloadHandler);
  152. return res;
  153. });
  154. };
  155. this.sandbox = {};
  156. /**
  157. * Provides a safe way to take an object defined in a privileged scope and
  158. * create a structured clone of it in a less-privileged scope. It returns
  159. * a reference to the clone.
  160. *
  161. * Unlike for |Components.utils.cloneInto|, |obj| may contain functions
  162. * and DOM elemnets.
  163. */
  164. sandbox.cloneInto = function (obj, sb) {
  165. return Cu.cloneInto(obj, sb, {cloneFunctions: true, wrapReflectors: true});
  166. };
  167. /**
  168. * Augment given sandbox by an adapter that has an {@code exports}
  169. * map property, or a normal map, of function names and function
  170. * references.
  171. *
  172. * @param {Sandbox} sb
  173. * The sandbox to augment.
  174. * @param {Object} adapter
  175. * Object that holds an {@code exports} property, or a map, of
  176. * function names and function references.
  177. *
  178. * @return {Sandbox}
  179. * The augmented sandbox.
  180. */
  181. sandbox.augment = function (sb, adapter) {
  182. function* entries(obj) {
  183. for (let key of Object.keys(obj)) {
  184. yield [key, obj[key]];
  185. }
  186. }
  187. let funcs = adapter.exports || entries(adapter);
  188. for (let [name, func] of funcs) {
  189. sb[name] = func;
  190. }
  191. return sb;
  192. };
  193. /**
  194. * Creates a sandbox.
  195. *
  196. * @param {Window} window
  197. * The DOM Window object.
  198. * @param {nsIPrincipal=} principal
  199. * An optional, custom principal to prefer over the Window. Useful if
  200. * you need elevated security permissions.
  201. *
  202. * @return {Sandbox}
  203. * The created sandbox.
  204. */
  205. sandbox.create = function (window, principal = null, opts = {}) {
  206. let p = principal || window;
  207. opts = Object.assign({
  208. sameZoneAs: window,
  209. sandboxPrototype: window,
  210. wantComponents: true,
  211. wantXrays: true,
  212. }, opts);
  213. return new Cu.Sandbox(p, opts);
  214. };
  215. /**
  216. * Creates a mutable sandbox, where changes to the global scope
  217. * will have lasting side-effects.
  218. *
  219. * @param {Window} window
  220. * The DOM Window object.
  221. *
  222. * @return {Sandbox}
  223. * The created sandbox.
  224. */
  225. sandbox.createMutable = function (window) {
  226. let opts = {
  227. wantComponents: false,
  228. wantXrays: false,
  229. };
  230. return sandbox.create(window, null, opts);
  231. };
  232. sandbox.createSystemPrincipal = function (window) {
  233. let principal = Cc["@mozilla.org/systemprincipal;1"]
  234. .createInstance(Ci.nsIPrincipal);
  235. return sandbox.create(window, principal);
  236. };
  237. sandbox.createSimpleTest = function (window, harness) {
  238. let sb = sandbox.create(window);
  239. sb = sandbox.augment(sb, harness);
  240. sb[FINISH] = () => sb[COMPLETE](harness.generate_results());
  241. return sb;
  242. };
  243. /**
  244. * Sandbox storage. When the user requests a sandbox by a specific name,
  245. * if one exists in the storage this will be used as long as its window
  246. * reference is still valid.
  247. */
  248. this.Sandboxes = class {
  249. /**
  250. * @param {function(): Window} windowFn
  251. * A function that returns the references to the current Window
  252. * object.
  253. */
  254. constructor(windowFn) {
  255. this.windowFn_ = windowFn;
  256. this.boxes_ = new Map();
  257. }
  258. get window_() {
  259. return this.windowFn_();
  260. }
  261. /**
  262. * Factory function for getting a sandbox by name, or failing that,
  263. * creating a new one.
  264. *
  265. * If the sandbox' window does not match the provided window, a new one
  266. * will be created.
  267. *
  268. * @param {string} name
  269. * The name of the sandbox to get or create.
  270. * @param {boolean} fresh
  271. * Remove old sandbox by name first, if it exists.
  272. *
  273. * @return {Sandbox}
  274. * A used or fresh sandbox.
  275. */
  276. get(name = "default", fresh = false) {
  277. let sb = this.boxes_.get(name);
  278. if (sb) {
  279. if (fresh || sb.window != this.window_) {
  280. this.boxes_.delete(name);
  281. return this.get(name, false);
  282. }
  283. } else {
  284. if (name == "system") {
  285. sb = sandbox.createSystemPrincipal(this.window_);
  286. } else {
  287. sb = sandbox.create(this.window_);
  288. }
  289. this.boxes_.set(name, sb);
  290. }
  291. return sb;
  292. }
  293. /**
  294. * Clears cache of sandboxes.
  295. */
  296. clear() {
  297. this.boxes_.clear();
  298. }
  299. };
  300. /**
  301. * Stores scripts imported from the local end through the
  302. * {@code GeckoDriver#importScript} command.
  303. *
  304. * Imported scripts are prepended to the script that is evaluated
  305. * on each call to {@code GeckoDriver#executeScript},
  306. * {@code GeckoDriver#executeAsyncScript}, and
  307. * {@code GeckoDriver#executeJSScript}.
  308. *
  309. * Usage:
  310. *
  311. * let importedScripts = new evaluate.ScriptStorage();
  312. * importedScripts.add(firstScript);
  313. * importedScripts.add(secondScript);
  314. *
  315. * let scriptToEval = importedScripts.concat(script);
  316. * // firstScript and secondScript are prepended to script
  317. *
  318. */
  319. evaluate.ScriptStorage = class extends Set {
  320. /**
  321. * Produce a string of all stored scripts.
  322. *
  323. * The stored scripts are concatenated into a string, with optional
  324. * additional scripts then appended.
  325. *
  326. * @param {...string} addional
  327. * Optional scripts to include.
  328. *
  329. * @return {string}
  330. * Concatenated string consisting of stored scripts and additional
  331. * scripts, in that order.
  332. */
  333. concat(...additional) {
  334. let rv = "";
  335. for (let s of this) {
  336. rv = s + rv;
  337. }
  338. for (let s of additional) {
  339. rv = rv + s;
  340. }
  341. return rv;
  342. }
  343. toJson() {
  344. return Array.from(this);
  345. }
  346. };
  347. /**
  348. * Service that enables the script storage service to be queried from
  349. * content space.
  350. *
  351. * The storage can back multiple |ScriptStorage|, each typically belonging
  352. * to a |Context|. Since imported scripts' scope are global and not
  353. * scoped to the current browsing context, all imported scripts are stored
  354. * in chrome space and fetched by content space as needed.
  355. *
  356. * Usage in chrome space:
  357. *
  358. * let service = new evaluate.ScriptStorageService(
  359. * [Context.CHROME, Context.CONTENT]);
  360. * let storage = service.for(Context.CHROME);
  361. * let scriptToEval = storage.concat(script);
  362. *
  363. */
  364. evaluate.ScriptStorageService = class extends Map {
  365. /**
  366. * Create the service.
  367. *
  368. * An optional array of names for script storages to initially create
  369. * can be provided.
  370. *
  371. * @param {Array.<string>=} initialStorages
  372. * List of names of the script storages to create initially.
  373. */
  374. constructor(initialStorages = []) {
  375. super(initialStorages.map(name => [name, new evaluate.ScriptStorage()]));
  376. }
  377. /**
  378. * Retrieve the scripts associated with the given context.
  379. *
  380. * @param {Context} context
  381. * Context to retrieve the scripts from.
  382. *
  383. * @return {ScriptStorage}
  384. * Scrips associated with given |context|.
  385. */
  386. for(context) {
  387. return this.get(context);
  388. }
  389. processMessage(msg) {
  390. switch (msg.name) {
  391. case "Marionette:getImportedScripts":
  392. let storage = this.for.apply(this, msg.json);
  393. return storage.toJson();
  394. default:
  395. throw new TypeError("Unknown message: " + msg.name);
  396. }
  397. }
  398. // TODO(ato): The idea of services in chrome space
  399. // can be generalised at some later time (see cookies.js:38).
  400. receiveMessage(msg) {
  401. try {
  402. return this.processMessage(msg);
  403. } catch (e) {
  404. logger.error(e);
  405. }
  406. }
  407. };
  408. evaluate.ScriptStorageService.prototype.QueryInterface =
  409. XPCOMUtils.generateQI([
  410. Ci.nsIMessageListener,
  411. Ci.nsISupportsWeakReference,
  412. ]);
  413. /**
  414. * Bridges the script storage in chrome space, to make it possible to
  415. * retrieve a {@code ScriptStorage} associated with a given
  416. * {@code Context} from content space.
  417. *
  418. * Usage in content space:
  419. *
  420. * let client = new evaluate.ScriptStorageServiceClient(chromeProxy);
  421. * let storage = client.for(Context.CONTENT);
  422. * let scriptToEval = storage.concat(script);
  423. *
  424. */
  425. evaluate.ScriptStorageServiceClient = class {
  426. /**
  427. * @param {proxy.SyncChromeSender} chromeProxy
  428. * Proxy for communicating with chrome space.
  429. */
  430. constructor(chromeProxy) {
  431. this.chrome = chromeProxy;
  432. }
  433. /**
  434. * Retrieve scripts associated with the given context.
  435. *
  436. * @param {Context} context
  437. * Context to retrieve scripts from.
  438. *
  439. * @return {ScriptStorage}
  440. * Scripts associated with given |context|.
  441. */
  442. for(context) {
  443. let scripts = this.chrome.getImportedScripts(context)[0];
  444. return new evaluate.ScriptStorage(scripts);
  445. }
  446. };