Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

copy all core files back

  • Loading branch information...
commit 4d246ae8302f7afa62b66a530d61600f4c42cb45 1 parent 350079c
@mhammond mhammond authored
View
26 bootstrap.js
@@ -562,31 +562,9 @@ function install(aParams, aReason) {
}
function startup(aParams, aReason) {
- // We can't know the order these things will initialize in...
- let realStartup = function() {
- OverlayManager.init(aParams, function() {
- // addon specific stuff we need to start before
- // applying our overlays
- let tmp = {}
- Cu.import("resource://socialapi/modules/provider.js", tmp);
- let createCallback = function(manifest) {
- return new tmp.SocialProvider(manifest);
- }
- Cu.import("resource://socialapi/modules/registry.js", tmp);
- tmp.initialize(createCallback);
- });
- };
let res = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
- if (res.hasSubstitution("socialapi")) {
- // the core has already initialized...
- realStartup();
- } else {
- Services.obs.addObserver({
- observe: function(aSubject, aTopic, aData) {
- realStartup();
- }
- }, "socialapi-core-startup", false);
- }
+ res.setSubstitution("socialapi", aParams.resourceURI);
+ OverlayManager.init(aParams, function() {});
}
function shutdown(aParams, aReason) {
View
3  chrome-branch-default.manifest
@@ -0,0 +1,3 @@
+locale socialapi en-US locale/en-US/
+
+resource socialapi ./
View
4 locale/en-US/strings.properties
@@ -0,0 +1,4 @@
+installoffer.notificationbar="This site supports additional functionality for Firefox, would you like to install it?"
+dontask.label=Don't ask again
+dontask.accesskey=d
+yes.label=Yes
View
563 modules/frameworker.js
@@ -0,0 +1,563 @@
+/* -*- Mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/.
+ *
+ * Contributor(s):
+ * Michael Hanson <mhanson@mozilla.com>
+ * Edward Lee <edilee@mozilla.com>
+ * Mark Hammond <mhammond@mozilla.com>
+ * Shane Caraveo <scaraveo@mozilla.com>
+ */
+
+/*
+* This is an implementation of a "Shared Worker" using an iframe in the
+* hidden DOM window. A subset of new APIs are introduced to the window
+* by cloning methods from the worker's JS origin.
+*/
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+var workerInfos = {}; // keyed by URL.
+
+
+function log(msg) {
+ Services.console.logStringMessage(new Date().toISOString() + " [frameworker]: " + msg);
+};
+
+var _nextPortId = 1;
+
+// This function is magically injected into the sandbox and used there.
+// Thus, it is only ever dealing with "worker" ports.
+function __initWorkerMessageHandler() {
+
+ let ports = {}; // all "worker" ports currently alive, keyed by ID.
+
+ function messageHandler(event) {
+ // We will ignore all messages destined for otherType.
+ let data = event.data;
+ let portid = data.portId;
+ let port;
+ if (!data.portFromType || data.portFromType === "worker") {
+ // this is a message posted by ourself so ignore it.
+ return;
+ }
+ switch (data.portTopic) {
+ case "port-create":
+ // a new port was created on the "client" side - create a new worker
+ // port and store it in the map
+ port = new WorkerPort(portid);
+ ports[portid] = port;
+ // and call the "onconnect" handler.
+ onconnect({ports: [port]});
+ break;
+
+ case "port-close":
+ // the client side of the port was closed, so close this side too.
+ port = ports[portid];
+ if (!port) {
+ // port already closed (which will happen when we call port.close()
+ // below - the client side will send us this message but we've
+ // already closed it.)
+ return;
+ }
+ delete ports[portid];
+ port.close();
+ break;
+
+ case "port-message":
+ // the client posted a message to this worker port.
+ port = ports[portid];
+ if (!port) {
+ // port must be closed - this shouldn't happen!
+ return;
+ }
+ port._onmessage(data.data);
+ break;
+
+ default:
+ break;
+ }
+ }
+ // addEventListener is injected into the sandbox.
+ addEventListener('message', messageHandler);
+}
+
+// And this is the message listener for the *client* (ie, chrome) side of the world.
+function initClientMessageHandler(workerInfo, workerWindow) {
+ function _messageHandler(event) {
+ // We will ignore all messages destined for otherType.
+ let data = event.data;
+ let portid = data.portId;
+ let port;
+ if (!data.portFromType || data.portFromType === "client") {
+ // this is a message posted by ourself so ignore it.
+ return;
+ }
+ switch (data.portTopic) {
+ // No "port-create" here - client ports are created explicitly.
+
+ case "port-close":
+ // the worker side of the port was closed, so close this side too.
+ port = workerInfo.ports[portid];
+ if (!port) {
+ // port already closed (which will happen when we call port.close()
+ // below - the worker side will send us this message but we've
+ // already closed it.)
+ return;
+ }
+ delete workerInfo.ports[portid];
+ port.close();
+ break;
+
+ case "port-message":
+ // the client posted a message to this worker port.
+ port = workerInfo.ports[portid];
+ if (!port) {
+ return;
+ }
+ port._onmessage(data.data);
+ break;
+
+ default:
+ break;
+ }
+ }
+ // this can probably go once debugged and working correctly!
+ function messageHandler(event) {
+ try {
+ _messageHandler(event);
+ } catch (ex) {
+ Cu.reportError("Error handling client port control message: " + ex + "\n" + ex.stack);
+ }
+ }
+ workerWindow.addEventListener('message', messageHandler);
+}
+
+
+// The port implementation which is shared between clients and workers.
+function AbstractPort(portid) {
+ this._portid = portid;
+ this._handler = undefined;
+ // pending messages sent to this port before it has a message handler.
+ this._pendingMessagesIncoming = [];
+}
+
+AbstractPort.prototype = {
+ _portType: null, // set by a subclass.
+ // abstract methods to be overridden.
+ _dopost: function(data) {
+ throw new Error("not implemented");
+ },
+ _onerror: function(err) {
+ throw new Error("not implemented");
+ },
+
+ // and concrete methods shared by client and workers.
+ toString: function() {
+ return "MessagePort(portType='" + this._portType + "', portId=" + this._portid + ")";
+ },
+ _JSONParse: function(data) JSON.parse(data),
+
+ _postControlMessage: function(topic, data) {
+ let postData = {portTopic: topic,
+ portId: this._portid,
+ portFromType: this._portType,
+ data: data};
+ this._dopost(postData);
+ },
+
+ _onmessage: function(data) {
+ // See comments in postMessage below - we work around a cloning
+ // issue by using JSON for these messages.
+ // Further, we allow the workers to override exactly how the JSON parsing
+ // is done - we try and do such parsing in the client window so things
+ // like prototype overrides on Array work as expected.
+ data = this._JSONParse(data);
+ if (!this._handler) {
+ this._pendingMessagesIncoming.push(data);
+ }
+ else {
+ try {
+ this._handler({data: data});
+ }
+ catch (ex) {
+ this._onerror(ex);
+ }
+ }
+ },
+
+ set onmessage(handler) { // property setter for onmessage
+ this._handler = handler;
+ while (this._pendingMessagesIncoming.length) {
+ this._onmessage(this._pendingMessagesIncoming.shift());
+ }
+ },
+
+ /**
+ * postMessage
+ *
+ * Send data to the onmessage handler on the other end of the port. The
+ * data object should have a topic property.
+ *
+ * @param {jsobj} data
+ */
+ postMessage: function(data) {
+ // There seems to be an issue with passing objects directly and letting
+ // the structured clone thing work - we sometimes get:
+ // [Exception... "The object could not be cloned." code: "25" nsresult: "0x80530019 (DataCloneError)"]
+ // The best guess is that this happens when funky things have been added to the prototypes.
+ // It doesn't happen for our "control" messages, only in messages from
+ // content - so we explicitly use JSON on these messages as that avoids
+ // the problem.
+ this._postControlMessage("port-message", JSON.stringify(data));
+ },
+
+ close: function() {
+ if (!this._portid) {
+ return; // already closed.
+ }
+ this._postControlMessage("port-close");
+ // and clean myself up.
+ this._handler = null;
+ this._pendingMessagesIncoming = [];
+ this._portid = null;
+ }
+}
+
+// Note: this is never instantiated in chrome - the source is sent across
+// to the worker and it is evaluated there and created in response to a
+// port-create message we send.
+function WorkerPort(portid) {
+ AbstractPort.call(this, portid);
+}
+
+WorkerPort.prototype = {
+ __proto__: AbstractPort.prototype,
+ _portType: "worker",
+
+ _dopost: function(data) {
+ // postMessage is injected into the sandbox.
+ postMessage(data, "*");
+ },
+
+ _onerror: function(err) {
+ // dump() is the only thing available in the worker context to report
+ // errors. We could possibly send a message back to the chrome code so
+ // it can be logged more appropriately - later..
+ dump("Port " + this + " handler failed: " + err + "\n" + err.stack);
+ }
+}
+
+// This port lives entirely in chrome.
+function ClientPort(portid, clientWindow) {
+ this._clientWindow = clientWindow
+ this._window = null;
+ // messages posted to the worker before the worker has loaded.
+ this._pendingMessagesOutgoing = [];
+ AbstractPort.call(this, portid);
+}
+
+ClientPort.prototype = {
+ __proto__: AbstractPort.prototype,
+ _portType: "client",
+
+ _JSONParse: function(data) {
+ if (this._clientWindow) {
+ return this._clientWindow.JSON.parse(data);
+ }
+ return JSON.parse(data);
+ },
+
+ _createWorkerAndEntangle: function(workerInfo) {
+ this._window = workerInfo.frame.contentWindow;
+ workerInfo.ports[this._portid] = this;
+ this._postControlMessage("port-create");
+ while (this._pendingMessagesOutgoing.length) {
+ this._dopost(this._pendingMessagesOutgoing.shift());
+ }
+ },
+
+ _dopost: function(data) {
+ if (!this._window) {
+ this._pendingMessagesOutgoing.push(data);
+ } else {
+ this._window.postMessage(data, "*");
+ }
+ },
+
+ _onerror: function(err) {
+ Cu.reportError("Port " + this + " handler failed: " + err + "\n" + err.stack);
+ },
+
+ close: function() {
+ if (!this._portid) {
+ return; // already closed.
+ }
+ // a leaky abstraction due to the worker spec not specifying how the
+ // other end of a port knows it is closing.
+ this.postMessage({topic: "social.port-closing"});
+ AbstractPort.prototype.close.call(this);
+ this._window = null;
+ this._pendingMessagesOutgoing = null;
+ }
+}
+
+/**
+ * FrameWorker
+ *
+ * A Frameworker is an iframe that is attached to the hiddenWindow,
+ * which contains a pair of MessagePorts. It is constructed with the
+ * URL of some JavaScript that will be run in the context of the window;
+ * the script does not have a full DOM but is instead run in a sandbox
+ * that has a select set of methods cloned from the URL's domain.
+ *
+ * The FrameWorker iframe is a singleton for a given script URL. If one
+ * alread, exists, the FrameWorker constructor will connect to it.
+ *
+ * @param {String} url
+ * @returns {Object} object containing a port and terminate function
+ */
+function FrameWorker(url, clientWindow, name) {
+ let workerName = (name ? name : url);
+ log("creating worker for " + workerName);
+ // first create the client port we are going to use. Laster we will
+ // message the worker to create the worker port.
+ let portid = _nextPortId++;
+ let clientPort = new ClientPort(portid, clientWindow);
+
+ let workerInfo = workerInfos[url];
+ if (!workerInfo) {
+ log("creating a new worker for " + workerName);
+ let appShell = Cc["@mozilla.org/appshell/appShellService;1"]
+ .getService(Ci.nsIAppShellService);
+ let hiddenDOMWindow = appShell.hiddenDOMWindow;
+ // hrmph - I guess mixedpuppy had a good reason for changing from
+ // createElement to createElementNS, but markh gets NS_ERROR_NOT_AVAILABLE
+ // so just try both :)
+ let frame;
+ try {
+ frame = hiddenDOMWindow.document.createElementNS(XUL_NS, "iframe");
+ }
+ catch (ex) {
+ frame = hiddenDOMWindow.document.createElement("iframe");
+ }
+ frame.setAttribute("type", "content");
+ frame.setAttribute("src", url);
+
+ // setup the workerInfo and add this connection to the pending queue
+ workerInfo = workerInfos[url] = {
+ frame: frame,
+ pendingPorts: [clientPort], // ports yet to be connected.
+ ports: {}, // all live, connected ports
+ loaded: false
+ };
+
+ var injectController = function(doc, topic, data) {
+ try {
+ if (!doc.defaultView || doc.defaultView != frame.contentWindow) {
+ return;
+ }
+ Services.obs.removeObserver(injectController, 'document-element-inserted', false);
+
+ let workerWindow = frame.contentWindow;
+
+ let sandbox = new Cu.Sandbox(workerWindow);
+ // copy the window apis onto the sandbox namespace only functions or
+ // objects that are naturally a part of an iframe, I'm assuming they are
+ // safe to import this way
+ let workerAPI = ['MozWebSocket', 'WebSocket', 'mozIndexedDB', 'localStorage',
+ 'XMLHttpRequest',
+ 'atob', 'btoa', 'clearInterval', 'clearTimeout', 'dump',
+ 'setInterval', 'setTimeout',
+ 'MozBlobBuilder', 'FileReader', 'Blob',
+ 'navigator'];
+ for each(let fn in workerAPI) {
+ try {
+ if (workerWindow[fn]) {
+ sandbox[fn] = workerWindow[fn];
+ }
+ }
+ catch(e) {
+ Cu.reportError("failed to import API "+fn+"\n"+e+"\n");
+ }
+ }
+ Object.defineProperty(sandbox, 'cookie', {
+ get: function() { return workerWindow.document.cookie },
+ set: function(val) { workerWindow.document.cookie = val },
+ enumerable: true
+ });
+ sandbox.importScripts = function importScripts() {
+ if (arguments.length < 1) return;
+ let workerURI = Services.io.newURI(url, null, null);
+ for each(let uri in arguments) {
+ // resolve the uri against the loaded worker
+ let scriptURL = workerURI.resolve(uri);
+ if (scriptURL.indexOf(workerURI.prePath) != 0) {
+ throw new Error("importScripts same-origin violation with "+uri);
+ }
+ log("importScripts loading "+scriptURL);
+ // load the url *synchronously*
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', scriptURL, false);
+ xhr.onreadystatechange = function(aEvt) {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200 || xhr.status == 0) {
+ try {
+ Cu.evalInSandbox(xhr.responseText, sandbox);
+ }
+ catch(e) {
+ Cu.reportError("importScripts eval failed: "+e);
+ }
+ }
+ else {
+ Cu.reportError("Unable to importScripts ["+scriptURL+"], status " + xhr.status);
+ }
+ }
+ };
+ xhr.send(null);
+ }
+ };
+ // and we delegate ononline and onoffline events to the worker.
+ // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html#workerglobalscope
+ frame.addEventListener('offline', function(event) {
+ Cu.evalInSandbox("onoffline();", sandbox);
+ }, false);
+ frame.addEventListener('online', function(event) {
+ Cu.evalInSandbox("ononline();", sandbox);
+ }, false);
+
+ sandbox.postMessage = function(d, o) { workerWindow.postMessage(d, o) };
+ sandbox.addEventListener = function(t, l, c) { workerWindow.addEventListener(t, l, c) };
+
+ // And a very hacky work-around for bug 734215
+ sandbox.bufferToArrayHack = function(a) {
+ return new workerWindow.Uint8Array(a);
+ };
+
+ workerWindow.addEventListener("load", function() {
+ log("got worker onload event for " + workerName);
+ // the iframe has loaded the js file as text - first inject the magic
+ // port-handling code into the sandbox.
+ function getProtoSource(ob) {
+ let raw = ob.prototype.toSource();
+ return ob.name + ".prototype=" + raw + ";"
+ }
+ try {
+ let scriptText = [AbstractPort.toSource(),
+ getProtoSource(AbstractPort),
+ WorkerPort.toSource(),
+ getProtoSource(WorkerPort),
+ // *sigh* - toSource() doesn't do __proto__
+ "WorkerPort.prototype.__proto__=AbstractPort.prototype;",
+ __initWorkerMessageHandler.toSource(),
+ "__initWorkerMessageHandler();" // and bootstrap it.
+ ].join("\n")
+ Cu.evalInSandbox(scriptText, sandbox, "1.8", "<injected port handling code>", 1);
+ }
+ catch (e) {
+ Cu.reportError("Error injecting port code into content side of the worker: " + e + "\n" + e.stack);
+ }
+ // and wire up the client message handling.
+ try {
+ initClientMessageHandler(workerInfo, workerWindow);
+ }
+ catch (e) {
+ Cu.reportError("Error setting up event listener for chrome side of the worker: " + e + "\n" + e.stack);
+ }
+ // Now get the worker js code and eval it into the sandbox
+ try {
+ let scriptText = workerWindow.document.body.textContent;
+ Cu.evalInSandbox(scriptText, sandbox, "1.8", workerWindow.location.href, 1);
+ } catch (e) {
+ Cu.reportError("Error evaluating worker script for " + workerName + ": " + e + "; " +
+ (e.lineNumber ? ("Line #" + e.lineNumber) : "") +
+ (e.stack ? ("\n" + e.stack) : ""));
+ return;
+ }
+ // so finally we are ready to roll - dequeue all the pending connects
+ workerInfo.loaded = true;
+ // save the sandbox somewhere convenient before we connect.
+ frame.sandbox = sandbox;
+ let pending = workerInfo.pendingPorts;
+ log("worker window " + workerName + " loaded - connecting " + pending.length + " ports");
+ while (pending.length) {
+ let port = pending.shift();
+ if (port._portid) { // may have already been closed!
+ try {
+ port._createWorkerAndEntangle(workerInfo);
+ }
+ catch(e) {
+ Cu.reportError("Failed to create worker port: " + e + "\n" + e.stack);
+ }
+ }
+ }
+
+ }, true);
+ }
+ catch(e) {
+ Cu.reportError("frameworker unable to inject for "+doc.location + " ("+ e + ")");
+ }
+ };
+ Services.obs.addObserver(injectController, 'document-element-inserted', false);
+ let doc = hiddenDOMWindow.document;
+ let container = doc.body ? doc.body : doc.documentElement;
+ container.appendChild(frame);
+ }
+ else {
+ // already have a worker - either queue or make the connection.
+ if (workerInfo.loaded) {
+ try {
+ clientPort._createWorkerAndEntangle(workerInfo);
+ }
+ catch (ex) {
+ Cu.reportError("Failed to connect a port: " + e + "\n" + e.stack);
+ }
+ }
+ else {
+ workerInfo.pendingPorts.push(clientPort);
+ }
+ }
+
+ // return the pseudo worker object.
+ // XXX - workers have no .close() method, but *do* have a .terminate()
+ // method which we should implement. However, the worker spec doesn't define
+ // a callback to be made in the worker when this happens - it all just dies.
+ // TODO: work out a sane impl for 'terminate'.
+ function terminate() {
+ log("worker at " + workerName + " terminating");
+ // closing the port also removes it from workerInfo.ports, so we don't
+ // iterate over that directly, just over the port IDs.
+ for each (let portid in Object.keys(workerInfo.ports)) {
+ try {
+ workerInfo.ports[portid].close();
+ }
+ catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+ // and do the actual killing on a timeout so the pending events get
+ // delivered first.
+ workerInfo.frame.contentWindow.setTimeout(function() {
+ // now nuke the iframe itself and forget everything about this worker.
+ let appShell = Cc["@mozilla.org/appshell/appShellService;1"]
+ .getService(Ci.nsIAppShellService);
+ let hiddenDOMWindow = appShell.hiddenDOMWindow;
+ let doc = hiddenDOMWindow.document;
+ let container = doc.body ? doc.body : doc.documentElement;
+ container.removeChild(workerInfo.frame);
+ delete workerInfos[url];
+ log("worker terminated!");
+ }, 0);
+ }
+ return {port: clientPort, terminate: terminate};
+};
+
+const EXPORTED_SYMBOLS = ["FrameWorker"];
View
310 modules/manifest.jsm
@@ -0,0 +1,310 @@
+/* -*- Mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/.
+ *
+ * Contributor(s):
+ * Shane Caraveo <scaraveo@mozilla.com>
+ *
+ * Utility methods for dealing with service manifests.
+ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, manager: Cm} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://socialapi/modules/manifestDB.jsm");
+Cu.import("resource://socialapi/modules/registry.js");
+
+
+/**
+ * testSafebrowsing
+ *
+ * given a url, see if it is in our malware/phishing lists.
+ * Returns immediately, calling the callback when a result is known.
+ * Callback gets one param, the result which will be non-zero
+ * if the url is a problem.
+ *
+ * @param url string
+ * @param callback function
+ */
+function testSafebrowsing(aUrl, aCallback) {
+ // callback gets zero if the url is not found
+ // pills.ind.in produces a positive hit for a bad site
+ // http://www.google.com/safebrowsing/diagnostic?site=pills.ind.in/
+ // result is non-zero if the url is in the malware or phising lists
+ let uri = Services.io.newURI(aUrl, null, null);
+ var dbservice = Cc["@mozilla.org/url-classifier/dbservice;1"]
+ .getService(Ci.nsIUrlClassifierDBService);
+ var handler = {
+ onClassifyComplete: function(result) {
+ aCallback(result);
+ }
+ }
+ var classifier = dbservice.QueryInterface(Ci.nsIURIClassifier);
+ var result = classifier.classify(uri, handler);
+ if (!result) {
+ // the callback will not be called back, do it ourselves
+ aCallback(0);
+ }
+}
+
+
+/* Utility function: returns the host:port of
+ * of a URI, or simply the host, if no port
+ * is provided. If the URI cannot be parsed,
+ * or is a resource: URI, returns the input
+ * URI text. */
+function normalizeOriginPort(aURL) {
+ try {
+ let uri = Services.io.newURI(aURL, null, null);
+ if (uri.scheme == 'resource') return aURL;
+ return uri.hostPort;
+ }
+ catch(e) {
+ Cu.reportError(e);
+ }
+ return aURL;
+}
+
+
+/**
+ * manifestRegistry is our internal api for registering manifest files that
+ contain data for various services. It interacts with ManifestDB to
+ store a list of service manifests, keyed on domain.
+ */
+function ManifestRegistry() {
+ this._prefBranch = Services.prefs.getBranch("social.provider.").QueryInterface(Ci.nsIPrefBranch2);
+}
+
+const manifestRegistryClassID = Components.ID("{8d764216-d779-214f-8da0-80e211d759eb}");
+const manifestRegistryCID = "@mozilla.org/manifestRegistry;1";
+
+ManifestRegistry.prototype = {
+ classID: manifestRegistryClassID,
+ contractID: manifestRegistryCID,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver]),
+
+ askUserInstall: function(aWindow, aCallback, location) {
+ let origin = normalizeOriginPort(location);
+ // BUG 732263 remember if the user says no, use that as a check in
+ // discoverActivity so we bypass a lot of work.
+ let nId = "manifest-ask-install";
+ let nBox = aWindow.gBrowser.getNotificationBox();
+ let notification = nBox.getNotificationWithValue(nId);
+ let strings = Services.strings.createBundle("chrome://socialapi/locale/strings.properties");
+
+ // Check that we aren't already displaying our notification
+ if (!notification) {
+ let self = this;
+ let message = strings.GetStringFromName("installoffer.notificationbar");
+
+ buttons = [{
+ label: strings.GetStringFromName("yes.label"),
+ accessKey: null,
+ callback: function () {
+ aWindow.setTimeout(function () {
+ aCallback();
+ }, 0);
+ }
+ },
+ {
+ label: strings.GetStringFromName("dontask.label"),
+ accessKey: strings.GetStringFromName("dontask.accesskey"),
+ callback: function() {
+ self._prefBranch.setBoolPref(origin+".ignore", true);
+ }
+ }];
+ nBox.appendNotification(message, nId, null,
+ nBox.PRIORITY_INFO_MEDIUM,
+ buttons);
+ }
+ },
+
+ /**
+ * validateManifest
+ *
+ * Given the manifest data, create a clean version of the manifest. Ensure
+ * any URLs are same-origin (proto+host+port). If the manifest is a builtin,
+ * URLs must either be resource or same-origin resolved against the manifest
+ * origin. We ignore any manifest entries that are not supported.
+ *
+ * @param location string string version of manifest location
+ * @param manifest json-object raw manifest data
+ * @returns manifest json-object a cleaned version of the manifest
+ */
+ validateManifest: function manifestRegistry_validateManifest(location, rawManifest) {
+ // anything in URLEntries will require same-origin policy, though we
+ // special-case iconURL to allow icons from CDN
+ let URLEntries = ['iconURL', 'workerURL', 'sidebarURL'];
+
+ // only items in validEntries will move into our cleaned manifest
+ let validEntries = ['name'].concat(URLEntries);
+
+ // Is this a "built-in" service?
+ let builtin = location.indexOf("resource:") == 0;
+ if (builtin) {
+ // builtin manifests may have a couple other entries
+ validEntries = validEntries.concat('origin', 'contentPatchPath');
+ }
+
+ // store the location we got the manifest from and the origin.
+ let manifest = {
+ location: location
+ };
+ for (var k in rawManifest.services.social) {
+ if (validEntries.indexOf(k) >= 0) manifest[k] = rawManifest.services.social[k];
+ }
+ // we've saved original location in manifest above, switch our location
+ // temporarily so we can correctly resolve urls for our builtins. We
+ // still validate the origin defined in a builtin manifest below.
+ if (builtin && manifest.origin) {
+ location = manifest.origin;
+ }
+
+ // resolve all URLEntries against the manifest location.
+ let basePathURI = Services.io.newURI(location, null, null);
+ // full proto+host+port origin for resolving same-origin urls
+ manifest.origin = basePathURI.prePath;
+ for each(let k in URLEntries) {
+
+ if (!manifest[k]) continue;
+
+ // shortcut - resource:// URIs don't get same-origin checks.
+ if (builtin && manifest[k].indexOf("resource:") == 0) continue;
+
+ // resolve the url to the basepath to handle relative urls, then verify
+ // same-origin, we'll let iconURL be on a different origin
+ let url = basePathURI.resolve(manifest[k]);
+
+ if (k != 'iconURL' && url.indexOf(manifest.origin) != 0) {
+ throw new Error("manifest URL origin mismatch " +manifest.origin+ " != " + manifest[k] +"\n")
+ }
+ manifest[k] = url; // store the resolved version
+ }
+ return manifest;
+ },
+
+ importManifest: function manifestRegistry_importManifest(aDocument, location, rawManifest, systemInstall, callback) {
+ //Services.console.logStringMessage("got manifest "+JSON.stringify(manifest));
+ let manifest = this.validateManifest(location, rawManifest);
+
+ // we want automatic updates to the manifest entry if we change our
+ // builtin manifest files. We also want to allow the "real" provider
+ // to overwrite our builtin manifest, however we NEVER want a builtin
+ // manifest to overwrite something installed from the "real" provider
+ function installManifest() {
+ ManifestDB.get(manifest.origin, function(key, item) {
+ // dont overwrite a non-resource entry with a resource entry.
+ if (item && manifest.location.indexOf('resource:') == 0 &&
+ item.location.indexOf('resource:') != 0) {
+ // being passed a builtin and existing not builtin - ignore.
+ if (callback) {
+ callback(false);
+ }
+ return;
+ }
+ // dont overwrite enabled, but first install is always enabled
+ manifest.enabled = item ? item.enabled : true;
+ ManifestDB.put(manifest.origin, manifest);
+ registry().register(manifest);
+ if (callback) {
+ callback(true);
+ }
+ });
+ }
+
+ if (systemInstall) {
+ // user approval has already been granted, or this is an automatic operation
+ installManifest();
+ }
+ else {
+ // we need to ask the user for confirmation:
+ var xulWindow = aDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ this.askUserInstall(xulWindow, function() {
+ installManifest();
+
+ // user requested install, lets make sure we enable after the install.
+ // This is especially important on first time install.
+
+ registry().enabled = true;
+ let prefBranch = Services.prefs.getBranch("social.provider.").QueryInterface(Ci.nsIPrefBranch2);
+ prefBranch.setBoolPref("visible", true);
+ Services.obs.notifyObservers(null,
+ "social-browsing-enabled",
+ registry().currentProvider.origin);
+ }, location)
+ return;
+ }
+ },
+
+ _checkManifestSecurity: function(channel) {
+ // this comes from https://developer.mozilla.org/En/How_to_check_the_security_state_of_an_XMLHTTPRequest_over_SSL
+ // although we are more picky about things (ie, secInfo MUST be a nsITransportSecurityInfo and a nsISSLStatusProvider)
+ let secInfo = channel.securityInfo;
+ if (!(secInfo instanceof Ci.nsITransportSecurityInfo) || ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_SECURE) != Ci.nsIWebProgressListener.STATE_IS_SECURE)) {
+ Cu.reportError("Attempt to load social service from insecure location (manifest securityState is not secure)");
+ return false;
+ }
+ if (!(secInfo instanceof Ci.nsISSLStatusProvider)) {
+ Cu.reportError("Attempt to load social service from insecure location (manifest host has no SSLStatusProvider)");
+ return false;
+ }
+ let cert = secInfo.QueryInterface(Ci.nsISSLStatusProvider)
+ .SSLStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
+ let verificationResult = cert.verifyForUsage(Ci.nsIX509Cert.CERT_USAGE_SSLServer);
+ if (verificationResult != Ci.nsIX509Cert.VERIFIED_OK) {
+ Cu.reportError("Attempt to load social service from insecure location (SSL status of the manifest host is invalid)");
+ return false;
+ }
+ return true;
+ },
+
+ loadManifest: function manifestRegistry_loadManifest(aDocument, url, systemInstall, callback) {
+ // test any manifest against safebrowsing
+ let self = this;
+ testSafebrowsing(url, function(result) {
+ if (result != 0) {
+ Cu.reportError("Attempt to load social service from unsafe location (safebrowsing result: ["+result+"] "+url + ")");
+ if (callback) callback(false);
+ return;
+ }
+
+ // BUG 732264 error and edge case handling
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ xhr.open('GET', url, true);
+ xhr.onreadystatechange = function(aEvt) {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200 || xhr.status == 0) {
+
+ // We implicitly trust resource:// manifest origins.
+ let needSecureManifest = !isDevMode() && url.indexOf("resource://") != 0;
+ if (needSecureManifest && !self._checkManifestSecurity(xhr.channel)) {
+ if (callback) callback(false);
+ return;
+ }
+ try {
+ self.importManifest(aDocument, url, JSON.parse(xhr.responseText), systemInstall, callback);
+ }
+ catch(e) {
+ Cu.reportError("Error while loading social service manifest from "+url+": "+e);
+ if (callback) callback(false);
+ }
+ }
+ else {
+ Cu.reportError("Error while loading social service manifest from " + url + ": status "+xhr.status);
+ }
+ }
+ };
+ xhr.send(null);
+ });
+ }
+};
+
+const manifestSvc = new ManifestRegistry();
+const EXPORTED_SYMBOLS = ['manifestSvc'];
View
96 modules/manifestDB.jsm
@@ -0,0 +1,96 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/.
+ *
+ * Contributor(s):
+ * Michael Hanson <mhanson@mozilla.com>
+ * Dan Walkowski <dwalkowski@mozilla.com>
+ * Anant Narayanan <anant@kix.in>
+ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://socialapi/modules/typedStorage.jsm");
+
+// a lightweight wrapper around TypedStorage to handle simple validation
+// of origin keys and manifest data
+var ManifestDB = (function() {
+ var typedStorage = TypedStorage();
+ var storage = typedStorage.open("manifest", "destiny");
+
+ // get the host+port of the url to use as a db key
+ function normalizeKey(aURL) {
+ let URI = Services.io.newURI(aURL, null, null);
+ if (URI.port > 0)
+ return URI.host+":"+URI.port;
+ return URI.host;
+ }
+
+ /**
+ * add
+ *
+ * @param origin url origin of the manifest
+ * @param manifest manifest record (js object)
+ * @param cb callback function
+ */
+ function put(origin, manifest, cb) {
+ // TODO validate the manifest now? what do we validate?
+ manifest.last_modified = new Date().getTime();
+ storage.put(normalizeKey(origin), manifest, cb);
+ }
+
+ function insert(origin, manifest, cb) {
+ // TODO validate the manifest now? what do we validate?
+ manifest.last_modified = new Date().getTime();
+ storage.insert(normalizeKey(origin), manifest, cb);
+ }
+
+ function remove(origin, cb) {
+ var self = this;
+ let originKey = normalizeKey(origin);
+ storage.get(originKey, function(key, item) {
+ if (!item) {
+ if (cb) cb(false);
+ }
+ else {
+ storage.remove(key, function() {
+ if (cb) cb(true);
+ });
+ }
+ });
+ }
+
+ function get(origin, cb) {
+ storage.get(normalizeKey(origin), cb);
+ }
+
+ function iterate(cb) {
+ storage.iterate(cb);
+ }
+
+ function close() {
+ storage.close();
+ }
+
+ // an observer to ensure we shutdown the database, else debug builds assert.
+ Services.obs.addObserver({
+ observe: function(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(this, "quit-application-granted");
+ close();
+ }
+ }, "quit-application-granted", false);
+
+ return {
+ insert: insert,
+ iterate: iterate,
+ put: put,
+ remove: remove,
+ get: get,
+ close: close
+ };
+})();
+
+var EXPORTED_SYMBOLS = ["ManifestDB"];
View
359 modules/registry.js
@@ -0,0 +1,359 @@
+/* -*- Mode: JavaScript; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/.
+ *
+ * Contributor(s):
+ * Michael Hanson <mhanson@mozilla.com>
+ * Edward Lee <edilee@mozilla.com>
+ * Mark Hammond <mhammond@mozilla.com>
+ * Shane Caraveo <scaraveo@mozilla.com>
+ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, manager: Cm} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://socialapi/modules/defaultprefs.js");
+Cu.import("resource://socialapi/modules/manifestDB.jsm");
+//Cu.import("resource://socialapi/modules/defaultServices.jsm");
+
+const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const FRECENCY = 100;
+
+/** Helper function to detect "development mode",
+ * which is set with the social.provider.devmode pref.
+ *
+ * When "devmode" is set, service URLs can be served
+ * domains other than the manifest's origin.
+ */
+function isDevMode() {
+ let prefBranch = Services.prefs.getBranch("social.provider.").QueryInterface(Ci.nsIPrefBranch2);
+ let enable_dev = false;
+ try {
+ enable_dev = prefBranch.getBoolPref("devmode");
+ } catch(e) {}
+ return enable_dev;
+}
+
+function log(msg) {
+ // dump(msg);
+}
+
+const providerRegistryClassID = Components.ID("{1a60fb78-b2d2-104b-b16a-7f497be5626d}");
+const providerRegistryCID = "@mozilla.org/socialProviderRegistry;1";
+
+function ProviderRegistry(createCallback) {
+ log("social registry service initializing\n");
+ this.createProviderCallback = createCallback;
+ this._prefBranch = Services.prefs.getBranch("social.provider.").QueryInterface(Ci.nsIPrefBranch2);
+
+ Services.obs.addObserver(this, "private-browsing", false);
+ Services.obs.addObserver(this, 'quit-application', true);
+
+ let self = this;
+ ManifestDB.iterate(function(key, manifest) {
+ self.register(manifest);
+ });
+
+ // developer?
+ let enable_dev = isDevMode();
+
+ // we need to have our service injector running on startup of the
+ // registry
+ this.injectController = function(doc, topic, data) {
+ try {
+ // if we have attached 'service' on to the social-browser for the window
+ // then we'll continue our injection.
+ if (!doc.defaultView) return;
+ var xulWindow = doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ // our service windows simply have browser attached to them
+ var sbrowser = xulWindow.document.getElementById("social-status-sidebar-browser") || xulWindow.browser;
+ var panelbrowser = xulWindow.document.getElementById("social-notification-browser");
+ if (panelbrowser && panelbrowser.contentDocument == doc) sbrowser = panelbrowser;
+
+ if (sbrowser && sbrowser.contentDocument == doc) {
+ let service = sbrowser.service? sbrowser.service : xulWindow.service;
+ if (service.workerURL) {
+ service.attachToWindow(doc.defaultView);
+ }
+ } else
+ if (enable_dev) {
+ // XXX dev code, allows us to load social panels into tabs and still
+ // call attachToWindow on them
+ for each(let svc in this._providers) {
+ if ((doc.location+"").indexOf(svc.origin) == 0) {
+ svc.attachToWindow(doc.defaultView);
+ break;
+ }
+ };
+ }
+ }
+ catch(e) {
+ Cu.reportError("unable to attachToWindow for "+doc.location+":" + e);
+ }
+ };
+ Services.obs.addObserver(this.injectController.bind(this), 'document-element-inserted', false);
+}
+ProviderRegistry.prototype = {
+ classID: providerRegistryClassID,
+ contractID: providerRegistryCID,
+ QueryInterface: XPCOMUtils.generateQI([Ci.mozISocialRegistry,
+ Ci.nsISupportsWeakReference,
+ Ci.nsIObserver]),
+
+ _providers: {}, // a list of installed social providers
+ _currentProvider: null,
+ _enabled: null,
+ _enabledBeforePrivateBrowsing: false,
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "private-browsing") {
+ if (aData == "enter") {
+ this._enabledBeforePrivateBrowsing = this.enabled;
+ this.enabled = false;
+ } else if (aData == "exit") {
+ this.enabled = this._enabledBeforePrivateBrowsing;
+ }
+ }
+ else if (aTopic == 'quit-application') {
+ this.each(function(provider) {
+ provider.shutdown();
+ })
+ }
+ },
+
+ /**
+ * register
+ *
+ * registers a provider manifest that is installed into the database. These
+ * are available social providers, whether enabled or not.
+ *
+ * @param manifest jsonObject
+ */
+ register: function(manifest) {
+ // we are not pushing into manifestDB here, rather manifestDB is calling us
+ try {
+ let provider = this.createProviderCallback(manifest);
+ this._providers[manifest.origin] = provider;
+ // registration on startup could happen in any order, so we avoid
+ // setting this as "current".
+ // notify manifest changed so listeners can pick up new providers (e.g.
+ // about:social)
+ Services.obs.notifyObservers(null, "social-service-manifest-changed", manifest.origin);
+ }
+ catch(e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * remove
+ *
+ * remove a provider from the registry and the database
+ *
+ * @param origin string scheme+host+port
+ * @param callback function
+ */
+ remove: function(origin, callback) {
+ this.disableProvider(origin);
+ try {
+ delete this._providers[origin];
+ } catch (ex) {
+ Cu.reportError("attempting to remove a non-existing manifest origin: " + origin);
+ }
+ ManifestDB.remove(origin, function() {
+ Services.obs.notifyObservers(null, "social-service-manifest-changed", origin);
+ if (callback) callback();
+ });
+ },
+
+ _findCurrentProvider: function() {
+ // workout what provider should be current.
+ if (!this.enabled) {
+ throw new Error("_findCurrentProvider should not be called when disabled.");
+ }
+ let origin = this._prefBranch.getCharPref("current");
+ if (origin && this._providers[origin] && this._providers[origin].enabled) {
+ return this._providers[origin];
+ }
+ // can't find it based on our prefs - just select any enabled one.
+ for each(let provider in this._providers) {
+ if (provider.enabled) {
+ // this one will do.
+ return provider;
+ }
+ }
+ // should be impossible to get here; our enabled state should be false
+ // if there are none we can select.
+ return null;
+ },
+ get currentProvider() {
+ // no concept of a "current" provider when we are disabled.
+ if (!this.enabled) {
+ return null;
+ }
+ return this._currentProvider;
+ },
+ set currentProvider(provider) {
+ if (provider && !provider.enabled) {
+ throw new Error("cannot set disabled provider as the current provider");
+ }
+ this._currentProvider = provider;
+ try {
+ this._prefBranch.setCharPref("current", provider.origin);
+ }
+ catch(e) {
+ // just during dev, otherwise we shouldn't log here
+ Cu.reportError(e);
+ }
+ Services.obs.notifyObservers(null,
+ "social-browsing-current-service-changed",
+ provider.origin);
+ },
+ get: function pr_get(origin) {
+ return this._providers[origin];
+ },
+ each: function pr_iterate(cb) {
+ for each(let provider in this._providers) {
+ //cb.handle(provider);
+ cb(provider);
+ }
+ },
+ enableProvider: function(origin, callback) {
+ let provider = this._providers[origin];
+ if (!provider) {
+ return false;
+ }
+
+ // Don't start up again if we're already enabled
+ if (provider.enabled) return true;
+
+ ManifestDB.get(origin, function(key, manifest) {
+ manifest.enabled = true;
+ ManifestDB.put(origin, manifest);
+ Services.obs.notifyObservers(null, "social-service-manifest-changed", origin);
+ if (callback) callback();
+ });
+ provider.enabled = true;
+ // if browsing is disabled we can't activate it!
+ // XXX - but checking "this.enabled" will have the side-effect of actually
+ // starting things up, which may be too early! So use the private ._enabled.
+ if (this._enabled) {
+ provider.activate();
+ }
+ // nothing else to do - it is now available to be the current provider
+ // but doesn't get that status simply because it was enabled.
+ return true;
+ },
+ disableProvider: function(origin, callback) {
+ let provider = this._providers[origin];
+ if (!provider) {
+ return false;
+ }
+
+ provider.shutdown();
+ provider.enabled = false;
+ // and update the manifest.
+ // XXX - this is wrong! We should track that state elsewhere, otherwise
+ // a manifest being updated by a provider loses this state!
+ ManifestDB.get(origin, function(key, manifest) {
+ manifest.enabled = false;
+ ManifestDB.put(origin, manifest);
+ Services.obs.notifyObservers(null, "social-service-manifest-changed", origin);
+ if (callback) callback();
+ });
+
+ if (this._currentProvider && this._currentProvider == provider) {
+ // it was current select a new current one.
+ this._currentProvider = null;
+ // however, if this was the last enabled service, then we must disable
+ // social browsing completely.
+ let numEnabled = 0;
+ for each(let look in this._providers) {
+ if (look.enabled) {
+ numEnabled += 1;
+ }
+ }
+ if (numEnabled == 0) {
+ log("provider disabled and no others are enabled - disabling social\n")
+ this.enabled = false;
+ } else {
+ // don't call this.currentProvider as we don't want to set the pref!
+ this._currentProvider = this._findCurrentProvider();
+ Services.obs.notifyObservers(null,
+ "social-browsing-current-service-changed",
+ this._currentProvider.origin);
+ }
+ }
+ return true;
+ },
+
+ // the rest of these methods are misplaced and should be in a generic
+ // "social service" rather than the registry - but this will do for now
+ // The global state of whether social browsing is enabled or not.
+ get enabled() {
+ if (this._enabled === null) {
+ this.enabled = this._prefBranch.getBoolPref("enabled");
+ }
+ return this._enabled;
+ },
+ set enabled(new_state) {
+ log("registry set enabled " + new_state + " (current state is " + this._enabled + ")\n");
+ if (new_state == this._enabled) {
+ return;
+ }
+
+ // XXX for now, we don't allow enabling social browsing during private browsing
+ if (new_state && Components.classes["@mozilla.org/privatebrowsing;1"]
+ .getService(Components.interfaces.nsIPrivateBrowsingService)
+ .privateBrowsingEnabled)
+ return;
+
+ this._enabled = new_state; // set early so later .enabled requests don't recurse.
+ if (new_state) {
+ for each(let provider in this._providers) {
+ provider.activate();
+ }
+ let current = this._findCurrentProvider();
+ if (current == null) {
+ log("attempted to enable browsing but no providers available\n");
+ this._enabled = false;
+ return;
+ }
+ // Set the current provider so anyone who asks as a result of the
+ // social-browsing-enabled gets the right answer, but don't broadcast
+ // about the new default until after,
+ this._currentProvider = current;
+ Services.obs.notifyObservers(null, "social-browsing-enabled", null);
+ Services.obs.notifyObservers(null, "social-browsing-current-service-changed", null);
+ } else {
+ for each(let provider in this._providers) {
+ provider.shutdown();
+ }
+ this._currentProvider = null;
+ Services.obs.notifyObservers(null, "social-browsing-disabled", null);
+ }
+ this._prefBranch.setBoolPref("enabled", new_state);
+ },
+}
+
+//const components = [ProviderRegistry];
+//const NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
+let providerRegistrySingleton;
+
+function initialize(createCallback) {
+ if (providerRegistrySingleton) {
+ throw new Error("already initialized");
+ }
+ providerRegistrySingleton = new ProviderRegistry(createCallback);
+}
+
+function registry() providerRegistrySingleton;
+const EXPORTED_SYMBOLS = ["registry", "initialize", "isDevMode"];
View
191 modules/typedStorage.jsm
@@ -0,0 +1,191 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* 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/.
+ */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function TypedStorageImpl() {}
+TypedStorageImpl.prototype = {
+ open: function(objType, dbName) {
+ return new ObjectStore(objType, dbName);
+ }
+};
+
+function ObjectStore(objType, dbName) {
+ let file = Services.dirsvc.
+ get("ProfD", Ci.nsIFile);
+ file.append(dbName + ".sqlite");
+
+ // Will also create the file if it does not exist
+ let dbConn = Services.storage.openDatabase(file);
+ this._dbConn = dbConn;
+
+ // See if the table is already created:
+ let statement;
+ let tableExists = false;
+ try {
+ statement = dbConn.createStatement("SELECT * FROM " + objType + " LIMIT 1");
+ statement.executeStep();
+ tableExists = true;
+ }
+ catch (e) {}
+ finally {
+ if (statement) statement.finalize();
+ }
+
+ if (!tableExists) {
+ try {
+ dbConn.executeSimpleSQL("CREATE TABLE " + objType + " (id INTEGER PRIMARY KEY, key TEXT UNIQUE NOT NULL, data TEXT)");
+ }
+ catch (e) {
+ Cu.reportError("Error while creating table: " + e);
+ throw e;
+ }
+ }
+
+ this._objType = objType;
+}
+ObjectStore.prototype = {
+ get: function(key, cb) {
+ let self = this;
+ let value;
+ let getStatement = this._dbConn.createStatement("SELECT data FROM " + this._objType + " WHERE key = :key LIMIT 1");
+ getStatement.params.key = key;
+ getStatement.executeAsync({
+ handleResult: function(result) {
+ let row = result.getNextRow();
+ if (row) {
+ value = JSON.parse(row.getResultByName("data"));
+ }
+ },
+ handleError: function(error) {
+ Cu.reportError("Error while selecting from table " + self._objType + ": " + error + "; " + self._dbConn.lastErrorString + " (" + this._dbConn.lastError + ")");
+ },
+ handleCompletion: function(reason) {
+ getStatement.reset();
+ if (reason != Ci.mozIStorageStatementCallback.REASON_FINISHED)
+ Cu.reportError("Get query canceled or aborted! " + reason);
+ else {
+ if (cb) cb(key, value);
+ }
+ }
+ });
+ },
+
+ insert: function(key, value, cb) {
+ let setStatement = this._dbConn.createStatement("INSERT INTO " + this._objType + " (key, data) VALUES (:key, :data )");
+ setStatement.params.key = key;
+ setStatement.params.data = JSON.stringify(value);
+ this._doAsyncExecute(setStatement, cb);
+ },
+
+ put: function(key, value, cb) {
+ let setStatement = this._dbConn.createStatement("INSERT OR REPLACE INTO " + this._objType + " (key, data) VALUES (:key, :data )");
+ setStatement.params.key = key;
+ setStatement.params.data = JSON.stringify(value);
+ this._doAsyncExecute(setStatement, cb);
+ },
+
+ remove: function(key, cb) {
+ let removeStatement = this._dbConn.createStatement("DELETE FROM " + this._objType + " WHERE key = :key");
+ removeStatement.params.key = key;
+ this._doAsyncExecute(removeStatement, cb);
+ },
+
+ clear: function(cb) {
+ let clearStatement = this._dbConn.createStatement("DELETE FROM " + this._objType);
+ this._doAsyncExecute(clearStatement, cb);
+ },
+
+ has: function(key, cb) {
+ this.get(key, function(key, data) {
+ cb(data !== null);
+ })
+ },
+
+ keys: function(cb) {
+ let resultKeys = [];
+ let keyStatement = this._dbConn.createStatement("SELECT key FROM " + this._objType);
+
+ let self = this;
+ keyStatement.executeAsync({
+ handleResult: function(result) {
+ let row;
+ while ((row = result.getNextRow())) {
+ resultKeys.push(row.getResultByName("key"));
+ }
+ },
+ handleError: function(error) {
+ Cu.reportError("Error while getting keys for " + self._objType + ": " + error + "; " + self._dbConn.lastErrorString + " (" + self._dbConn.lastError + ")");
+ },
+ handleCompletion: function(reason) {
+ keyStatement.reset();
+ if (reason != Ci.mozIStorageStatementCallback.REASON_FINISHED)
+ Cu.reportError("Keys query canceled or aborted! " + reason);
+ else {
+ try {
+ cb(resultKeys);
+ }
+ catch (e) {
+ Cu.reportError("Error in completion callback for ObjectStore.keys(): " + e);
+ }
+ }
+ }
+ });
+ },
+
+ iterate: function(cb) {
+ // sometimes asynchronous calls can make your head hurt
+ let store = this;
+ this.keys(function(allKeys) {
+ for each(let key in allKeys) {
+ store.get(key, function(k, values) {
+ let result = cb(k, values);
+ if (result === false) return;
+ });
+ }
+ });
+ },
+
+ // Helper function for async execute with no results
+ _doAsyncExecute: function(statement, cb) {
+ let self = this;
+ statement.executeAsync({
+ handleResult: function(result) {},
+ handleError: function(error) {
+ Cu.reportError("Error while executing "+ statement+ "on"+ this._objType+ ":"+ error.message, error.result);
+ if (this._dbConn) {
+ Cu.reportError("database error details:"+ this._dbConn.lastErrorString+ "(" + this._dbConn.lastError + ")")
+ }
+ },
+ handleCompletion: function(reason) {
+ statement.reset();
+ if (reason != Ci.mozIStorageStatementCallback.REASON_FINISHED)
+ Cu.reportError("Query canceled or aborted! " + reason);
+ else {
+ if (cb) cb(true);
+ }
+ }
+ });
+ },
+
+ close: function() {
+ if (this._dbConn) {
+ this._dbConn.asyncClose();
+ this._dbConn = null;
+ }
+ }
+};
+
+// We create a Singleton
+var TypedStorageImplSingleton = new TypedStorageImpl();
+
+function TypedStorage() {
+ return TypedStorageImplSingleton;
+}
+var EXPORTED_SYMBOLS = ["TypedStorage"];
View
31 test/Makefile-branch-default.in
@@ -0,0 +1,31 @@
+# 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/.
+
+DEPTH = ../../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = browser/features/socialapi/test
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
+
+_BROWSER_TEST_FILES = \
+ head.js \
+ browser_frameworker.js \
+ browser_manifest.js \
+ browser_registry.js \
+
+ $(NULL)
+
+_BROWSER_TEST_PROVIDER_FILES = \
+ testprovider/app.manifest \
+ $(NULL)
+
+libs:: $(_BROWSER_TEST_FILES)
+ $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
+
+libs:: $(_BROWSER_TEST_PROVIDER_FILES)
+ $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)/testprovider
View
243 test/browser_frameworker.js
@@ -0,0 +1,243 @@
+let modules = {};
+Cu.import("resource://socialapi/modules/frameworker.js", modules);
+
+function makeWorkerUrl(runner) {
+ return "data:application/javascript," + encodeURI("let run=" + runner.toSource()) + ";run();"
+}
+
+function test() {
+ runTests(tests);
+}
+
+let tests = {
+ testSimple: function(cbnext) {
+ let run = function() {
+ onconnect = function(e) {
+ let port = e.ports[0];
+ port.onmessage = function(e) {
+ if (e.data.topic == "ping") {
+ port.postMessage({topic: "pong"});
+ }
+ }
+ }
+ }
+
+ let worker = modules.FrameWorker(makeWorkerUrl(run), undefined, "testSimple");
+ worker.port.onmessage = function(e) {
+ if (e.data.topic == "pong") {
+ worker.terminate();
+ cbnext();
+ }
+ }
+ worker.port.postMessage({topic: "ping"})
+ },
+
+ // when the client closes early but the worker tries to send anyway...
+ testEarlyClose: function(cbnext) {
+ let run = function() {
+ onconnect = function(e) {
+ let port = e.ports[0];
+ port.postMessage({topic: "oh hai"});
+ }
+ }
+
+ let worker = modules.FrameWorker(makeWorkerUrl(run), undefined, "testEarlyClose");
+ worker.port.close();
+ worker.terminate();
+ cbnext();
+ },
+
+ // Check we do get a social.port-closing message as the port is closed.
+ testPortClosingMessage: function(cbnext) {
+ // We use 2 ports - we close the first and report success via the second.
+ let run = function() {
+ let firstPort, secondPort;
+ onconnect = function(e) {
+ let port = e.ports[0];
+ if (firstPort === undefined) {
+ firstPort = port;
+ port.onmessage = function(e) {
+ if (e.data.topic == "social.port-closing") {
+ secondPort.postMessage({topic: "got-closing"});
+ }
+ }
+ } else {
+ secondPort = port;
+ // now both ports are connected we can trigger the client side
+ // closing the first.
+ secondPort.postMessage({topic: "connected"});
+ }
+ }
+ }
+ let workerurl = makeWorkerUrl(run);
+ let worker1 = modules.FrameWorker(workerurl, undefined, "testPortClosingMessage worker1");
+ let worker2 = modules.FrameWorker(workerurl, undefined, "testPortClosingMessage worker2");
+ worker2.port.onmessage = function(e) {
+ if (e.data.topic == "connected") {
+ // both ports connected, so close the first.
+ worker1.port.close();
+ } else if (e.data.topic == "got-closing") {
+ worker2.terminate();
+ cbnext();
+ }
+ }
+ },
+
+ // Tests that prototypes added to core objects work with data sent over
+ // the message ports.
+ testPrototypes: function(cbnext) {
+ let run = function() {
+ // Modify the Array prototype...
+ Array.prototype.customfunction = function() {};
+ onconnect = function(e) {
+ let port = e.ports[0];
+ port.onmessage = function(e) {
+ // Check the data we get via the port has the prototype modification
+ if (e.data.topic == "hello" && e.data.data.customfunction) {
+ port.postMessage({topic: "hello", data: [1,2,3]});
+ }
+ }
+ }
+ }
+ // hrmph - this kinda sucks as it is really just testing the actual
+ // implementation rather than the end result, but is OK for now.
+ // Really we are just testing that JSON.parse in the client window
+ // is called.
+ let fakeWindow = {
+ JSON: {
+ parse: function(s) {
+ let data = JSON.parse(s);
+ data.data.somextrafunction = function() {};
+ return data;
+ }
+ }
+ }
+ let worker = modules.FrameWorker(makeWorkerUrl(run), fakeWindow, "testPrototypes");
+ worker.port.onmessage = function(e) {
+ if (e.data.topic == "hello" && e.data.data.somextrafunction) {
+ worker.terminate();
+ cbnext();
+ }
+ }
+ worker.port.postMessage({topic: "hello", data: [1,2,3]});
+ },
+
+ testArray: function(cbnext) {
+ let run = function() {
+ // Modify the Array prototype...
+ Array.prototype.customfunction = function() {};
+ onconnect = function(e) {
+ let port = e.ports[0];
+ port.onmessage = function(e) {
+ // Check the data we get via the port has the prototype modification
+ if (e.data.topic == "go") {
+ let buffer = new ArrayBuffer(10);
+ // this one has always worked in the past, but worth checking anyway...
+ if (new Uint8Array(buffer).length != 10) {
+ port.postMessage({topic: "result", reason: "first length was not 10"});
+ return;
+ }
+ let reader = new FileReader();
+ reader.onload = function(event) {
+ if (new Uint8Array(buffer).length != 10) {
+ port.postMessage({topic: "result", reason: "length in onload handler was not 10"});
+ return;
+ }
+ // all seems good!
+ port.postMessage({topic: "result", reason: "ok"});
+ }
+ let blob = new Blob([buffer], {type: "binary"});
+ reader.readAsArrayBuffer(blob);
+ }
+ }
+ }
+ }
+ let worker = modules.FrameWorker(makeWorkerUrl(run), undefined, "testArray");
+ worker.port.onmessage = function(e) {
+ if (e.data.topic == "result") {
+ is(e.data.reason, "ok", "check the array worked");
+ worker.terminate();
+ cbnext();
+ }
+ }
+ worker.port.postMessage({topic: "go"});
+ },
+
+ testXHR: function(cbnext) {
+ let run = function() {
+ onconnect = function(e) {
+ let port = e.ports[0];
+ let req;
+ try {
+ req = new XMLHttpRequest();
+ } catch(e) {
+ port.postMessage({topic: "done", result: "FAILED to create XHR object, " + e.toString() });
+ }
+ if (req === undefined) { // until bug 756173 is fixed...
+ port.postMessage({topic: "done", result: "FAILED to create XHR object"});
+ return;
+ }
+ // read a URL from our test provider so it is in the same origin as the worker.
+ // might as well just read the manifest!
+ // XXX - THIS IS WRONG! it should work with this URL as it is in our origin!!!
+ // but we get a .status of 0, which implies CORS is killing us.
+ // req.open("GET", "http://mochi.test:8888/browser/browser/features/socialapi/test/testprovider/app.manifest", true);
+ req.open("GET", "http://enable-cors.org/", true);
+ req.onreadystatechange = function() {
+ if (req.readyState === 4) {
+ dump("XHR: req.status " + req.status + "\n");
+ let ok = req.status == 200 && req.responseText.length > 0;
+ if (ok) {
+ // check we actually got something sane...
+ try {
+ let data = JSON.parse(req.responseText);
+ ok = "services" in data && "social" in data.services;
+ } catch(e) {
+ ok = e.toString();
+ }
+ }
+ port.postMessage({topic: "done", result: ok});
+ }
+ }
+ req.send(null);
+ }
+ }
+ let worker = modules.FrameWorker(makeWorkerUrl(run), undefined, "testXHR");
+ worker.port.onmessage = function(e) {
+ if (e.data.topic == "done") {
+ todo_is(e.data.result, "ok", "check the xhr test worked");
+ worker.terminate();
+ cbnext();
+ }
+ }
+ },
+
+ testSameOriginImport: function(cbnext) {
+ let run = function() {
+ onconnect = function(e) {
+ let port = e.ports[0];
+ port.onmessage = function(e) {
+ if (e.data.topic == "ping") {
+ try {
+ importScripts("http://foo.bar/error");
+ } catch(ex) {
+ port.postMessage({topic: "pong", data: ex});
+ return;
+ }
+ port.postMessage({topic: "pong", data: null});
+ }
+ }
+ }
+ }
+
+ let worker = modules.FrameWorker(makeWorkerUrl(run), undefined, "testSameOriginImport");
+ worker.port.onmessage = function(e) {
+ if (e.data.topic == "pong") {
+ isnot(e.data.data, null, "check same-origin applied to importScripts");
+ worker.terminate();
+ cbnext();
+ }
+ }
+ worker.port.postMessage({topic: "ping"})
+ }
+}
View
145 test/browser_manifest.js
@@ -0,0 +1,145 @@
+Cu.import("resource://gre/modules/Services.jsm");
+let modules = {} // work around the test framework complaining of leaks
+Cu.import("resource://socialapi/modules/registry.js", modules);
+Cu.import("resource://socialapi/modules/manifest.jsm", modules);
+
+function registry() modules.registry();
+
+function test() {
+ runTests(tests);
+}
+
+function doValidationTest(location, rawManifest, cb) {
+ let r = registry();
+ let origin = Services.io.newURI(location, null, null).prePath;
+ try {
+ let manifest = modules.manifestSvc.validateManifest(location, rawManifest);
+ cb(manifest);
+ } catch(e) {
+ info("validation exception "+e.toString());
+ cb(undefined);
+ }
+}
+
+function doInstallTest(location, rawManifest, cb) {
+ try {
+ modules.manifestSvc.importManifest(null, location, rawManifest, true, cb);
+ } catch(e) {
+ info("install exception "+e.toString());
+ cb(undefined);
+ }
+}
+
+let tests = {
+ testManifestOK: function (cbnext) {
+ doValidationTest("https://example.com/app.manifest",{
+ "services": {
+ "social": {
+ "name": "Test GOOD Provider",
+ "iconURL": "icon.png",
+ "workerURL": "worker.js",
+ "sidebarURL": "sidebar.htm",
+ }
+ }
+ }, function callback(manifest) {
+ isnot(manifest, undefined, "manifest validated");
+ cbnext();
+ });
+ },
+ testManifestSameOriginFail: function (cbnext) {
+ doValidationTest("https://example.com/app.manifest",{
+ "services": {
+ "social": {
+ "name": "Test BAD Provider",
+ "iconURL": "icon.png",
+ "workerURL": "http://example.com/worker.js",
+ "sidebarURL": "http://example.com/sidebar.htm",
+ }
+ }
+ }, function callback(manifest) {
+ is(manifest, undefined, "manifest validation with bad origin should fail");
+ cbnext();
+ });
+ },
+ testManifestBuiltinOverwriteOK: function (cbnext) {
+ doInstallTest("resource://socialapi/app.manifest",{
+ "services": {
+ "social": {
+ "name": "Test Overwrite Provider",
+ "iconURL": "icon.png",
+ "workerURL": "https://example.com/worker.js",
+ "sidebarURL": "https://example.com/sidebar.htm",
+ "origin": "https://example.com"
+ }
+ }
+ },
+ function callback(success) {
+ // now try to overwrite it with a builtin
+ doInstallTest("https://example.com/app.manifest",{
+ "services": {
+ "social": {
+ "name": "Test Good Provider",
+ "iconURL": "icon.png",
+ "workerURL": "https://example.com/worker.js",
+ "sidebarURL": "https://example.com/sidebar.htm",
+ }
+ }
+ },
+ function callback(success) {
+ is(success, true, "overwriting builtin manifest with remote should work");
+ removeProvider("https://example.com", function() {
+ cbnext();
+ });
+ });
+ });
+ },
+ testManifestBuiltinOverwriteFail: function (cbnext) {
+ doInstallTest("https://example.com/app.manifest",{
+ "services": {
+ "social": {
+ "name": "Test Good Provider",
+ "iconURL": "icon.png",
+ "workerURL": "https://example.com/worker.js",
+ "sidebarURL": "https://example.com/sidebar.htm",
+ }
+ }
+ },
+ function callback(success) {
+ // now try to overwrite it with a builtin
+ doInstallTest("resource://socialapi/app.manifest",{
+ "services": {
+ "social": {
+ "name": "Test Overwrite Provider",
+ "iconURL": "icon.png",
+ "workerURL": "https://example.com/worker.js",
+ "sidebarURL": "https://example.com/sidebar.htm",
+ "origin": "https://example.com"
+ }
+ }
+ },
+ function callback(success) {
+ is(success, false, "overwriting remote manifest with builtin should fail");
+ removeProvider("https://example.com", function() {
+ cbnext();
+ });
+ });
+ });
+ },
+ testManifestLoad: function(cbnext) {
+ //// XXX - disabled for now.
+ //todo(false, "need a test provider manifest!");
+ //cbnext();
+ //return;
+ // XXX
+ let url = TEST_PROVIDER_MANIFEST;
+ let r = modules.registry();
+ modules.manifestSvc.loadManifest(null, url, true, function() {
+ let origin = Services.io.newURI(url, null, null).prePath;
+ let provider = r.get(origin);
+ isnot(provider, undefined, "manifest loading via XHR");
+ removeProvider(origin, function() {
+ cbnext();
+ });
+ });
+ }
+};
View
132 test/browser_registry.js
@@ -0,0 +1,132 @@
+let registryModule = {} // work around the test framework complaining of leaks
+Cu.import("resource://socialapi/modules/registry.js", registryModule);
+Cu.import("resource://gre/modules/Services.jsm");
+
+function registry() registryModule.registry();
+
+function test() {
+ runTests(tests, function(cb) {resetSocial(); executeSoon(cb);});
+}
+
+let tests = {
+ testSingleProvider: function(cbnext) {
+ let r = registry();
+
+ let oc = new observerChecker(["social-browsing-current-service-changed",
+ "social-service-manifest-changed",
+ "social-browsing-enabled",
+ "social-browsing-disabled"]);
+
+ registerCleanupFunction(function() {
+ oc.terminate();
+ });
+
+ is(r.enabled, false, "registry should indicate disabled");
+ is(r.currentProvider, null, "must be no current provider when disabled.");
+
+ // attempt to enable it - it should fail as we have no providers.
+ r.enabled = true;
+ is(r.enabled, false, "social should fail to become enabled when no providers");
+ // and no notifications should have been sent above.
+ oc.check([]);
+
+ // we need a provider to use for further testing...
+ installTestProvider(function() {
+ // now we can enable it.
+ r.enabled = true;
+ is(r.enabled, true, "social should enable as there is now a provider");
+ is(r.currentProvider.origin, TEST_PROVIDER_ORIGIN, "check current test provider");
+ oc.check([{topic: "social-service-manifest-changed"},
+ {topic: "social-browsing-enabled"},
+ {topic: "social-browsing-current-service-changed"}
+ ]);
+ // disable our test provider - that should disable social.
+ ok(r.disableProvider(TEST_PROVIDER_ORIGIN, function() {
+ is(r.enabled, false, "social should be disabled after disabling only provider");
+ oc.check([{topic: "social-browsing-disabled"},
+ {topic: "social-service-manifest-changed"}]);
+
+ // re-enable it.
+ ok(r.enableProvider(TEST_PROVIDER_ORIGIN, function() {
+ // but social should still be disabled.
+ is(r.enabled, false, "social should remain disabled after enabling only provider");
+ r.enabled = true;
+ oc.check([{topic: "social-service-manifest-changed"},
+ {topic: "social-browsing-enabled"},
+ {topic: "social-browsing-current-service-changed"}
+ ]);
+
+ // disable browsing.
+ r.enabled = false;
+ is(r.enabled, false, "social should be disabled");
+ oc.check([{topic: "social-browsing-disabled"}]);
+ cbnext();
+ }), "check provider was enabled");
+ }), "check provider was disabled");
+ });
+ },
+
+ testMultipleProviders: function(cbnext) {
+ let r = registry();
+ // install the first provider and enable social.
+ installTestProvider(function() {
+ // now we can enable it.
+ r.enabled = true;
+ is(r.currentProvider.origin, TEST_PROVIDER_ORIGIN, "check current test provider");
+ // install the second - the first should remain current.
+ installTestProvider(function() {
+ is(r.currentProvider.origin, TEST_PROVIDER_ORIGIN, "check existing still current");
+ // disable the first - second should go current.
+ ok(r.disableProvider(TEST_PROVIDER_ORIGIN), "provider disabled ok");
+ is(r.currentProvider.origin, TEST_PROVIDER2_ORIGIN, "check new provider made current");
+ // re-enable the first and make it current.
+ ok(r.enableProvider(TEST_PROVIDER_ORIGIN, function() {
+ r.currentProvider = r.get(TEST_PROVIDER_ORIGIN);
+ is(r.currentProvider.origin, TEST_PROVIDER_ORIGIN, "check old provider made current");
+ // now delete the first provider - second should be current.
+ removeProvider(TEST_PROVIDER_ORIGIN, function() {
+ is(r.currentProvider.origin, TEST_PROVIDER2_ORIGIN, "check new provider made current");
+ cbnext();
+ })
+ }), "check provider was enabled");
+ }, TEST_PROVIDER2_MANIFEST);
+ });
+ },
+
+ testProviderPortIgnored: function(cbnext) {
+ let r = registry();
+ // install a provider without the port specified, then attempt to install
+ // it again with the port specified - the second should be ignored.
+ const originWithPort = TEST_PROVIDER_ORIGIN + ":443";
+ const manifestWithPort = originWithPort + TEST_PROVIDER_PATH + "/app.manifest";
+ is(Object.keys(r._providers).length, 0, "should be zero installed at the start");
+ installTestProvider(function() {
+ is(Object.keys(r._providers).length, 1, "should be one installed now");
+ // and again...
+ installTestProvider(function() {
+ is(Object.keys(r._providers).length, 1, "should still be one installed now");
+ cbnext();
+ }, manifestWithPort);
+ });
+ },
+
+ testProviderCert: function(cbnext, origin) {
+ // with no args this is testing the "no cert" case.
+ if (!origin) origin = "https://nocert.example.com";
+ let r = registry();
+ const manifestUrl = origin + TEST_PROVIDER_PATH + "/app.manifest";;
+ installTestProvider(function() {
+ is(r.get(origin), undefined, "ensure provider didn't get installed");
+ cbnext();
+ }, manifestUrl);
+ },
+
+ testProviderUntrustedCert: function(cbnext) {
+ this.testProviderCert(cbnext, "https://untrusted.example.com");
+ },
+
+ testProviderExpiredCert: function(cbnext) {
+ this.testProviderCert(cbnext, "https://expired.example.com");
+ },
+
+}
View
170 test/head-branch-default.js
@@ -0,0 +1,170 @@
+Cu.import("resource://gre/modules/Services.jsm");
+
+// This is the path to the test provider relative to its origin.
+const TEST_PROVIDER_PATH = "/browser/browser/features/socialapi/test/testprovider"
+
+// See http://mxr.mozilla.org/mozilla-central/source/build/pgo/server-locations.txt
+// for other possibly origins we could use.
+const TEST_PROVIDER_ORIGIN = "https://example.com";
+const TEST_PROVIDER_MANIFEST = TEST_PROVIDER_ORIGIN + TEST_PROVIDER_PATH + "/app.manifest";
+
+// Some tests use 2 providers - it really is the same provider but served from
+// a different origin.
+const TEST_PROVIDER2_ORIGIN = "https://test1.example.com"
+const TEST_PROVIDER2_MANIFEST = TEST_PROVIDER2_ORIGIN + TEST_PROVIDER_PATH + "/app.manifest";
+
+function makeTestProvider(input) {
+ return {
+ name: input.name,
+ workerURL: input.workerURL,
+ sidebarURL: input.sidebarURL,
+ iconURL: input.iconURL,
+ origin: input.origin,
+ enabled: input.enabled,
+ activate: function() {},
+ shutdown: function() {}
+ }
+}
+
+let headModules = {}
+Cu.import("resource://socialapi/modules/registry.js", headModules);
+Cu.import("resource://socialapi/modules/manifest.jsm", headModules);
+try {
+ headModules.initialize(makeTestProvider);
+} catch (ex) {
+ if (ex.toString() != "Error: already initialized") {
+ info("Unexpected failure to initialize the registry: " + ex)
+ throw ex;
+ }
+ // it's already been done...
+}
+
+function installTestProvider(callback, manifestUrl) {
+ if (!manifestUrl) {
+ manifestUrl = TEST_PROVIDER_MANIFEST;
+ }
+ let ms = headModules.manifestSvc;
+ ms.loadManifest(window.document, manifestUrl, true,
+ function() {if (callback) executeSoon(callback)});
+}
+
+function removeProvider(origin, cb) {
+ headModules.registry().remove(origin, function() {
+ if (cb) executeSoon(cb);
+ });
+}
+
+function resetPrefs() {
+ let prefBranch = Services.prefs.getBranch("social.provider.").QueryInterface(Ci.nsIPrefBranch2);
+ prefBranch.deleteBranch('');
+ let tmp = {};
+ Cu.import("resource://socialapi/modules/defaultprefs.js", tmp);
+ tmp.setDefaultPrefs();
+}
+
+function resetSocial() {
+ // ACK - this is most useful via registerCleanupFunction, but it doesn't
+ // have a concept of async - so no callback arg.
+ // reset the entire social world back to the state it is on a "clean" first
+ // startup - ie, all UI elements and prefs.
+ let r = headModules.registry();
+ r.enabled = false;
+ // all providers get nuked. - we reach into the impl here...
+ let providers = r._providers;
+ let origins = Object.keys(providers); // take a copy as we are mutating it
+ for each (let origin in origins) {
+ r.remove(origin);
+ }
+ resetPrefs();
+ info("social was reset to the default state"); // well, in theory anyway :)
+}
+
+// ALL tests here want a clean state.
+registerCleanupFunction(resetSocial);
+
+
+// a helper for checking observer notifications.
+function observerChecker(topics) {
+ this.topics = topics;
+ for each (let topic in topics) {
+ Services.obs.addObserver(this, topic, false);
+ }
+ this.observed = [];
+}
+
+observerChecker.prototype = {
+ terminate: function() {
+ for each (let topic in this.topics) {
+ Services.obs.removeObserver(this, topic);
+ }
+ this.observed = null;
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ this.observed.push({subject: aSubject, topic: aTopic, data: aData});
+ },
+
+ check: function(expected) {
+ if (this.observed.length != expected.length) {
+ dump("observer check failed - got topics " + [o.topic for each (o in this.observed)].join(", ") +
+ " - expected " + [o.topic for each (o in expected)].join(", ") + "\n");
+ }
+ is(this.observed.length, expected.length, "check expected number of observations");
+ for (let i = 0; i < expected.length; i++) {
+ let obtopic = this.observed[i] ? this.observed[i].topic : "<nothing observed>";
+ is(obtopic, expected[i].topic, "check observation " + i);
+ // todo: add subject etc checks?
+ }
+ this.observed = [];
+ }
+}
+
+// A helper to run a suite of tests.
+// The "test object" should be an object with function names as keys and a
+// function as the value. The functions will be called with a "cbnext" param
+// which should be called when the test is complete.
+// eg:
+// test = {
+// foo: function(cbnext) {... cbnext();}
+// }
+function runTests(tests, cbPreTest, cbPostTest) {
+ resetPrefs(); // all tests want the default prefs to start.
+ waitForExplicitFinish();
+ let testIter = Iterator(tests);
+
+ if (cbPreTest === undefined) {
+ cbPreTest = function(cb) {cb()};
+ }
+ if (cbPostTest === undefined) {
+ cbPostTest = function(cb) {cb()};
+ }
+
+ let runNextTest = function() {
+ let name, func;
+ try {
+ [name, func] = testIter.next();
+ } catch (err if err instanceof StopIteration) {
+ // out of items:
+ finish();
+ return;
+ }
+ // We run on a timeout as the frameworker also makes use of timeouts, so
+ // this helps keep the debug messages sane.
+ window.setTimeout(function() {
+ function cleanupAndRunNextTest() {
+ info("sub-test " + name + " complete");
+ cbPostTest(runNextTest);
+ }
+ cbPreTest(function() {
+ info("sub-test " + name + " starting");
+ try {
+ func.call(tests, cleanupAndRunNextTest);
+ } catch (ex) {
+ ok(false, "sub-test " + name + " failed: " + ex.toString());
+ cleanupAndRunNextTest();
+ }
+ })
+ }, 0)
+ }
+ runNextTest();
+}
Please sign in to comment.
Something went wrong with that request. Please try again.