Bug 1223907: Refactor cookies in Marionette
Moves most of the cookie implementation to a new file,
testing/marionette/cookies.js. The new Cookies class encapsulates all
APIs for manipulating and querying cookies from content space.
It communicates with chrome space, where the cookie manager lives, through
a new SyncContentSender provided by testing/marionette/proxy.js. This new
interface provides synchronous and transparent communication from content
to chrome, not dissimilar from how the original listener proxy works.
r=dburns
new file mode 100644
--- /dev/null
+++ b/testing/marionette/cookies.js
@@ -0,0 +1,131 @@
+/* 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("chrome://marionette/content/error.js");
+
+const logger = Log.repository.getLogger("Marionette");
+
+this.EXPORTED_SYMBOLS = ["Cookies"];
+
+const IPV4_PORT_EXPR = /:\d+$/;
+
+/**
+ * Interface for manipulating cookies from content space.
+ */
+this.Cookies = class {
+
+ /**
+ * @param {function(): Document} documentFn
+ * Closure that returns the current content document.
+ * @param {Proxy(SyncChromeSender)} chromeProxy
+ * A synchronous proxy interface to chrome space.
+ */
+ constructor(documentFn, chromeProxy) {
+ this.documentFn_ = documentFn;
+ this.chrome = chromeProxy;
+ }
+
+ get document() {
+ return this.documentFn_();
+ }
+
+ [Symbol.iterator]() {
+ let path = this.document.location.pathname || "/";
+ let cs = this.chrome.getVisibleCookies(path, this.document.location.hostname)[0];
+ return cs[Symbol.iterator]();
+ }
+
+ /**
+ * Add a new cookie to a content document.
+ *
+ * @param {string} name
+ * Cookie key.
+ * @param {string} value
+ * Cookie value.
+ * @param {Object.<string, ?>} opts
+ * An object with the optional fields {@code domain}, {@code path},
+ * {@code secure}, {@code httpOnly}, and {@code expiry}.
+ *
+ * @return {Object.<string, ?>}
+ * A serialisation of the cookie that was added.
+ *
+ * @throws UnableToSetCookieError
+ * If the document's content type isn't HTML, the current document's
+ * domain is a mismatch to the cookie's provided domain, or there
+ * otherwise was issues with the input data.
+ */
+ add(name, value, opts={}) {
+ if (typeof this.document == "undefined" || !this.document.contentType.match(/html/i)) {
+ throw new UnableToSetCookieError(
+ "You may only set cookies on HTML documents: " + this.document.contentType);
+ }
+
+ if (!opts.expiry) {
+ // date twenty years into future, in seconds
+ let date = new Date();
+ let now = new Date(Date.now());
+ date.setYear(now.getFullYear() + 20);
+ opts.expiry = date.getTime() / 1000;
+ }
+
+ if (!opts.domain) {
+ opts.domain = this.document.location.host;
+ } else if (this.document.location.host.indexOf(opts.domain) < 0) {
+ throw new InvalidCookieDomainError(
+ "You may only set cookies for the current domain");
+ }
+
+ // remove port from domain, if present.
+ // unfortunately this catches IPv6 addresses by mistake
+ // TODO: Bug 814416
+ opts.domain = opts.domain.replace(IPV4_PORT_EXPR, "");
+
+ let cookie = {
+ domain: opts.domain,
+ path: opts.path,
+ name: name,
+ value: value,
+ secure: opts.secure,
+ httpOnly: opts.httpOnly,
+ session: false,
+ expiry: opts.expiry,
+ };
+ if (!this.chrome.addCookie(cookie)) {
+ throw new UnableToSetCookieError();
+ }
+
+ return cookie;
+ }
+
+ /**
+ * Delete cookie by reference or by name.
+ *
+ * @param {(string|Object.<string, ?>)} cookie
+ * Name of cookie or cookie object.
+ *
+ * @throws {UnknownError}
+ * If unable to delete the cookie.
+ */
+ delete(cookie) {
+ let name;
+ if (cookie.hasOwnProperty("name")) {
+ name = cookie.name;
+ } else {
+ name = cookie;
+ }
+
+ for (let candidate of this) {
+ if (candidate.name == name) {
+ if (!this.chrome.deleteCookie(candidate)) {
+ throw new UnknownError("Unable to delete cookie by name: " + name);
+ }
+ }
+ }
+ }
+};
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -2286,37 +2286,74 @@ GeckoDriver.prototype.clearElement = fun
GeckoDriver.prototype.switchToShadowRoot = function(cmd, resp) {
let id;
if (cmd.parameters) { id = cmd.parameters.id; }
yield this.listener.switchToShadowRoot(id);
};
/** Add a cookie to the document. */
GeckoDriver.prototype.addCookie = function(cmd, resp) {
- yield this.listener.addCookie({cookie: cmd.parameters.cookie});
+ let cb = msg => {
+ this.mm.removeMessageListener("Marionette:addCookie", cb);
+ let cookie = msg.json;
+ Services.cookies.add(
+ cookie.domain,
+ cookie.path,
+ cookie.name,
+ cookie.value,
+ cookie.secure,
+ cookie.httpOnly,
+ cookie.session,
+ cookie.expiry);
+ return true;
+ };
+ this.mm.addMessageListener("Marionette:addCookie", cb);
+ yield this.listener.addCookie(cmd.parameters.cookie);
};
/**
* Get all the cookies for the current domain.
*
* This is the equivalent of calling {@code document.cookie} and parsing
* the result.
*/
GeckoDriver.prototype.getCookies = function(cmd, resp) {
resp.body = yield this.listener.getCookies();
};
/** Delete all cookies that are visible to a document. */
GeckoDriver.prototype.deleteAllCookies = function(cmd, resp) {
+ let cb = msg => {
+ let cookie = msg.json;
+ cookieManager.remove(
+ cookie.host,
+ cookie.name,
+ cookie.path,
+ false);
+ return true;
+ };
+ this.mm.addMessageListener("Marionette:deleteCookie", cb);
yield this.listener.deleteAllCookies();
+ this.mm.removeMessageListener("Marionette:deleteCookie", cb);
};
/** Delete a cookie by name. */
GeckoDriver.prototype.deleteCookie = function(cmd, resp) {
- yield this.listener.deleteCookie({name: cmd.parameters.name});
+ let cb = msg => {
+ this.mm.removeMessageListener("Marionette:deleteCookie", cb);
+ let cookie = msg.json;
+ cookieManager.remove(
+ cookie.host,
+ cookie.name,
+ cookie.path,
+ false);
+ return true;
+ };
+ this.mm.addMessageListener("Marionette:deleteCookie", cb);
+ yield this.listener.deleteCookie(cmd.parameters.name);
};
/**
* Close the current window, ending the session if it's the last
* window currently open.
*
* On B2G this method is a noop and will return immediately.
*/
@@ -2816,17 +2853,17 @@ GeckoDriver.prototype.receiveMessage = f
if (message.json.storePrevious) {
this.previousFrameElement = this.currentFrameElement;
}
this.currentFrameElement = message.json.frameValue;
}
break;
case "Marionette:getVisibleCookies":
- let [currentPath, host] = message.json.value;
+ let [currentPath, host] = message.json;
let isForCurrentPath = path => currentPath.indexOf(path) != -1;
let results = [];
let en = cookieManager.getCookiesFromHost(host);
while (en.hasMoreElements()) {
let cookie = en.getNext().QueryInterface(Ci.nsICookie2);
// take the hostname and progressively shorten
let hostname = host;
@@ -2844,38 +2881,16 @@ GeckoDriver.prototype.receiveMessage = f
});
break;
}
hostname = hostname.replace(/^.*?\./, "");
} while (hostname.indexOf(".") != -1);
}
return results;
- case "Marionette:addCookie":
- let cookieToAdd = message.json.value;
- Services.cookies.add(
- cookieToAdd.domain,
- cookieToAdd.path,
- cookieToAdd.name,
- cookieToAdd.value,
- cookieToAdd.secure,
- cookieToAdd.httpOnly,
- false,
- cookieToAdd.expiry);
- return true;
-
- case "Marionette:deleteCookie":
- let cookieToDelete = message.json.value;
- cookieManager.remove(
- cookieToDelete.host,
- cookieToDelete.name,
- cookieToDelete.path,
- false);
- return true;
-
case "Marionette:getFiles":
// Generates file objects to send back to the content script
// for handling file uploads.
let val = message.json.value;
let command_id = message.json.command_id;
Cu.importGlobalProperties(["File"]);
try {
let file = new File(val);
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -26,58 +26,33 @@ const errors = [
"UnknownCommandError",
"UnknownError",
"UnsupportedOperationError",
"WebDriverError",
];
this.EXPORTED_SYMBOLS = ["error"].concat(errors);
-// Because XPCOM is a cesspool of undocumented odd behaviour,
-// Object.getPrototypeOf(err) causes another exception if err is an XPCOM
-// exception, and cannot be used to determine if err is a prototypal Error.
-//
-// Consequently we need to check for properties in its prototypal chain
-// (using in, instead of err.hasOwnProperty because that causes other
-// issues).
-//
-// Since the input is arbitrary it might _not_ be an Error, and can as
-// such be an object with a "result" property without it being considered to
-// be an exception. The solution is to build a lookup table of XPCOM
-// exceptions from Components.results and check if the value of err#results
-// is in that table.
-const XPCOM_EXCEPTIONS = [];
-{
- for (let prop in Cr) {
- XPCOM_EXCEPTIONS.push(Cr[prop]);
- }
-}
-
this.error = {};
/**
- * Determines if the given status is successful.
- */
-error.isSuccess = status => status === "success";
-
-/**
* Checks if obj is an instance of the Error prototype in a safe manner.
* Prefer using this over using instanceof since the Error prototype
* isn't unique across browsers, and XPCOM exceptions are special
* snowflakes.
*
* @param {*} val
* Any value that should be undergo the test for errorness.
* @return {boolean}
* True if error, false otherwise.
*/
error.isError = function(val) {
if (val === null || typeof val != "object") {
return false;
- } else if ("result" in val && val.result in XPCOM_EXCEPTIONS) {
+ } else if ("result" in val) {
return true;
} else {
return Object.getPrototypeOf(val) == "Error";
}
};
/**
* Checks if obj is an object in the WebDriverError prototypal chain.
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -17,16 +17,17 @@ marionette.jar:
content/ChromeUtils.js (ChromeUtils.js)
content/error.js (error.js)
content/command.js (command.js)
content/dispatcher.js (dispatcher.js)
content/emulator.js (emulator.js)
content/modal.js (modal.js)
content/proxy.js (proxy.js)
content/capture.js (capture.js)
+ content/cookies.js (cookies.js)
#ifdef ENABLE_TESTS
content/test.xul (client/marionette/chrome/test.xul)
content/test2.xul (client/marionette/chrome/test2.xul)
content/test_nested_iframe.xul (client/marionette/chrome/test_nested_iframe.xul)
content/test_anonymous_content.xul (client/marionette/chrome/test_anonymous_content.xul)
#endif
% content specialpowers %content/
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -9,18 +9,21 @@ var uuidGen = Cc["@mozilla.org/uuid-gene
var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
loader.loadSubScript("chrome://marionette/content/simpletest.js");
loader.loadSubScript("chrome://marionette/content/common.js");
loader.loadSubScript("chrome://marionette/content/actions.js");
Cu.import("chrome://marionette/content/capture.js");
+Cu.import("chrome://marionette/content/cookies.js");
Cu.import("chrome://marionette/content/elements.js");
Cu.import("chrome://marionette/content/error.js");
+Cu.import("chrome://marionette/content/proxy.js");
+
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
var utils = {};
utils.window = content;
// Load Event/ChromeUtils for use with JS scripts:
loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils);
@@ -72,16 +75,19 @@ var readyStateTimer = Cc["@mozilla.org/t
// timer for navigation commands.
var navTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
var onDOMContentLoaded;
// Send move events about this often
var EVENT_INTERVAL = 30; // milliseconds
// last touch for each fingerId
var multiLast = {};
+var chrome = proxy.toChrome(sendSyncMessage.bind(this));
+var cookies = new Cookies(() => curContainer.frame.document, chrome);
+
Cu.import("resource://gre/modules/Log.jsm");
var logger = Log.repository.getLogger("Marionette");
logger.info("loaded listener.js");
var modalHandler = function() {
// This gets called on the system app only since it receives the mozbrowserprompt event
sendSyncMessage("Marionette:switchedToFrame", { frameValue: null, storePrevious: true });
let isLocal = sendSyncMessage("MarionetteFrame:handleModal", {})[0].value;
if (isLocal) {
@@ -213,16 +219,19 @@ var findElementsContentFn = dispatch(fin
var isElementSelectedFn = dispatch(isElementSelected);
var clearElementFn = dispatch(clearElement);
var isElementDisplayedFn = dispatch(isElementDisplayed);
var getElementValueOfCssPropertyFn = dispatch(getElementValueOfCssProperty);
var switchToShadowRootFn = dispatch(switchToShadowRoot);
var getCookiesFn = dispatch(getCookies);
var singleTapFn = dispatch(singleTap);
var takeScreenshotFn = dispatch(takeScreenshot);
+var addCookieFn = dispatch(addCookie);
+var deleteCookieFn = dispatch(deleteCookie);
+var deleteAllCookiesFn = dispatch(deleteAllCookies);
/**
* Start all message listeners
*/
function startListeners() {
addMessageListenerId("Marionette:receiveFiles", receiveFiles);
addMessageListenerId("Marionette:newSession", newSession);
addMessageListenerId("Marionette:executeScript", executeScript);
@@ -259,20 +268,20 @@ function startListeners() {
addMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
addMessageListenerId("Marionette:deleteSession", deleteSession);
addMessageListenerId("Marionette:sleepSession", sleepSession);
addMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
addMessageListenerId("Marionette:importScript", importScript);
addMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
addMessageListenerId("Marionette:setTestName", setTestName);
addMessageListenerId("Marionette:takeScreenshot", takeScreenshotFn);
- addMessageListenerId("Marionette:addCookie", addCookie);
+ addMessageListenerId("Marionette:addCookie", addCookieFn);
addMessageListenerId("Marionette:getCookies", getCookiesFn);
- addMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies);
- addMessageListenerId("Marionette:deleteCookie", deleteCookie);
+ addMessageListenerId("Marionette:deleteAllCookies", deleteAllCookiesFn);
+ addMessageListenerId("Marionette:deleteCookie", deleteCookieFn);
}
/**
* Used during newSession and restart, called to set up the modal dialog listener in b2g
*/
function waitForReady() {
if (content.document.readyState == 'complete') {
readyStateTimer.cancel();
@@ -364,20 +373,20 @@ function deleteSession(msg) {
removeMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
removeMessageListenerId("Marionette:deleteSession", deleteSession);
removeMessageListenerId("Marionette:sleepSession", sleepSession);
removeMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
removeMessageListenerId("Marionette:importScript", importScript);
removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
removeMessageListenerId("Marionette:setTestName", setTestName);
removeMessageListenerId("Marionette:takeScreenshot", takeScreenshotFn);
- removeMessageListenerId("Marionette:addCookie", addCookie);
+ removeMessageListenerId("Marionette:addCookie", addCookieFn);
removeMessageListenerId("Marionette:getCookies", getCookiesFn);
- removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies);
- removeMessageListenerId("Marionette:deleteCookie", deleteCookie);
+ removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookiesFn);
+ removeMessageListenerId("Marionette:deleteCookie", deleteCookieFn);
if (isB2G) {
content.removeEventListener("mozbrowsershowmodalprompt", modalHandler, false);
}
elementManager.reset();
// reset container frame to the top-most frame
curContainer = { frame: content, shadowRoot: null };
curContainer.frame.focus();
actions.touchIds = {};
@@ -1828,65 +1837,26 @@ function switchToFrame(msg) {
curContainer.frame = curContainer.frame.contentWindow;
if (msg.json.focus)
curContainer.frame.focus();
checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
}
sendResponse({value: rv}, command_id);
}
- /**
- * Add a cookie to the document
- */
-function addCookie(msg) {
- let cookie = msg.json.cookie;
- if (!cookie.expiry) {
- var date = new Date();
- var thePresent = new Date(Date.now());
- date.setYear(thePresent.getFullYear() + 20);
- cookie.expiry = date.getTime() / 1000; // Stored in seconds.
- }
- if (!cookie.domain) {
- var location = curContainer.frame.document.location;
- cookie.domain = location.hostname;
- } else {
- var currLocation = curContainer.frame.location;
- var currDomain = currLocation.host;
- if (currDomain.indexOf(cookie.domain) == -1) {
- sendError(new InvalidCookieDomainError("You may only set cookies for the current domain"), msg.json.command_id);
- }
- }
-
- // The cookie's domain may include a port. Which is bad. Remove it
- // We'll catch ip6 addresses by mistake. Since no-one uses those
- // this will be okay for now. See Bug 814416
- if (cookie.domain.match(/:\d+$/)) {
- cookie.domain = cookie.domain.replace(/:\d+$/, '');
- }
-
- var document = curContainer.frame.document;
- if (!document || !document.contentType.match(/html/i)) {
- sendError(new UnableToSetCookieError("You may only set cookies on html documents"), msg.json.command_id);
- }
-
- let added = sendSyncMessage("Marionette:addCookie", {value: cookie});
- if (added[0] !== true) {
- sendError(new UnableToSetCookieError(), msg.json.command_id);
- return;
- }
- sendOk(msg.json.command_id);
+function addCookie(cookie) {
+ cookies.add(cookie.name, cookie.value, cookie);
}
/**
* Get all cookies for the current domain.
*/
function getCookies() {
let rv = [];
- let cookies = getVisibleCookies(curContainer.frame.location);
for (let cookie of cookies) {
let expires = cookie.expires;
// session cookie, don't return an expiry
if (expires == 0) {
expires = null;
// date before epoch time, cap to epoch
} else if (expires == 1) {
@@ -1902,57 +1872,29 @@ function getCookies() {
'expiry': expires
});
}
return rv;
}
/**
- * Delete a cookie by name
+ * Delete a cookie by name.
*/
-function deleteCookie(msg) {
- let toDelete = msg.json.name;
- let cookies = getVisibleCookies(curContainer.frame.location);
- for (let cookie of cookies) {
- if (cookie.name == toDelete) {
- let deleted = sendSyncMessage("Marionette:deleteCookie", {value: cookie});
- if (deleted[0] !== true) {
- sendError(new UnknownError("Could not delete cookie: " + msg.json.name), msg.json.command_id);
- return;
- }
- }
- }
-
- sendOk(msg.json.command_id);
+function deleteCookie(name) {
+ cookies.delete(name);
}
/**
- * Delete all the visibile cookies on a page
+ * Delete all the visibile cookies on a page.
*/
-function deleteAllCookies(msg) {
- let cookies = getVisibleCookies(curContainer.frame.location);
+function deleteAllCookies() {
for (let cookie of cookies) {
- let deleted = sendSyncMessage("Marionette:deleteCookie", {value: cookie});
- if (!deleted[0]) {
- sendError(new UnknownError("Could not delete cookie: " + JSON.stringify(cookie)), msg.json.command_id);
- return;
- }
+ cookies.delete(cookie);
}
- sendOk(msg.json.command_id);
-}
-
-/**
- * Get all the visible cookies from a location
- */
-function getVisibleCookies(location) {
- let currentPath = location.pathname || '/';
- let result = sendSyncMessage("Marionette:getVisibleCookies",
- {value: [currentPath, location.hostname]});
- return result[0];
}
function getAppCacheStatus(msg) {
sendResponse({ value: curContainer.frame.applicationCache.status },
msg.json.command_id);
}
// emulator callbacks
--- a/testing/marionette/proxy.js
+++ b/testing/marionette/proxy.js
@@ -1,164 +1,202 @@
/* 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";
-var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("chrome://marionette/content/modal.js");
this.EXPORTED_SYMBOLS = ["proxy"];
const MARIONETTE_OK = "Marionette:ok";
const MARIONETTE_DONE = "Marionette:done";
const MARIONETTE_ERROR = "Marionette:error";
const logger = Log.repository.getLogger("Marionette");
const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+// Proxy handler that traps requests to get a property. Will prioritise
+// properties that exist on the object's own prototype.
+var ownPriorityGetterTrap = {
+ get: (obj, prop) => {
+ if (obj.hasOwnProperty(prop)) {
+ return obj[prop];
+ }
+ return (...args) => obj.send(prop, args);
+ }
+};
+
this.proxy = {};
/**
* Creates a transparent interface between the chrome- and content
- * processes.
+ * contexts.
*
* Calls to this object will be proxied via the message manager to the active
* browsing context (content) and responses will be provided back as
* promises.
*
* The argument sequence is serialised and passed as an array, unless it
* consists of a single object type that isn't null, in which case it's
* passed literally. The latter specialisation is temporary to achieve
* backwards compatibility with listener.js.
*
* @param {function(): (nsIMessageSender|nsIMessageBroadcaster)} mmFn
* Function returning the current message manager.
* @param {function(string, Object, number)} sendAsyncFn
* Callback for sending async messages to the current listener.
*/
proxy.toListener = function(mmFn, sendAsyncFn) {
- let sender = new ContentSender(mmFn, sendAsyncFn);
- let handler = {
- get: (obj, prop) => {
- if (obj.hasOwnProperty(prop)) {
- return obj[prop];
- }
- return (...args) => obj.send(prop, args);
- }
- };
- return new Proxy(sender, handler);
+ let sender = new AsyncContentSender(mmFn, sendAsyncFn);
+ return new Proxy(sender, ownPriorityGetterTrap);
};
/**
- * The ContentSender allows one to make synchronous calls to the
+ * The AsyncContentSender allows one to make synchronous calls to the
* message listener of the content frame of the current browsing context.
*
* Presumptions about the responses from content space are made so we
* can provide a nicer API on top of the message listener primitives that
* make calls from chrome- to content space seem synchronous by leveraging
* promises.
*
* The promise is guaranteed not to resolve until the execution of the
* command in content space is complete.
- *
- * @param {function(): (nsIMessageSender|nsIMessageBroadcaster)} mmFn
- * Function returning the current message manager.
- * @param {function(string, Object, number)} sendAsyncFn
- * Callback for sending async messages to the current listener.
*/
-var ContentSender = function(mmFn, sendAsyncFn) {
- this.curId = null;
- this.sendAsync = sendAsyncFn;
- this.mmFn_ = mmFn;
- this._listeners = [];
-};
-
-Object.defineProperty(ContentSender.prototype, "mm", {
- get: function() { return this.mmFn_(); }
-});
-
-ContentSender.prototype.removeListeners = function () {
- this._listeners.map(l => this.mm.removeMessageListener(l[0], l[1]));
- this._listeners = [];
-}
+this.AsyncContentSender = class {
+ constructor(mmFn, sendAsyncFn) {
+ this.curId = null;
+ this.sendAsync = sendAsyncFn;
+ this.mmFn_ = mmFn;
+ this._listeners = [];
+ }
-/**
- * Call registered function in the frame script environment of the
- * current browsing context's content frame.
- *
- * @param {string} name
- * Function to call in the listener, e.g. for "Marionette:foo8",
- * use "foo".
- * @param {Array} args
- * Argument list to pass the function. If args has a single entry
- * that is an object, we assume it's an old style dispatch, and
- * the object will passed literally.
- *
- * @return {Promise}
- * A promise that resolves to the result of the command.
- */
-ContentSender.prototype.send = function(name, args) {
- if (this._listeners[0]) {
- // A prior (probably timed-out) request has left listeners behind.
- // Remove them before proceeding.
- logger.warn("A previous failed command left content listeners behind!");
- this.removeListeners();
+ get mm() {
+ return this.mmFn_();
+ }
+
+ removeListeners() {
+ this._listeners.map(l => this.mm.removeMessageListener(l[0], l[1]));
+ this._listeners = [];
}
- this.curId = uuidgen.generateUUID().toString();
+ /**
+ * Call registered function in the frame script environment of the
+ * current browsing context's content frame.
+ *
+ * @param {string} name
+ * Function to call in the listener, e.g. for "Marionette:foo8",
+ * use "foo".
+ * @param {Array} args
+ * Argument list to pass the function. If args has a single entry
+ * that is an object, we assume it's an old style dispatch, and
+ * the object will passed literally.
+ *
+ * @return {Promise}
+ * A promise that resolves to the result of the command.
+ */
+ send(name, args) {
+ if (this._listeners[0]) {
+ // A prior (probably timed-out) request has left listeners behind.
+ // Remove them before proceeding.
+ logger.warn("A previous failed command left content listeners behind!");
+ this.removeListeners();
+ }
+
+ this.curId = uuidgen.generateUUID().toString();
- let proxy = new Promise((resolve, reject) => {
- let removeListeners = (n, fn) => {
- let rmFn = msg => {
- if (this.curId !== msg.json.command_id) {
- logger.warn("Skipping out-of-sync response from listener: " +
- `Expected response to ${name} with ID ${this.curId}, ` +
- "but got: " + msg.name + msg.json.toSource());
- return;
- }
+ let proxy = new Promise((resolve, reject) => {
+ let removeListeners = (n, fn) => {
+ let rmFn = msg => {
+ if (this.curId !== msg.json.command_id) {
+ logger.warn("Skipping out-of-sync response from listener: " +
+ `Expected response to ${name} with ID ${this.curId}, ` +
+ "but got: " + msg.name + msg.json.toSource());
+ return;
+ }
+
+ this.removeListeners();
+ modal.removeHandler(handleDialog);
- this.removeListeners();
- modal.removeHandler(handleDialog);
+ fn(msg);
+ this.curId = null;
+ };
+
+ this._listeners.push([n, rmFn]);
+ return rmFn;
+ };
- fn(msg);
- this.curId = null;
+ let okListener = () => resolve();
+ let valListener = msg => resolve(msg.json.value);
+ let errListener = msg => reject(msg.objects.error);
+
+ let handleDialog = (subject, topic) => {
+ this.removeListeners()
+ modal.removeHandler(handleDialog);
+ this.sendAsync("cancelRequest");
+ resolve();
};
- this._listeners.push([n, rmFn]);
- return rmFn;
- };
-
- let okListener = () => resolve();
- let valListener = msg => resolve(msg.json.value);
- let errListener = msg => reject(msg.objects.error);
-
- let handleDialog = (subject, topic) => {
- this.removeListeners()
- modal.removeHandler(handleDialog);
- this.sendAsync("cancelRequest");
- resolve();
- };
+ // start content process listeners, and install observers for global-
+ // and tab modal dialogues
+ this.mm.addMessageListener(MARIONETTE_OK, removeListeners(MARIONETTE_OK, okListener));
+ this.mm.addMessageListener(MARIONETTE_DONE, removeListeners(MARIONETTE_DONE, valListener));
+ this.mm.addMessageListener(MARIONETTE_ERROR, removeListeners(MARIONETTE_ERROR, errListener));
+ modal.addHandler(handleDialog);
- // start content process listeners, and install observers for global-
- // and tab modal dialogues
- this.mm.addMessageListener(MARIONETTE_OK, removeListeners(MARIONETTE_OK, okListener));
- this.mm.addMessageListener(MARIONETTE_DONE, removeListeners(MARIONETTE_DONE, valListener));
- this.mm.addMessageListener(MARIONETTE_ERROR, removeListeners(MARIONETTE_ERROR, errListener));
- modal.addHandler(handleDialog);
+ this.sendAsync(name, marshal(args), this.curId);
+ });
- // new style dispatches are arrays of arguments, old style dispatches
- // are key-value objects
- let msg = args;
- if (args.length == 1 && typeof args[0] == "object") {
- msg = args[0];
- }
-
- this.sendAsync(name, msg, this.curId);
- });
-
- return proxy;
+ return proxy;
+ }
};
-proxy.ContentSender = ContentSender;
+/**
+ * Creates a transparent interface from the content- to the chrome context.
+ *
+ * Calls to this object will be proxied via the frame's sendSyncMessage
+ * (nsISyncMessageSender) function. Since the message is synchronous,
+ * the return value is presented as a return value.
+ *
+ * Example on how to use from a frame content script:
+ *
+ * let chrome = proxy.toChrome(sendSyncMessage.bind(this));
+ * let cookie = chrome.getCookie("foo");
+ *
+ * @param {nsISyncMessageSender} sendSyncMessageFn
+ * The frame message manager's sendSyncMessage function.
+ */
+proxy.toChrome = function(sendSyncMessageFn) {
+ let sender = new SyncChromeSender(sendSyncMessageFn);
+ return new Proxy(sender, ownPriorityGetterTrap);
+};
+
+/**
+ * The SyncChromeSender sends synchronous RPC messages to the chrome
+ * context, using a frame's sendSyncMessage (nsISyncMessageSender) function.
+ *
+ * Example on how to use from a frame content script:
+ *
+ * let sender = new SyncChromeSender(sendSyncMessage.bind(this));
+ * let res = sender.send("addCookie", cookie);
+ */
+this.SyncChromeSender = class {
+ constructor(sendSyncMessage) {
+ this.sendSyncMessage_ = sendSyncMessage;
+ }
+
+ send(func, args) {
+ let name = "Marionette:" + func;
+ return this.sendSyncMessage_(name, marshal(args));
+ }
+};
+
+function marshal(args) {
+ if (args.length == 1 && typeof args[0] == "object") {
+ return args[0];
+ }
+ return args;
+}