diff --git a/README.md b/README.md
new file mode 100644
index 0000000..96c98ce
--- /dev/null
+++ b/README.md
@@ -0,0 +1,52 @@
+Introduction
+------------
+
+This is an implementation of a WebIntents-like API, as sketched out by Ian Hickson in http://lists.w3.org/Archives/Public/public-webapps/2011JulSep/1509.html
+
+This is implemented as a pure-JS shim - pages need only load `intents.js` and they can function as an invoker of intent or as a handler of intents. A rough outline of how it hangs together is:
+
+* intents.js arranges to create a hidden iframe back to localhost:8888 and uses a jschannel to communicate for registration and unregistration. localStorage on this host is the "repository".
+* When client code calls `invokeIntent()`, the picker is displayed an a jschannel created between the client code and the picker.
+* The picker loads the intent handlers in an iframe and yet another jschannel exists between the picker and the handler.
+Thus, the picker acts as a kind of relay - when the client communicates with the handler it goes over its jschannel to the picker, which then relays it over the jschannel to the handler.
+
+As Firefox doesn't implement MessagePorts yet, a simple port-like javascript object is used.
+
+Running the demo
+----------------
+
+* Start `server.py` in the root of this tree. This will start a http server on port 8888.
+* Open http://localhost:8888/handlers/sample_handler.html in your browser. Not much will happen but it should call registerIntentHandler to register that page as a handler for a 'share' intent. Close the page.
+
+We also use the Firefox Share addon as an OAuth helper for the google etc plugins. This takes a few steps.
+
+* Grab the `experiment/hixtents` branch from the git repo at https://github.com/mhammond/fx-share-addon
+* Grab the `develop` branches from the repos at https://github.com/mozilla/oauthorizer/ and https://github.com/mozilla/activities
+* After activating the addon-sdk, run the command:
+`cfx run --pkgdir=path-to-fx-share-addon --package-path=path-to-oauthorizer package-path=path-to-activities`
+* Start `server.py' in the root of the fx-share-addon tree. This will start a http server on port 8889.
+* Open http://localhost:8889/data/apps/google/google.html - this will register that page as a share handler. Close the page.
+* Open clients/task.html in the browser - you can open this either as a file:/// URL or via the server (ie, http://localhost:8888/clients/simple.html.
+* Click on the link to invoke a share intent.
+
+You should now see the picker with the 2 handlers displayed in tabs. The google service should be fully functional - ie, you can login and send an email.
+
+Notable differences from Hixie's sketch
+---------------------------------------
+
+Instead of:
+
+ var port = navigator.handleIntent(intent, filter);
+ port.postMessage(data);
+ port.onmessage = function (event) { handle(event.data) };
+
+we use a callback approach:
+
+ navigator.handleIntent(intent, filter, function(port) {
+ port.postMessage(data);
+ port.onmessage = function (event) { handle(event.data) };
+ });
+
+This is done as it make take some time for the UI to select an intent handler and we want to avoid blocking the main thread.
+
+Similarly, `registerIntentHandler`, `isIntentHandlerRegistered` and `unregisterIntentHandler` take a callback, although this was done for pragmatic reasons - the shim uses `postMessage` to a server for the respository implementation.
diff --git a/clients/simple.html b/clients/simple.html
new file mode 100644
index 0000000..f673b5c
--- /dev/null
+++ b/clients/simple.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+ Click to invoke 'test' intent...
+ Click to invoke 'share' intent...
+
+
+
diff --git a/handlers/README.md b/handlers/README.md
new file mode 100644
index 0000000..aabc779
--- /dev/null
+++ b/handlers/README.md
@@ -0,0 +1,9 @@
+This directory contains some demo "intent handlers". While they can be served by the server as a convenience, they don't rely on being on the same origin as the server. One way to test this out is to load them as file:/// URLs - but Browsers take a dim view of file:/// content hosted in iframes. To work around this, you can modify your profile's user.js and add the following lines:
+
+```
+user_pref("capability.policy.policynames", "localfilelinks");
+user_pref("capability.policy.localfilelinks.sites", "http://localhost:8888");
+user_pref("capability.policy.localfilelinks.checkloaduri.enabled", "allAccess");
+```
+
+You will then be able to load them from a file:/// URL (to re-register it) and the picker will display them.
diff --git a/handlers/sample_handler.html b/handlers/sample_handler.html
new file mode 100644
index 0000000..7773c08
--- /dev/null
+++ b/handlers/sample_handler.html
@@ -0,0 +1,25 @@
+
+
+ A test handler
+
+
+
+
+
+
+ Hey - I'm an intent handler and
+ I'm waiting for intent data
+
+
\ No newline at end of file
diff --git a/handlers/sample_handler.png b/handlers/sample_handler.png
new file mode 100644
index 0000000..840e2c6
Binary files /dev/null and b/handlers/sample_handler.png differ
diff --git a/intents-repo.html b/intents-repo.html
new file mode 100644
index 0000000..af8d177
--- /dev/null
+++ b/intents-repo.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/intents-repo.js b/intents-repo.js
new file mode 100644
index 0000000..f60159e
--- /dev/null
+++ b/intents-repo.js
@@ -0,0 +1,47 @@
+
+(function() {
+
+ function _getIntentRegistrations() {
+ if (localStorage.intentHandlers) {
+ return JSON.parse(localStorage.intentHandlers);
+ }
+ return {};
+ }
+
+ function _setIntentRegistrations(ob) {
+ localStorage.intentHandlers = JSON.stringify(ob);
+ }
+
+ var chan = Channel.build({window: window.parent, origin: "*", scope: "intents_channel"});
+ chan.bind("registerIntentHandler", function(trans, data) {
+ var intentsOb = _getIntentRegistrations();
+ if (!intentsOb[data.intent]) {
+ intentsOb[data.intent] = {};
+ }
+ if (!intentsOb[data.intent][data.filter]) {
+ intentsOb[data.intent][data.filter] = {};
+ }
+ intentsOb[data.intent][data.filter][data.url] = data;
+ _setIntentRegistrations(intentsOb);
+ });
+
+ chan.bind("isIntentHandlerRegistered", function(trans, data) {
+ var intentsOb = _getIntentRegistrations();
+ try {
+ return !!intentsOb[data.intent][data.filter][data.url];
+ } catch(ex) {
+ return false;
+ }
+ });
+
+ chan.bind("unregisterIntentandler", function(trans, data) {
+ var intentsOb = _getIntentRegistrations();
+ try {
+ delete localStorage.intentHandlers[data.intent][data.filter][data.url];
+ } catch (ex) {
+ return; // nothing to do.
+ }
+ _setIntentRegistrations(intentsOb);
+ });
+
+})();
diff --git a/intents.js b/intents.js
new file mode 100644
index 0000000..027fd0e
--- /dev/null
+++ b/intents.js
@@ -0,0 +1,838 @@
+
+console = {
+ log: function() {
+ var args = Array.prototype.slice.call(arguments);
+ dump(args.join(" ") + "\n");
+ }
+};
+
+// This uses potentially 3 different channels:
+// * One to intents-repo.html for "repository" (ie localStorage) related work.
+// * If this module is loaded by a client window, there is a channel between
+// that client window and the picker.
+// * If this module is loaded by an intent handler, there is a channel between
+// that handler and the picker.
+// Obviously only one of the last 2 will actually be active for a given
+// instance of this script.
+(function() {
+ var intentsRepoOrigin = "http://localhost:8888";
+ var intentsRepoPath = "/intents-repo.html";
+ var intentsPickerPath = "/picker.html";
+ var pickerUrlOrigin = "*"; // XXX - fix this once there is a real origin!!
+
+ // from webintents.js
+ var getFavIcon = function() {
+ var links = document.getElementsByTagName("link");
+ var link;
+ for(var i = 0; link = links[i]; i++) {
+ if((link.rel == "icon" || link.rel == "shortcut") && !!link.href ) {
+ var url = link.href;
+ if(url.substring(0, 7) == "file://") // hack for a demo!
+ return url;
+ if(url.substring(0, 7) != "http://" &&
+ url.substring(0, 8) != "https://") {
+ if(url.substring(0,1) == "/") {
+ // absolute path
+ return window.location.protocol + "//" + window.location.host + "/" + url;
+ }
+ else {
+ // relative path
+ var path = document.location.href;
+ path = path.substring(0, path.lastIndexOf('/') + 1);
+ url = path + url;
+ dump("RESULT " + url);
+ return url;
+ }
+ }
+ else {
+ return url;
+ }
+ }
+ }
+
+ return window.location.protocol + "//" + window.location.host + "/favicon.ico";
+ };
+
+ function defaultErrorHandler(err, msg) {
+ dump("channel error: " + err + "/" + msg + "\n");
+ }
+
+ // Attempt to create a jschannel between the picker window and us.
+ function setupPickerHandlerChannel() {
+ var pickerHandlerChannel = Channel.build({
+ window: window.top,
+ origin: pickerUrlOrigin,
+ scope: "intents_handler_channel",
+ onReady: function() {
+ pickerHandlerReady = true;
+ }
+ });
+ // and for the delivery of intents to the handler
+ var currentIntentPort = null;
+ pickerHandlerChannel.bind("intentInitialize", function(trans, data) {
+ currentIntentPort = null;
+ });
+ pickerHandlerChannel.bind("intentRequest", function(trans, data) {
+ if (!window.onintent) {
+ // strange, but nothing we can do.
+ return;
+ }
+// console.log("intents.js intentRequest handler for", window.location.href, "-", JSON.stringify(data), "with existing port", currentIntentPort);
+ if (!currentIntentPort) {
+ // simulate a port object - we don't seem able to use real ports yet,
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=677638 - then call
+ // window.onintent.
+ currentIntentPort = {
+ postMessage: function(response) {
+// console.log("intent port sending", response, "to the picker");
+ pickerHandlerChannel.call({method: "intentResponse",
+ params: response,
+ success: function() {},
+ error: defaultErrorHandler
+ });
+ }
+ // the onintent handler may attach an onmessage...
+ };
+ var simevt = {
+ ports: [currentIntentPort],
+ data: data
+ };
+ window.onintent(simevt);
+ } else {
+ // We've already created the port and called onintent - so just
+ // deliver this directly to the port.
+ // (and just let things die a noisy death if there is no onmessage
+ // for the port - it is a good clue to the dev that the handler
+ // isn't expecting further data...)
+ currentIntentPort.onmessage(data);
+ };
+ });
+ };
+
+ window.addEventListener("load", function() {
+ var pickerHandlerReady = false;
+
+ if (window !== window.parent) {
+ // We are in an iframe, so this module might have been loaded by the
+ // intent handler.
+ setupPickerHandlerChannel();
+ }
+
+ // hacky helper for file:// stuff...
+ function getOriginForPM(origin) {
+ if (!origin || origin == "null") return '*';
+ return origin;
+ }
+
+ var _repoChannel;
+ function _getRepoChannel() {
+ if (!_repoChannel) {
+ // Create hidden iframe dom element
+ var doc = window.document;
+ var _iframe = doc.createElement("iframe");
+ _iframe.style.display = "none";
+ // Append iframe to the dom and load up our repo inside
+ doc.body.appendChild(_iframe);
+ _iframe.src = intentsRepoOrigin + intentsRepoPath;
+
+ _repoChannel = Channel.build({
+ window: _iframe.contentWindow,
+ origin: intentsRepoOrigin,
+ scope: "intents_channel"
+ });
+ }
+ return _repoChannel;
+ };
+
+ navigator.registerIntentHandler = function(intent, filter, url, kind) {
+ var title = document.title || window.location.host;
+ var icon = getFavIcon();
+ var params = {intent: intent, filter: filter, url: url, kind: kind,
+ title: title, icon: icon};
+ _getRepoChannel().call({method: "registerIntentHandler",
+ params: params,
+ success: function() {},
+ error: defaultErrorHandler
+ });
+ };
+
+ navigator.isIntentHandlerRegistered = function(intent, filter, url, callback) {
+ var params = {intent: intent, filter: filter, url: url};
+ _getRepoChannel().call({method: "isIntentHandlerRegistered",
+ params: params,
+ success: function(result) {callback(result)},
+ error: function(err) {callback(false);}
+ });
+ };
+
+ navigator.unregisterIntentandler = function(intent, filter, url) {
+ var params = {intent: intent, filter: filter, url: url};
+ _getRepoChannel().call({method: "unregisterIntentandler",
+ params: params,
+ success: function() {},
+ error: defaultErrorHandler
+ });
+ };
+
+ navigator.handleIntent = function(intent, contentType, callback) {
+ // There was a misguided attempt at reusing an existing picker window, but
+ // for now just create a new one each time.
+ // XXX - needs more thought as an existing one will still have a channel
+ // open...
+ var features = 'height=400,width=500';
+ var pickerUrl = intentsRepoOrigin + intentsPickerPath;
+ var pickerWindow = window.open(pickerUrl, "_blank", features);
+ // create a jschannel between us and the picker.
+ var pickerClientChannel = Channel.build({
+ window: pickerWindow,
+ origin: '*',
+ scope: "intents_client_channel",
+ onReady: function() {
+ console.log("intents.js channel to picker is ready");
+ var data = {intent: intent, contentType: contentType};
+ pickerClientChannel.call({method: "intentHandle",
+ params: data,
+ success: function() {},
+ error: defaultErrorHandler
+ });
+ }
+ });
+ pickerClientChannel.bind("intentBegin", function(trans, data) {
+ // The picker has wired-up the intent with a specific service, so now
+ // we can simulate a port object and callback into the client.
+ var port = {
+ postMessage: function(data) {
+ // when the client calls postMessage it just ends up as an
+ // 'intentRequest' call on our channel.
+ pickerClientChannel.call({method: 'intentRequest',
+ params: data,
+ success: function() {},
+ error: defaultErrorHandler
+ });
+ }
+ };
+ callback(port);
+ });
+
+ }; // end of navigator.handleIntent.
+ }, false);
+
+// inline copy of jschannel.js
+/**
+ * js_channel is a very lightweight abstraction on top of
+ * postMessage which defines message formats and semantics
+ * to support interactions more rich than just message passing
+ * js_channel supports:
+ * + query/response - traditional rpc
+ * + query/update/response - incremental async return of results
+ * to a query
+ * + notifications - fire and forget
+ * + error handling
+ *
+ * js_channel is based heavily on json-rpc, but is focused at the
+ * problem of inter-iframe RPC.
+ *
+ * Message types:
+ * There are 5 types of messages that can flow over this channel,
+ * and you may determine what type of message an object is by
+ * examining its parameters:
+ * 1. Requests
+ * + integer id
+ * + string method
+ * + (optional) any params
+ * 2. Callback Invocations (or just "Callbacks")
+ * + integer id
+ * + string callback
+ * + (optional) params
+ * 3. Error Responses (or just "Errors)
+ * + integer id
+ * + string error
+ * + (optional) string message
+ * 4. Responses
+ * + integer id
+ * + (optional) any result
+ * 5. Notifications
+ * + string method
+ * + (optional) any params
+ */
+
+;var Channel = (function() {
+ "use strict";
+
+ // current transaction id, start out at a random *odd* number between 1 and a million
+ // There is one current transaction counter id per page, and it's shared between
+ // channel instances. That means of all messages posted from a single javascript
+ // evaluation context, we'll never have two with the same id.
+ var s_curTranId = Math.floor(Math.random()*1000001);
+
+ // no two bound channels in the same javascript evaluation context may have the same origin, scope, and window.
+ // futher if two bound channels have the same window and scope, they may not have *overlapping* origins
+ // (either one or both support '*'). This restriction allows a single onMessage handler to efficiently
+ // route messages based on origin and scope. The s_boundChans maps origins to scopes, to message
+ // handlers. Request and Notification messages are routed using this table.
+ // Finally, channels are inserted into this table when built, and removed when destroyed.
+ var s_boundChans = { };
+
+ // add a channel to s_boundChans, throwing if a dup exists
+ function s_addBoundChan(win, origin, scope, handler) {
+ function hasWin(arr) {
+ for (var i = 0; i < arr.length; i++) if (arr[i].win === win) return true;
+ return false;
+ }
+
+ // does she exist?
+ var exists = false;
+
+
+ if (origin === '*') {
+ // we must check all other origins, sadly.
+ for (var k in s_boundChans) {
+ if (!s_boundChans.hasOwnProperty(k)) continue;
+ if (k === '*') continue;
+ if (typeof s_boundChans[k][scope] === 'object') {
+ exists = hasWin(s_boundChans[k][scope]);
+ if (exists) break;
+ }
+ }
+ } else {
+ // we must check only '*'
+ if ((s_boundChans['*'] && s_boundChans['*'][scope])) {
+ exists = hasWin(s_boundChans['*'][scope]);
+ }
+ if (!exists && s_boundChans[origin] && s_boundChans[origin][scope])
+ {
+ exists = hasWin(s_boundChans[origin][scope]);
+ }
+ }
+ if (exists) throw "A channel is already bound to the same window which overlaps with origin '"+ origin +"' and has scope '"+scope+"'";
+
+ if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { };
+ if (typeof s_boundChans[origin][scope] != 'object') s_boundChans[origin][scope] = [ ];
+ s_boundChans[origin][scope].push({win: win, handler: handler});
+ }
+
+ function s_removeBoundChan(win, origin, scope) {
+ var arr = s_boundChans[origin][scope];
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].win === win) {
+ arr.splice(i,1);
+ }
+ }
+ if (s_boundChans[origin][scope].length === 0) {
+ delete s_boundChans[origin][scope]
+ }
+ }
+
+ function s_isArray(obj) {
+ if (Array.isArray) return Array.isArray(obj);
+ else {
+ return (obj.constructor.toString().indexOf("Array") != -1);
+ }
+ }
+
+ // No two outstanding outbound messages may have the same id, period. Given that, a single table
+ // mapping "transaction ids" to message handlers, allows efficient routing of Callback, Error, and
+ // Response messages. Entries are added to this table when requests are sent, and removed when
+ // responses are received.
+ var s_transIds = { };
+
+ // class singleton onMessage handler
+ // this function is registered once and all incoming messages route through here. This
+ // arrangement allows certain efficiencies, message data is only parsed once and dispatch
+ // is more efficient, especially for large numbers of simultaneous channels.
+ var s_onMessage = function(e) {
+ try {
+ var m = JSON.parse(e.data);
+ if (typeof m !== 'object' || m === null) throw "malformed";
+ } catch(e) {
+ // just ignore any posted messages that do not consist of valid JSON
+ return;
+ }
+
+ var w = e.source;
+ var o = e.origin;
+ var s, i, meth;
+
+ if (typeof m.method === 'string') {
+ var ar = m.method.split('::');
+ if (ar.length == 2) {
+ s = ar[0];
+ meth = ar[1];
+ } else {
+ meth = m.method;
+ }
+ }
+
+ if (typeof m.id !== 'undefined') i = m.id;
+
+ // w is message source window
+ // o is message origin
+ // m is parsed message
+ // s is message scope
+ // i is message id (or undefined)
+ // meth is unscoped method name
+ // ^^ based on these factors we can route the message
+
+ // if it has a method it's either a notification or a request,
+ // route using s_boundChans
+ if (typeof meth === 'string') {
+ var delivered = false;
+ if (s_boundChans[o] && s_boundChans[o][s]) {
+ for (var i = 0; i < s_boundChans[o][s].length; i++) {
+ if (s_boundChans[o][s][i].win === w) {
+ s_boundChans[o][s][i].handler(o, meth, m);
+ delivered = true;
+ break;
+ }
+ }
+ }
+
+ if (!delivered && s_boundChans['*'] && s_boundChans['*'][s]) {
+ for (var i = 0; i < s_boundChans['*'][s].length; i++) {
+ if (s_boundChans['*'][s][i].win === w) {
+ s_boundChans['*'][s][i].handler(o, meth, m);
+ break;
+ }
+ }
+ }
+ }
+ // otherwise it must have an id (or be poorly formed
+ else if (typeof i != 'undefined') {
+ if (s_transIds[i]) s_transIds[i](o, meth, m);
+ }
+ };
+
+ // Setup postMessage event listeners
+ if (window.addEventListener) window.addEventListener('message', s_onMessage, false);
+ else if(window.attachEvent) window.attachEvent('onmessage', s_onMessage);
+
+ /* a messaging channel is constructed from a window and an origin.
+ * the channel will assert that all messages received over the
+ * channel match the origin
+ *
+ * Arguments to Channel.build(cfg):
+ *
+ * cfg.window - the remote window with which we'll communicate
+ * cfg.origin - the expected origin of the remote window, may be '*'
+ * which matches any origin
+ * cfg.scope - the 'scope' of messages. a scope string that is
+ * prepended to message names. local and remote endpoints
+ * of a single channel must agree upon scope. Scope may
+ * not contain double colons ('::').
+ * cfg.debugOutput - A boolean value. If true and window.console.log is
+ * a function, then debug strings will be emitted to that
+ * function.
+ * cfg.debugOutput - A boolean value. If true and window.console.log is
+ * a function, then debug strings will be emitted to that
+ * function.
+ * cfg.postMessageObserver - A function that will be passed two arguments,
+ * an origin and a message. It will be passed these immediately
+ * before messages are posted.
+ * cfg.gotMessageObserver - A function that will be passed two arguments,
+ * an origin and a message. It will be passed these arguments
+ * immediately after they pass scope and origin checks, but before
+ * they are processed.
+ * cfg.onReady - A function that will be invoked when a channel becomes "ready",
+ * this occurs once both sides of the channel have been
+ * instantiated and an application level handshake is exchanged.
+ * the onReady function will be passed a single argument which is
+ * the channel object that was returned from build().
+ */
+ return {
+ build: function(cfg) {
+ var debug = function(m) {
+ if (cfg.debugOutput && window.console && window.console.log) {
+ // try to stringify, if it doesn't work we'll let javascript's built in toString do its magic
+ try { if (typeof m !== 'string') m = JSON.stringify(m); } catch(e) { }
+ console.log("["+chanId+"] " + m);
+ }
+ }
+
+ /* browser capabilities check */
+ if (!window.postMessage) throw("jschannel cannot run this browser, no postMessage");
+ if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) {
+ throw("jschannel cannot run this browser, no JSON parsing/serialization");
+ }
+
+ /* basic argument validation */
+ if (typeof cfg != 'object') throw("Channel build invoked without a proper object argument");
+
+ if (!cfg.window || !cfg.window.postMessage) throw("Channel.build() called without a valid window argument");
+
+ /* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same
+ * window... Not sure if we care to support that */
+ if (window === cfg.window) throw("target window is same as present window -- not allowed");
+
+ // let's require that the client specify an origin. if we just assume '*' we'll be
+ // propagating unsafe practices. that would be lame.
+ var validOrigin = false;
+ if (typeof cfg.origin === 'string') {
+ var oMatch;
+ if (cfg.origin === "*") validOrigin = true;
+ // allow valid domains under http and https. Also, trim paths off otherwise valid origins.
+ else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9\.])+(?::\d+)?/))) {
+ cfg.origin = oMatch[0].toLowerCase();
+ validOrigin = true;
+ }
+ }
+
+ if (!validOrigin) throw ("Channel.build() called with an invalid origin");
+
+ if (typeof cfg.scope !== 'undefined') {
+ if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string';
+ if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'"
+ }
+
+ /* private variables */
+ // generate a random and psuedo unique id for this channel
+ var chanId = (function () {
+ var text = "";
+ var alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ for(var i=0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length));
+ return text;
+ })();
+
+ // registrations: mapping method names to call objects
+ var regTbl = { };
+ // current oustanding sent requests
+ var outTbl = { };
+ // current oustanding received requests
+ var inTbl = { };
+ // are we ready yet? when false we will block outbound messages.
+ var ready = false;
+ var pendingQueue = [ ];
+
+ var createTransaction = function(id,origin,callbacks) {
+ var shouldDelayReturn = false;
+ var completed = false;
+
+ return {
+ origin: origin,
+ invoke: function(cbName, v) {
+ // verify in table
+ if (!inTbl[id]) throw "attempting to invoke a callback of a nonexistent transaction: " + id;
+ // verify that the callback name is valid
+ var valid = false;
+ for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; }
+ if (!valid) throw "request supports no such callback '" + cbName + "'";
+
+ // send callback invocation
+ postMessage({ id: id, callback: cbName, params: v});
+ },
+ error: function(error, message) {
+ completed = true;
+ // verify in table
+ if (!inTbl[id]) throw "error called for nonexistent message: " + id;
+
+ // remove transaction from table
+ delete inTbl[id];
+
+ // send error
+ postMessage({ id: id, error: error, message: message });
+ },
+ complete: function(v) {
+ completed = true;
+ // verify in table
+ if (!inTbl[id]) throw "complete called for nonexistent message: " + id;
+ // remove transaction from table
+ delete inTbl[id];
+ // send complete
+ postMessage({ id: id, result: v });
+ },
+ delayReturn: function(delay) {
+ if (typeof delay === 'boolean') {
+ shouldDelayReturn = (delay === true);
+ }
+ return shouldDelayReturn;
+ },
+ completed: function() {
+ return completed;
+ }
+ };
+ }
+
+ var setTransactionTimeout = function(transId, timeout, method) {
+ return window.setTimeout(function() {
+ if (outTbl[transId]) {
+ // XXX: what if client code raises an exception here?
+ var msg = "timeout (" + timeout + "ms) exceeded on method '" + method + "'";
+ (1,outTbl[transId].error)("timeout_error", msg);
+ delete outTbl[transId];
+ delete s_transIds[transId];
+ }
+ }, timeout);
+ }
+
+ var onMessage = function(origin, method, m) {
+ // if an observer was specified at allocation time, invoke it
+ if (typeof cfg.gotMessageObserver === 'function') {
+ // pass observer a clone of the object so that our
+ // manipulations are not visible (i.e. method unscoping).
+ // This is not particularly efficient, but then we expect
+ // that message observers are primarily for debugging anyway.
+ try {
+ cfg.gotMessageObserver(origin, m);
+ } catch (e) {
+ debug("gotMessageObserver() raised an exception: " + e.toString());
+ }
+ }
+
+ // now, what type of message is this?
+ if (m.id && method) {
+ // a request! do we have a registered handler for this request?
+ if (regTbl[method]) {
+ var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : [ ]);
+ inTbl[m.id] = { };
+ try {
+ // callback handling. we'll magically create functions inside the parameter list for each
+ // callback
+ if (m.callbacks && s_isArray(m.callbacks) && m.callbacks.length > 0) {
+ for (var i = 0; i < m.callbacks.length; i++) {
+ var path = m.callbacks[i];
+ var obj = m.params;
+ var pathItems = path.split('/');
+ for (var j = 0; j < pathItems.length - 1; j++) {
+ var cp = pathItems[j];
+ if (typeof obj[cp] !== 'object') obj[cp] = { };
+ obj = obj[cp];
+ }
+ obj[pathItems[pathItems.length - 1]] = (function() {
+ var cbName = path;
+ return function(params) {
+ return trans.invoke(cbName, params);
+ }
+ })();
+ }
+ }
+ var resp = regTbl[method](trans, m.params);
+ if (!trans.delayReturn() && !trans.completed()) trans.complete(resp);
+ } catch(e) {
+ // automagic handling of exceptions:
+ dump("ERROR " + typeof e + "/" + e.toString() + "/" + method + "\n");
+ var error = "runtime_error";
+ var message = null;
+ // * if it's a string then it gets an error code of 'runtime_error' and string is the message
+ if (typeof e === 'string') {
+ message = e;
+ } else if (typeof e === 'object') {
+ // either an array or an object
+ // * if it's an array of length two, then array[0] is the code, array[1] is the error message
+ if (e && s_isArray(e) && e.length == 2) {
+ error = e[0];
+ message = e[1];
+ }
+ // * if it's an object then we'll look form error and message parameters
+ else if (typeof e.error === 'string') {
+ error = e.error;
+ if (!e.message) message = "";
+ else if (typeof e.message === 'string') message = e.message;
+ else e = e.message; // let the stringify/toString message give us a reasonable verbose error string
+ }
+ }
+
+ // message is *still* null, let's try harder
+ if (message === null) {
+ try {
+ message = JSON.stringify(e);
+ /* On MSIE8, this can result in 'out of memory', which
+ * leaves message undefined. */
+ if (typeof(message) == 'undefined')
+ message = e.toString();
+ } catch (e2) {
+ message = e.toString();
+ }
+ }
+
+ trans.error(error,message);
+ }
+ }
+ } else if (m.id && m.callback) {
+ if (!outTbl[m.id] ||!outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback])
+ {
+ debug("ignoring invalid callback, id:"+m.id+ " (" + m.callback +")");
+ } else {
+ // XXX: what if client code raises an exception here?
+ outTbl[m.id].callbacks[m.callback](m.params);
+ }
+ } else if (m.id) {
+ if (!outTbl[m.id]) {
+ debug("ignoring invalid response: " + m.id);
+ } else {
+ // XXX: what if client code raises an exception here?
+ if (m.error) {
+ (1,outTbl[m.id].error)(m.error, m.message);
+ } else {
+ if (m.result !== undefined) (1,outTbl[m.id].success)(m.result);
+ else (1,outTbl[m.id].success)();
+ }
+ delete outTbl[m.id];
+ delete s_transIds[m.id];
+ }
+ } else if (method) {
+ // tis a notification.
+ if (regTbl[method]) {
+ // yep, there's a handler for that.
+ // transaction is null for notifications.
+ regTbl[method](null, m.params);
+ // if the client throws, we'll just let it bubble out
+ // what can we do? Also, here we'll ignore return values
+ }
+ }
+ }
+
+ // now register our bound channel for msg routing
+ s_addBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage);
+
+ // scope method names based on cfg.scope specified when the Channel was instantiated
+ var scopeMethod = function(m) {
+ if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::");
+ return m;
+ }
+
+ // a small wrapper around postmessage whose primary function is to handle the
+ // case that clients start sending messages before the other end is "ready"
+ var postMessage = function(msg, force) {
+ if (!msg) throw "postMessage called with null message";
+
+ // delay posting if we're not ready yet.
+ var verb = (ready ? "post " : "queue ");
+ debug(verb + " message: " + JSON.stringify(msg));
+ if (!force && !ready) {
+ pendingQueue.push(msg);
+ } else {
+ if (typeof cfg.postMessageObserver === 'function') {
+ try {
+ cfg.postMessageObserver(cfg.origin, msg);
+ } catch (e) {
+ debug("postMessageObserver() raised an exception: " + e.toString());
+ }
+ }
+
+ cfg.window.postMessage(JSON.stringify(msg), cfg.origin);
+ }
+ }
+
+ var onReady = function(trans, type) {
+ debug('ready msg received');
+ if (ready) throw "received ready message while in ready state. help!";
+
+ if (type === 'ping') {
+ chanId += '-R';
+ } else {
+ chanId += '-L';
+ }
+
+ obj.unbind('__ready'); // now this handler isn't needed any more.
+ ready = true;
+ debug('ready msg accepted.');
+
+ if (type === 'ping') {
+ obj.notify({ method: '__ready', params: 'pong' });
+ }
+
+ // flush queue
+ while (pendingQueue.length) {
+ postMessage(pendingQueue.pop());
+ }
+
+ // invoke onReady observer if provided
+ if (typeof cfg.onReady === 'function') cfg.onReady(obj);
+ };
+
+ var obj = {
+ // tries to unbind a bound message handler. returns false if not possible
+ unbind: function (method) {
+ if (regTbl[method]) {
+ if (!(delete regTbl[method])) throw ("can't delete method: " + method);
+ return true;
+ }
+ return false;
+ },
+ bind: function (method, cb) {
+ if (!method || typeof method !== 'string') throw "'method' argument to bind must be string";
+ if (!cb || typeof cb !== 'function') throw "callback missing from bind params";
+
+ if (regTbl[method]) throw "method '"+method+"' is already bound!";
+ regTbl[method] = cb;
+ return this;
+ },
+ call: function(m) {
+ if (!m) throw 'missing arguments to call function';
+ if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string";
+ if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call";
+
+ // now it's time to support the 'callback' feature of jschannel. We'll traverse the argument
+ // object and pick out all of the functions that were passed as arguments.
+ var callbacks = { };
+ var callbackNames = [ ];
+
+ var pruneFunctions = function (path, obj) {
+ if (typeof obj === 'object') {
+ for (var k in obj) {
+ if (!obj.hasOwnProperty(k)) continue;
+ var np = path + (path.length ? '/' : '') + k;
+ if (typeof obj[k] === 'function') {
+ callbacks[np] = obj[k];
+ callbackNames.push(np);
+ delete obj[k];
+ } else if (typeof obj[k] === 'object') {
+ pruneFunctions(np, obj[k]);
+ }
+ }
+ }
+ };
+ pruneFunctions("", m.params);
+
+ // build a 'request' message and send it
+ var msg = { id: s_curTranId, method: scopeMethod(m.method), params: m.params };
+ if (callbackNames.length) msg.callbacks = callbackNames;
+
+ if (m.timeout)
+ // XXX: This function returns a timeout ID, but we don't do anything with it.
+ // We might want to keep track of it so we can cancel it using clearTimeout()
+ // when the transaction completes.
+ setTransactionTimeout(s_curTranId, m.timeout, scopeMethod(m.method));
+
+ // insert into the transaction table
+ outTbl[s_curTranId] = { callbacks: callbacks, error: m.error, success: m.success };
+ s_transIds[s_curTranId] = onMessage;
+
+ // increment current id
+ s_curTranId++;
+
+ postMessage(msg);
+ },
+ notify: function(m) {
+ if (!m) throw 'missing arguments to notify function';
+ if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string";
+
+ // no need to go into any transaction table
+ postMessage({ method: scopeMethod(m.method), params: m.params });
+ },
+ destroy: function () {
+ s_removeBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''));
+ if (window.removeEventListener) window.removeEventListener('message', onMessage, false);
+ else if(window.detachEvent) window.detachEvent('onmessage', onMessage);
+ ready = false;
+ regTbl = { };
+ inTbl = { };
+ outTbl = { };
+ cfg.origin = null;
+ pendingQueue = [ ];
+ debug("channel destroyed");
+ chanId = "";
+ }
+ };
+
+ obj.bind('__ready', onReady);
+ setTimeout(function() {
+ postMessage({ method: scopeMethod('__ready'), params: "ping" }, true);
+ }, 0);
+
+ return obj;
+ }
+ };
+})();
+// end of inline copy of jschannel.js
+
+})();
diff --git a/jschannel.js b/jschannel.js
new file mode 100644
index 0000000..86a9f59
--- /dev/null
+++ b/jschannel.js
@@ -0,0 +1,614 @@
+/**
+ * js_channel is a very lightweight abstraction on top of
+ * postMessage which defines message formats and semantics
+ * to support interactions more rich than just message passing
+ * js_channel supports:
+ * + query/response - traditional rpc
+ * + query/update/response - incremental async return of results
+ * to a query
+ * + notifications - fire and forget
+ * + error handling
+ *
+ * js_channel is based heavily on json-rpc, but is focused at the
+ * problem of inter-iframe RPC.
+ *
+ * Message types:
+ * There are 5 types of messages that can flow over this channel,
+ * and you may determine what type of message an object is by
+ * examining its parameters:
+ * 1. Requests
+ * + integer id
+ * + string method
+ * + (optional) any params
+ * 2. Callback Invocations (or just "Callbacks")
+ * + integer id
+ * + string callback
+ * + (optional) params
+ * 3. Error Responses (or just "Errors)
+ * + integer id
+ * + string error
+ * + (optional) string message
+ * 4. Responses
+ * + integer id
+ * + (optional) any result
+ * 5. Notifications
+ * + string method
+ * + (optional) any params
+ */
+
+;var Channel = (function() {
+ "use strict";
+
+ // current transaction id, start out at a random *odd* number between 1 and a million
+ // There is one current transaction counter id per page, and it's shared between
+ // channel instances. That means of all messages posted from a single javascript
+ // evaluation context, we'll never have two with the same id.
+ var s_curTranId = Math.floor(Math.random()*1000001);
+
+ // no two bound channels in the same javascript evaluation context may have the same origin, scope, and window.
+ // futher if two bound channels have the same window and scope, they may not have *overlapping* origins
+ // (either one or both support '*'). This restriction allows a single onMessage handler to efficiently
+ // route messages based on origin and scope. The s_boundChans maps origins to scopes, to message
+ // handlers. Request and Notification messages are routed using this table.
+ // Finally, channels are inserted into this table when built, and removed when destroyed.
+ var s_boundChans = { };
+
+ // add a channel to s_boundChans, throwing if a dup exists
+ function s_addBoundChan(win, origin, scope, handler) {
+ function hasWin(arr) {
+ for (var i = 0; i < arr.length; i++) if (arr[i].win === win) return true;
+ return false;
+ }
+
+ // does she exist?
+ var exists = false;
+
+
+ if (origin === '*') {
+ // we must check all other origins, sadly.
+ for (var k in s_boundChans) {
+ if (!s_boundChans.hasOwnProperty(k)) continue;
+ if (k === '*') continue;
+ if (typeof s_boundChans[k][scope] === 'object') {
+ exists = hasWin(s_boundChans[k][scope]);
+ if (exists) break;
+ }
+ }
+ } else {
+ // we must check only '*'
+ if ((s_boundChans['*'] && s_boundChans['*'][scope])) {
+ exists = hasWin(s_boundChans['*'][scope]);
+ }
+ if (!exists && s_boundChans[origin] && s_boundChans[origin][scope])
+ {
+ exists = hasWin(s_boundChans[origin][scope]);
+ }
+ }
+ if (exists) throw "A channel is already bound to the same window which overlaps with origin '"+ origin +"' and has scope '"+scope+"'";
+
+ if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { };
+ if (typeof s_boundChans[origin][scope] != 'object') s_boundChans[origin][scope] = [ ];
+ s_boundChans[origin][scope].push({win: win, handler: handler});
+ }
+
+ function s_removeBoundChan(win, origin, scope) {
+ var arr = s_boundChans[origin][scope];
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i].win === win) {
+ arr.splice(i,1);
+ }
+ }
+ if (s_boundChans[origin][scope].length === 0) {
+ delete s_boundChans[origin][scope]
+ }
+ }
+
+ function s_isArray(obj) {
+ if (Array.isArray) return Array.isArray(obj);
+ else {
+ return (obj.constructor.toString().indexOf("Array") != -1);
+ }
+ }
+
+ // No two outstanding outbound messages may have the same id, period. Given that, a single table
+ // mapping "transaction ids" to message handlers, allows efficient routing of Callback, Error, and
+ // Response messages. Entries are added to this table when requests are sent, and removed when
+ // responses are received.
+ var s_transIds = { };
+
+ // class singleton onMessage handler
+ // this function is registered once and all incoming messages route through here. This
+ // arrangement allows certain efficiencies, message data is only parsed once and dispatch
+ // is more efficient, especially for large numbers of simultaneous channels.
+ var s_onMessage = function(e) {
+ try {
+ var m = JSON.parse(e.data);
+ if (typeof m !== 'object' || m === null) throw "malformed";
+ } catch(e) {
+ // just ignore any posted messages that do not consist of valid JSON
+ return;
+ }
+
+ var w = e.source;
+ var o = e.origin;
+ var s, i, meth;
+
+ if (typeof m.method === 'string') {
+ var ar = m.method.split('::');
+ if (ar.length == 2) {
+ s = ar[0];
+ meth = ar[1];
+ } else {
+ meth = m.method;
+ }
+ }
+
+ if (typeof m.id !== 'undefined') i = m.id;
+
+ // w is message source window
+ // o is message origin
+ // m is parsed message
+ // s is message scope
+ // i is message id (or undefined)
+ // meth is unscoped method name
+ // ^^ based on these factors we can route the message
+
+ // if it has a method it's either a notification or a request,
+ // route using s_boundChans
+ if (typeof meth === 'string') {
+ var delivered = false;
+ if (s_boundChans[o] && s_boundChans[o][s]) {
+ for (var i = 0; i < s_boundChans[o][s].length; i++) {
+ if (s_boundChans[o][s][i].win === w) {
+ s_boundChans[o][s][i].handler(o, meth, m);
+ delivered = true;
+ break;
+ }
+ }
+ }
+
+ if (!delivered && s_boundChans['*'] && s_boundChans['*'][s]) {
+ for (var i = 0; i < s_boundChans['*'][s].length; i++) {
+ if (s_boundChans['*'][s][i].win === w) {
+ s_boundChans['*'][s][i].handler(o, meth, m);
+ break;
+ }
+ }
+ }
+ }
+ // otherwise it must have an id (or be poorly formed
+ else if (typeof i != 'undefined') {
+ if (s_transIds[i]) s_transIds[i](o, meth, m);
+ }
+ };
+
+ // Setup postMessage event listeners
+ if (window.addEventListener) window.addEventListener('message', s_onMessage, false);
+ else if(window.attachEvent) window.attachEvent('onmessage', s_onMessage);
+
+ /* a messaging channel is constructed from a window and an origin.
+ * the channel will assert that all messages received over the
+ * channel match the origin
+ *
+ * Arguments to Channel.build(cfg):
+ *
+ * cfg.window - the remote window with which we'll communicate
+ * cfg.origin - the expected origin of the remote window, may be '*'
+ * which matches any origin
+ * cfg.scope - the 'scope' of messages. a scope string that is
+ * prepended to message names. local and remote endpoints
+ * of a single channel must agree upon scope. Scope may
+ * not contain double colons ('::').
+ * cfg.debugOutput - A boolean value. If true and window.console.log is
+ * a function, then debug strings will be emitted to that
+ * function.
+ * cfg.debugOutput - A boolean value. If true and window.console.log is
+ * a function, then debug strings will be emitted to that
+ * function.
+ * cfg.postMessageObserver - A function that will be passed two arguments,
+ * an origin and a message. It will be passed these immediately
+ * before messages are posted.
+ * cfg.gotMessageObserver - A function that will be passed two arguments,
+ * an origin and a message. It will be passed these arguments
+ * immediately after they pass scope and origin checks, but before
+ * they are processed.
+ * cfg.onReady - A function that will be invoked when a channel becomes "ready",
+ * this occurs once both sides of the channel have been
+ * instantiated and an application level handshake is exchanged.
+ * the onReady function will be passed a single argument which is
+ * the channel object that was returned from build().
+ */
+ return {
+ build: function(cfg) {
+ var debug = function(m) {
+ if (cfg.debugOutput && window.console && window.console.log) {
+ // try to stringify, if it doesn't work we'll let javascript's built in toString do its magic
+ try { if (typeof m !== 'string') m = JSON.stringify(m); } catch(e) { }
+ console.log("["+chanId+"] " + m);
+ }
+ }
+
+ /* browser capabilities check */
+ if (!window.postMessage) throw("jschannel cannot run this browser, no postMessage");
+ if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) {
+ throw("jschannel cannot run this browser, no JSON parsing/serialization");
+ }
+
+ /* basic argument validation */
+ if (typeof cfg != 'object') throw("Channel build invoked without a proper object argument");
+
+ if (!cfg.window || !cfg.window.postMessage) throw("Channel.build() called without a valid window argument");
+
+ /* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same
+ * window... Not sure if we care to support that */
+ if (window === cfg.window) throw("target window is same as present window -- not allowed");
+
+ // let's require that the client specify an origin. if we just assume '*' we'll be
+ // propagating unsafe practices. that would be lame.
+ var validOrigin = false;
+ if (typeof cfg.origin === 'string') {
+ var oMatch;
+ if (cfg.origin === "*") validOrigin = true;
+ // allow valid domains under http and https. Also, trim paths off otherwise valid origins.
+ else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9\.])+(?::\d+)?/))) {
+ cfg.origin = oMatch[0].toLowerCase();
+ validOrigin = true;
+ }
+ }
+
+ if (!validOrigin) throw ("Channel.build() called with an invalid origin");
+
+ if (typeof cfg.scope !== 'undefined') {
+ if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string';
+ if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'"
+ }
+
+ /* private variables */
+ // generate a random and psuedo unique id for this channel
+ var chanId = (function () {
+ var text = "";
+ var alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ for(var i=0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length));
+ return text;
+ })();
+
+ // registrations: mapping method names to call objects
+ var regTbl = { };
+ // current oustanding sent requests
+ var outTbl = { };
+ // current oustanding received requests
+ var inTbl = { };
+ // are we ready yet? when false we will block outbound messages.
+ var ready = false;
+ var pendingQueue = [ ];
+
+ var createTransaction = function(id,origin,callbacks) {
+ var shouldDelayReturn = false;
+ var completed = false;
+
+ return {
+ origin: origin,
+ invoke: function(cbName, v) {
+ // verify in table
+ if (!inTbl[id]) throw "attempting to invoke a callback of a nonexistent transaction: " + id;
+ // verify that the callback name is valid
+ var valid = false;
+ for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; }
+ if (!valid) throw "request supports no such callback '" + cbName + "'";
+
+ // send callback invocation
+ postMessage({ id: id, callback: cbName, params: v});
+ },
+ error: function(error, message) {
+ completed = true;
+ // verify in table
+ if (!inTbl[id]) throw "error called for nonexistent message: " + id;
+
+ // remove transaction from table
+ delete inTbl[id];
+
+ // send error
+ postMessage({ id: id, error: error, message: message });
+ },
+ complete: function(v) {
+ completed = true;
+ // verify in table
+ if (!inTbl[id]) throw "complete called for nonexistent message: " + id;
+ // remove transaction from table
+ delete inTbl[id];
+ // send complete
+ postMessage({ id: id, result: v });
+ },
+ delayReturn: function(delay) {
+ if (typeof delay === 'boolean') {
+ shouldDelayReturn = (delay === true);
+ }
+ return shouldDelayReturn;
+ },
+ completed: function() {
+ return completed;
+ }
+ };
+ }
+
+ var setTransactionTimeout = function(transId, timeout, method) {
+ return window.setTimeout(function() {
+ if (outTbl[transId]) {
+ // XXX: what if client code raises an exception here?
+ var msg = "timeout (" + timeout + "ms) exceeded on method '" + method + "'";
+ (1,outTbl[transId].error)("timeout_error", msg);
+ delete outTbl[transId];
+ delete s_transIds[transId];
+ }
+ }, timeout);
+ }
+
+ var onMessage = function(origin, method, m) {
+ // if an observer was specified at allocation time, invoke it
+ if (typeof cfg.gotMessageObserver === 'function') {
+ // pass observer a clone of the object so that our
+ // manipulations are not visible (i.e. method unscoping).
+ // This is not particularly efficient, but then we expect
+ // that message observers are primarily for debugging anyway.
+ try {
+ cfg.gotMessageObserver(origin, m);
+ } catch (e) {
+ debug("gotMessageObserver() raised an exception: " + e.toString());
+ }
+ }
+
+ // now, what type of message is this?
+ if (m.id && method) {
+ // a request! do we have a registered handler for this request?
+ if (regTbl[method]) {
+ var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : [ ]);
+ inTbl[m.id] = { };
+ try {
+ // callback handling. we'll magically create functions inside the parameter list for each
+ // callback
+ if (m.callbacks && s_isArray(m.callbacks) && m.callbacks.length > 0) {
+ for (var i = 0; i < m.callbacks.length; i++) {
+ var path = m.callbacks[i];
+ var obj = m.params;
+ var pathItems = path.split('/');
+ for (var j = 0; j < pathItems.length - 1; j++) {
+ var cp = pathItems[j];
+ if (typeof obj[cp] !== 'object') obj[cp] = { };
+ obj = obj[cp];
+ }
+ obj[pathItems[pathItems.length - 1]] = (function() {
+ var cbName = path;
+ return function(params) {
+ return trans.invoke(cbName, params);
+ }
+ })();
+ }
+ }
+ var resp = regTbl[method](trans, m.params);
+ if (!trans.delayReturn() && !trans.completed()) trans.complete(resp);
+ } catch(e) {
+ // automagic handling of exceptions:
+ var error = "runtime_error";
+ var message = null;
+ // * if it's a string then it gets an error code of 'runtime_error' and string is the message
+ if (typeof e === 'string') {
+ message = e;
+ } else if (typeof e === 'object') {
+ // either an array or an object
+ // * if it's an array of length two, then array[0] is the code, array[1] is the error message
+ if (e && s_isArray(e) && e.length == 2) {
+ error = e[0];
+ message = e[1];
+ }
+ // * if it's an object then we'll look form error and message parameters
+ else if (typeof e.error === 'string') {
+ error = e.error;
+ if (!e.message) message = "";
+ else if (typeof e.message === 'string') message = e.message;
+ else e = e.message; // let the stringify/toString message give us a reasonable verbose error string
+ }
+ }
+
+ // message is *still* null, let's try harder
+ if (message === null) {
+ try {
+ message = JSON.stringify(e);
+ /* On MSIE8, this can result in 'out of memory', which
+ * leaves message undefined. */
+ if (typeof(message) == 'undefined')
+ message = e.toString();
+ } catch (e2) {
+ message = e.toString();
+ }
+ }
+
+ trans.error(error,message);
+ }
+ }
+ } else if (m.id && m.callback) {
+ if (!outTbl[m.id] ||!outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback])
+ {
+ debug("ignoring invalid callback, id:"+m.id+ " (" + m.callback +")");
+ } else {
+ // XXX: what if client code raises an exception here?
+ outTbl[m.id].callbacks[m.callback](m.params);
+ }
+ } else if (m.id) {
+ if (!outTbl[m.id]) {
+ debug("ignoring invalid response: " + m.id);
+ } else {
+ // XXX: what if client code raises an exception here?
+ if (m.error) {
+ (1,outTbl[m.id].error)(m.error, m.message);
+ } else {
+ if (m.result !== undefined) (1,outTbl[m.id].success)(m.result);
+ else (1,outTbl[m.id].success)();
+ }
+ delete outTbl[m.id];
+ delete s_transIds[m.id];
+ }
+ } else if (method) {
+ // tis a notification.
+ if (regTbl[method]) {
+ // yep, there's a handler for that.
+ // transaction is null for notifications.
+ regTbl[method](null, m.params);
+ // if the client throws, we'll just let it bubble out
+ // what can we do? Also, here we'll ignore return values
+ }
+ }
+ }
+
+ // now register our bound channel for msg routing
+ s_addBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage);
+
+ // scope method names based on cfg.scope specified when the Channel was instantiated
+ var scopeMethod = function(m) {
+ if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::");
+ return m;
+ }
+
+ // a small wrapper around postmessage whose primary function is to handle the
+ // case that clients start sending messages before the other end is "ready"
+ var postMessage = function(msg, force) {
+ if (!msg) throw "postMessage called with null message";
+
+ // delay posting if we're not ready yet.
+ var verb = (ready ? "post " : "queue ");
+ debug(verb + " message: " + JSON.stringify(msg));
+ if (!force && !ready) {
+ pendingQueue.push(msg);
+ } else {
+ if (typeof cfg.postMessageObserver === 'function') {
+ try {
+ cfg.postMessageObserver(cfg.origin, msg);
+ } catch (e) {
+ debug("postMessageObserver() raised an exception: " + e.toString());
+ }
+ }
+
+ cfg.window.postMessage(JSON.stringify(msg), cfg.origin);
+ }
+ }
+
+ var onReady = function(trans, type) {
+ debug('ready msg received');
+ if (ready) throw "received ready message while in ready state. help!";
+
+ if (type === 'ping') {
+ chanId += '-R';
+ } else {
+ chanId += '-L';
+ }
+
+ obj.unbind('__ready'); // now this handler isn't needed any more.
+ ready = true;
+ debug('ready msg accepted.');
+
+ if (type === 'ping') {
+ obj.notify({ method: '__ready', params: 'pong' });
+ }
+
+ // flush queue
+ while (pendingQueue.length) {
+ postMessage(pendingQueue.pop());
+ }
+
+ // invoke onReady observer if provided
+ if (typeof cfg.onReady === 'function') cfg.onReady(obj);
+ };
+
+ var obj = {
+ // tries to unbind a bound message handler. returns false if not possible
+ unbind: function (method) {
+ if (regTbl[method]) {
+ if (!(delete regTbl[method])) throw ("can't delete method: " + method);
+ return true;
+ }
+ return false;
+ },
+ bind: function (method, cb) {
+ if (!method || typeof method !== 'string') throw "'method' argument to bind must be string";
+ if (!cb || typeof cb !== 'function') throw "callback missing from bind params";
+
+ if (regTbl[method]) throw "method '"+method+"' is already bound!";
+ regTbl[method] = cb;
+ return this;
+ },
+ call: function(m) {
+ if (!m) throw 'missing arguments to call function';
+ if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string";
+ if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call";
+
+ // now it's time to support the 'callback' feature of jschannel. We'll traverse the argument
+ // object and pick out all of the functions that were passed as arguments.
+ var callbacks = { };
+ var callbackNames = [ ];
+
+ var pruneFunctions = function (path, obj) {
+ if (typeof obj === 'object') {
+ for (var k in obj) {
+ if (!obj.hasOwnProperty(k)) continue;
+ var np = path + (path.length ? '/' : '') + k;
+ if (typeof obj[k] === 'function') {
+ callbacks[np] = obj[k];
+ callbackNames.push(np);
+ delete obj[k];
+ } else if (typeof obj[k] === 'object') {
+ pruneFunctions(np, obj[k]);
+ }
+ }
+ }
+ };
+ pruneFunctions("", m.params);
+
+ // build a 'request' message and send it
+ var msg = { id: s_curTranId, method: scopeMethod(m.method), params: m.params };
+ if (callbackNames.length) msg.callbacks = callbackNames;
+
+ if (m.timeout)
+ // XXX: This function returns a timeout ID, but we don't do anything with it.
+ // We might want to keep track of it so we can cancel it using clearTimeout()
+ // when the transaction completes.
+ setTransactionTimeout(s_curTranId, m.timeout, scopeMethod(m.method));
+
+ // insert into the transaction table
+ outTbl[s_curTranId] = { callbacks: callbacks, error: m.error, success: m.success };
+ s_transIds[s_curTranId] = onMessage;
+
+ // increment current id
+ s_curTranId++;
+
+ postMessage(msg);
+ },
+ notify: function(m) {
+ if (!m) throw 'missing arguments to notify function';
+ if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string";
+
+ // no need to go into any transaction table
+ postMessage({ method: scopeMethod(m.method), params: m.params });
+ },
+ destroy: function () {
+ s_removeBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''));
+ if (window.removeEventListener) window.removeEventListener('message', onMessage, false);
+ else if(window.detachEvent) window.detachEvent('onmessage', onMessage);
+ ready = false;
+ regTbl = { };
+ inTbl = { };
+ outTbl = { };
+ cfg.origin = null;
+ pendingQueue = [ ];
+ debug("channel destroyed");
+ chanId = "";
+ }
+ };
+
+ obj.bind('__ready', onReady);
+ setTimeout(function() {
+ postMessage({ method: scopeMethod('__ready'), params: "ping" }, true);
+ }, 0);
+
+ return obj;
+ }
+ };
+})();
diff --git a/picker.css b/picker.css
new file mode 100644
index 0000000..a65e1a0
--- /dev/null
+++ b/picker.css
@@ -0,0 +1,78 @@
+.serviceIcon {
+ width:24px;
+ height:24px;
+ vertical-align:middle;
+}
+.serviceTitle {
+ max-width:76px;
+ vertical-align:middle;
+ text-align:center;
+ display:block;
+ overflow-x:hidden;
+}
+.serviceTab {
+ text-align:center;
+ max-width:76px;
+ overflow-x:hidden;
+ color:black;
+ font:0.7em 'Lucida Grande',Tahoma,Arial,sans-serif;
+}
+
+#services {
+ border-radius:0;
+ -moz-user-select:none;
+}
+
+
+ul.tabs {
+ margin: 0;
+ padding: 0;
+ float: left;
+ list-style: none;
+ height: 62px; /*--Set height of tabs--*/
+ border-bottom: 1px solid #999;
+ border-left: 1px solid #999;
+ width: 100%;
+}
+ul.tabs li {
+ float: left;
+ margin: 0;
+ padding: 0;
+ height: 61px; /*--Subtract 1px from the height of the unordered list--*/
+ line-height: 31px; /*--Vertically aligns the text within the tab--*/
+ border: 1px solid #999;
+ border-left: none;
+ margin-bottom: -1px; /*--Pull the list item down 1px--*/
+ overflow: hidden;
+ position: relative;
+ background: #e0e0e0;
+}
+ul.tabs li a {
+ text-decoration: none;
+ color: #000;
+ display: block;
+ font-size: 1.2em;
+ padding: 0 20px;
+ border: 1px solid #fff; /*--Gives the bevel look with a 1px white border inside the list item--*/
+ outline: none;
+}
+ul.tabs li a:hover {
+ background: #ccc;
+}
+html ul.tabs li.active, html ul.tabs li.active a:hover { /*--Makes sure that the active tab does not listen to the hover properties--*/
+ background: #fff;
+ border-bottom: 1px solid #fff; /*--Makes the active tab look like it's connected with its content--*/
+}
+
+.tab_container {
+ border: 1px solid #999;
+ border-top: none;
+ overflow: hidden;
+ clear: both;
+ float: left; width: 100%;
+ background: #fff;
+}
+.tab_content {
+ padding: 20px;
+ font-size: 1.2em;
+}
diff --git a/picker.html b/picker.html
new file mode 100644
index 0000000..ed0c87c
--- /dev/null
+++ b/picker.html
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ I'm an intent picker window - I've been created with an intent
+ ???
+ and a content-type of ???
+
+
+
+
+
+
+
+
+
+
diff --git a/server.py b/server.py
new file mode 100644
index 0000000..af1905f
--- /dev/null
+++ b/server.py
@@ -0,0 +1,46 @@
+import sys
+import os
+import posixpath
+import urllib
+from SimpleHTTPServer import SimpleHTTPRequestHandler
+from BaseHTTPServer import HTTPServer
+
+class Handler(SimpleHTTPRequestHandler):
+ def translate_path(self, path):
+ """Translate a /-separated PATH to the local filename syntax.
+
+ Components that mean special things to the local file system
+ (e.g. drive or directory names) are ignored. (XXX They should
+ probably be diagnosed.)
+
+ """
+ # abandon query parameters
+ path = path.split('?',1)[0]
+ path = path.split('#',1)[0]
+ path = posixpath.normpath(urllib.unquote(path))
+ words = path.split('/')
+ words = filter(None, words)
+ path = os.path.dirname(__file__)
+ for word in words:
+ drive, word = os.path.splitdrive(word)
+ head, word = os.path.split(word)
+ if word in (os.curdir, os.pardir): continue
+ path = os.path.join(path, word)
+ return path
+
+def main():
+ if sys.argv[1:]:
+ port = int(sys.argv[1])
+ else:
+ port = 8888
+ server_address = ('', port)
+
+ #Handler.protocol_version = "HTTP/1.1"
+ httpd = HTTPServer(server_address, Handler)
+
+ sa = httpd.socket.getsockname()
+ print "Serving HTTP on", sa[0], "port", sa[1], "..."
+ httpd.serve_forever()
+
+if __name__=='__main__':
+ main()