Skip to content
This repository has been archived by the owner on Feb 26, 2022. It is now read-only.

Bug 685564 - Make page-mod work on Firefox mobile #310

Merged
merged 13 commits into from Dec 30, 2011
2 changes: 2 additions & 0 deletions packages/api-utils/data/test-message-manager.js
@@ -0,0 +1,2 @@
const TEST_VALUE = 11;

9 changes: 9 additions & 0 deletions packages/api-utils/docs/message-manager.md
@@ -0,0 +1,9 @@
<!-- contributed by Matteo Ferretti [zer0@mozilla.com] -->

Overview
--------
The `message-manager` module provides a minimalist implementation
of the [Message Manager](https://developer.mozilla.org/en/The_message_manager)
APIs, in a single process environment.

It's mainly used internally for Fennec Birch support.
21 changes: 11 additions & 10 deletions packages/api-utils/docs/namespace.md
Expand Up @@ -2,15 +2,15 @@ Provides an API for creating namespaces for any given objects, which
effectively may be used for creating fields that are not part of objects
public API.

let { Namespace } = require('api-utils/namespace');
let ns = Namespace();
let { ns } = require('api-utils/namespace');
let aNamespace = ns();

ns(publicAPI).secret = secret;
aNamespace(publicAPI).secret = secret;

One namespace may be used with multiple objects:

let { Namespace } = require('api-utils/namespace');
let dom = Namespace();
let { ns } = require('api-utils/namespace');
let dom = ns();

function View(element) {
let view = Object.create(View.prototype);
Expand All @@ -31,29 +31,29 @@ Also, multiple namespaces can be used with one object:
// ./widget.js

let { Cu } = require('chrome');
let { Namespace } = require('api-utils/namespace');
let { ns } = require('api-utils/namespace');
let { View } = require('./view');

// Note this is completely independent from View's internal Namespace object.
let ns = Namespace();
let sandboxes = ns();

function Widget(options) {
let { element, contentScript } = options;
let widget = Object.create(Widget.prototype);
View.call(widget, options.element);
ns(widget).sandbox = Cu.Sandbox(element.ownerDocument.defaultView);
sandboxes(widget).sandbox = Cu.Sandbox(element.ownerDocument.defaultView);
// ...
}
Widget.prototype = Object.create(View.prototype);
Widget.prototype.postMessage = function postMessage(message) {
let { sandbox } = ns(this);
let { sandbox } = sandboxes(this);
sandbox.postMessage(JSON.stringify(JSON.parse(message)));
...
};
Widget.prototype.destroy = function destroy() {
View.prototype.destroy.call(this);
// ...
delete ns(this).sandbox;
delete sandboxes(this).sandbox;
};
exports.Widget = Widget;

Expand All @@ -64,3 +64,4 @@ handing them a namespace accessor function.
Widget.prototype.setInnerHTML = function setInnerHTML(html) {
dom(this).element.innerHTML = String(html);
};

6 changes: 4 additions & 2 deletions packages/api-utils/docs/sandbox.md
Expand Up @@ -13,7 +13,7 @@ string, in which case sandbox will get exact same privileges as a scripts
loaded from that URL. Argument also could be a DOM window object, to inherit
privileges from the window being passed. Finally if argument is omitted or is
`null` sandbox will have a chrome privileges giving it access to all the XPCOM
components. Optionally `sandbox` function can be passed a second optional
components. Optionally `sandbox` function can be passed a second optional
argument (See [sandbox documentation on MDN](https://developer.mozilla.org/en/Components.utils.Sandbox#Optional_parameter)
for details).

Expand All @@ -39,7 +39,9 @@ Version of JavaScript can be also specified via optional argument:

### Loading scripts ###

API provides limited API for loading scripts right form the local URLs.
API provides limited API for loading scripts right form the local URLs,
but data: URLs are supported.

load(scope, 'resource://path/to/my/script.js');
load(scope, 'file:///path/to/script.js');
load(scope, 'data:,var a = 5;');
4 changes: 2 additions & 2 deletions packages/api-utils/lib/hidden-frame.js
Expand Up @@ -49,7 +49,7 @@ const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

let hostFrame, hostDocument, hiddenWindow, isHostFrameReady = false;

if (!require("./xul-app").isOneOf(["Firefox", "Thunderbird"])) {
if (!require("./xul-app").isOneOf(["Firefox", "Fennec", "Thunderbird"])) {
throw new Error([
"The hidden-frame module currently supports only Firefox and Thunderbird. ",
"In the future, we would like it to support other applications, however. ",
Expand Down Expand Up @@ -121,7 +121,7 @@ function HiddenFrame(options) {
hostFrame = hiddenWindow.document.createElement("iframe");

// ugly ugly hack. This is the most lightweight chrome:// file I could find on the tree
// This hack should be removed by proper platform support on bug 565388
// This hack should be removed by proper platform support on bug 565388
hostFrame.setAttribute("src", "chrome://global/content/mozilla.xhtml");
hostFrame.addEventListener("DOMContentLoaded", setHostFrameReady, false);

Expand Down
235 changes: 235 additions & 0 deletions packages/api-utils/lib/message-manager.js
@@ -0,0 +1,235 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Jetpack.
*
* The Initial Developer of the Original Code is Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Matteo Ferretti <zer0@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */

"use strict";

const BAD_LISTENER = "The event listener must be a function.";

const { Cc, Ci, Cu, CC } = require("chrome");
const { setTimeout } = require("./timer");

const { ns } = require("./namespace");

const { curry, invoke } = require("./utils/function");

const Sandbox = require("./sandbox");

// JSON.stringify is buggy with cross-sandbox values,
// it may return "{}" on functions. Use a replacer to match them correctly.
const jsonFixer = function (k, v) typeof v === "function" ? undefined : v;

/**
* Defers invoking the function until the current call stack has cleared.
*
* @param {Function} fn
* The function to defer.
*
* @returns {Function}
* The deferred function
*/
const defer = function(fn) function() {
setTimeout(invoke, 0, fn, arguments, this)
};

/**
* Adds a message listener.
* This listener will receive messages sent from the remote frame.
*
* @param {String} name
* The name of the message for which to add a listener.
* @param {Function} listener
* The listener function called when the message is received.
*/
function addMessageListener(name, listener) {
if (typeof listener !== "function")
throw new Error(BAD_LISTENER);

let listeners = frame(this).listeners;

if (name in listeners) {
if (~listeners[name].indexOf(listener))
return;
} else {
listeners[name] = [];
}

listeners[name].push(listener);
}

/**
* Removes a message listener previously added by calling addMessageListener.
*
* @param {String} name
* The name of the message for which to remove a listener.
* @param {Function} listener
* The listener function has to be removed.
*/
function removeMessageListener(name, listener) {
if (typeof listener !== "function")
throw new Error(BAD_LISTENER);

let listeners = frame(this).listeners;

if (!(name in listeners))
return;

let index = listeners[name].indexOf(listener);

if (~index) {
listeners[name].splice(index, 1);
}
}

/**
* Sends a message to the listeners.
*
* @param {Boolean} sync
* Indicates if the call is synchronous or asynchronous
* @param {String} name
* The name of the message to send to the listeners.
* @param {Object} [data=null]
* A JSON object containing data to be delivered to the listeners.
*
* @returns {Array|undefined}
* An array with the return values of the listeners if `sync` is `true`,
* otherwise `undefined`.
*/
function sendMessage(sync, name, data) {
typeof data === "undefined" && (data = null);

let listeners = frame(frame(this).receiver).listeners;

let responses = [];

let returnValue = sync ? responses : undefined;

if (!(name in listeners))
return returnValue;

let json = JSON.parse(JSON.stringify(data, jsonFixer));

for each(let listener in listeners[name]) {
try {
let response = listener.call(null, {
sync : sync,
name : name,
json : json,
target : null
});

if (sync) {
if (typeof response === "undefined")
responses.push(response);
else
responses.push(JSON.parse(JSON.stringify(response, jsonFixer)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively you could just JSON.parse(JSON.stringify(responses, jsonFixer)) all the responses.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case JSON.stringify will censored the undefined values to null, and that is not the expected behavior.

}

} catch (e) {
console.exception(e);
}
}
return returnValue;
};

let sendSyncMessage = curry(sendMessage, true);
let sendAsyncMessage = curry(defer(sendMessage), false);

let frame = ns({receiver: null, listeners: null});

/**
* The MessageManager object emulates the Message Manager API, without creating
* new processes. It useful in mono process context, like Fennec.
*
* @see
* https://developer.mozilla.org/en/The_message_manager
*/
function MessageManager() {

let sandbox = Sandbox.sandbox(null, { wantXrays : false });

Object.defineProperties(sandbox, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably use merge:

merge(sandbox, {
  addMessageListener: addMessageListener.bind(sandbox),
  ....
})

It will methods configurable & writable, but they are in normal massegaManager anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't have hard feeling about it, I'd like to keep the descriptors.

addMessageListener: {value: addMessageListener.bind(sandbox)},

removeMessageListener: { value: removeMessageListener.bind(sandbox)},

sendAsyncMessage: {value: sendAsyncMessage.bind(sandbox)},

sendSyncMessage: { value: sendSyncMessage.bind(sandbox) }
});

frame(this).receiver = sandbox;
frame(sandbox).receiver = this;

frame(this).listeners = {};
frame(sandbox).listeners = {};
}

MessageManager.prototype = {
constructor: MessageManager,

addMessageListener : addMessageListener,

removeMessageListener : removeMessageListener,

sendAsyncMessage : sendAsyncMessage,

/**
* Loads a script into the remote frame.
*
* @param {String} uri
* The URL of the script to load into the frame; this must be an absolute
* local URL, but data: URLs are supported.
* @param {Boolean} allowDelayedLoad
* Not used.
*/
loadFrameScript: function loadFrameScript(uri, async) {
if (arguments.length < loadFrameScript.length)
throw new Error("Not enough arguments");

let sandbox = frame(this).receiver;

try {
Sandbox.load(sandbox, uri);
} catch (e) {
console.exception(e)
}
}
}

Object.freeze(MessageManager);
Object.freeze(MessageManager.prototype);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd encourage you to use Base.extend for defining a MessageManager as it will reduce noise and will make sure that everything that requires freezing etc will be frozen without manually steps.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not comfortable yet with Base.extend approach, I personally think that in case of regular constructors it adds more additional steps (even if they are hidden) and became a sort of overkill. If it's okay to you, as you suggested also in one of your comment, I will leave as is this part until we are able to have have a talk about it, and I will modify later in case of needed.


exports.MessageManager = MessageManager;
5 changes: 5 additions & 0 deletions packages/api-utils/lib/namespace.js
Expand Up @@ -19,6 +19,7 @@
*
* Contributor(s):
* Irakli Gozalishvili <gozala@mozilla.com>
* Matteo Ferretti <zer0@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
Expand Down Expand Up @@ -53,3 +54,7 @@ exports.Namespace = function Namespace(prototype) {
map.set(target, Object.create(prototype)), map.get(target);
};
};

// `Namespace` is a e4x function in the scope, so we export the function also as
// `ns` as alias to avoid clashing.
exports.ns = exports.Namespace;
5 changes: 5 additions & 0 deletions packages/api-utils/lib/process.js
Expand Up @@ -41,12 +41,15 @@ const { createRemoteBrowser } = require("api-utils/window-utils");
const { channel } = require("./channel");
const packaging = require('@packaging');
const { when } = require('./unload');
const { MessageManager } = require('./message-manager');

const addonService = '@mozilla.org/addon/service;1' in Cc ?
Cc['@mozilla.org/addon/service;1'].getService(Ci.nsIAddonService) : null

const ENABLE_E10S = packaging.enable_e10s;

const isFennec = require("./xul-app").is("Fennec");

function loadScript(target, uri, sync) {
return 'loadScript' in target ? target.loadScript(uri, sync)
: target.loadFrameScript(uri, sync)
Expand Down Expand Up @@ -82,6 +85,8 @@ exports.spawn = function spawn(id, path) {
if (ENABLE_E10S && addonService) {
console.log('!!!!!!!!!!!!!!!!!!!! Using addon process !!!!!!!!!!!!!!!!!!');
deliver(process(addonService.createAddon(), id, path));
} else if (isFennec) {
deliver(process(new MessageManager(), id, path));
} else {
createRemoteBrowser(ENABLE_E10S)(function(browser) {
let messageManager = browser.QueryInterface(Ci.nsIFrameLoaderOwner).
Expand Down