/*
* easyXDM
* http://easyxdm.net/
* Copyright(c) 2009, Øyvind Sean Kinsey, oyvind@kinsey.no.
*
* MIT Licensed - http://easyxdm.net/license/mit.txt
*
*/
(function (window, document, location, setTimeout, decodeURIComponent, encodeURIComponent) {
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, JSON, XMLHttpRequest, window, escape, unescape, ActiveXObject */
var global = this;
var channelId = 0;
var emptyFn = Function.prototype;
var reURI = /^(http.?:\/\/([^\/\s]+))/, // returns groups for origin (1) and domain (2)
reParent = /[\-\w]+\/\.\.\//, // matches a foo/../ expression
reDoubleSlash = /([^:])\/\//g; // matches // anywhere but in the protocol
//Sniffing is bad, but in this case unavoidable
var CREATE_FRAME_USING_HTML = /msie [67]/.test(navigator.userAgent.toLowerCase());
var _trace = emptyFn;
/* Methods for feature testing
* From http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
*/
function isHostMethod(object, property){
var t = typeof object[property];
return t == 'function' ||
(!!(t == 'object' && object[property])) ||
t == 'unknown';
}
function isHostObject(object, property){
return !!(typeof(object[property]) == 'object' && object[property]);
}
/*
* Cross Browser implementation for adding and removing event listeners.
*/
var on, un;
if (isHostMethod(window, "addEventListener")) {
on = function(target, type, listener){
_trace("adding listener " + type);
target.addEventListener(type, listener, false);
};
un = function(target, type, listener, useCapture){
_trace("removing listener " + type);
target.removeEventListener(type, listener, useCapture);
};
}
else if (isHostMethod(window, "attachEvent")) {
on = function(object, sEvent, fpNotify){
_trace("adding listener " + sEvent);
object.attachEvent("on" + sEvent, fpNotify);
};
un = function(object, sEvent, fpNotify){
_trace("removing listener " + sEvent);
object.detachEvent("on" + sEvent, fpNotify);
};
}
else {
throw new Error("Browser not supported");
}
/*
* Cross Browser implementation of DOMContentLoaded.
*/
var isReady = false, domReadyQueue = [];
if ("readyState" in document) {
isReady = (document.readyState == "complete" || document.readyState == "loaded");
}
else {
// If readyState is not supported in the browser, then in order to be able to fire whenReady functions apropriately
// when added dynamically _after_ DOM load, we have to deduce wether the DOM is ready or not.
if (document.body) {
// document.body is not available prior to the body being built
// This does mean that we might fire it prematurely, but we only need the body element to be available for appending.
isReady = true;
}
}
function dom_onLoaded(){
if (isReady) {
return;
}
_trace("firing dom_onLoaded");
isReady = true;
for (var i = 0; i < domReadyQueue.length; i++) {
domReadyQueue[i]();
}
domReadyQueue.length = 0;
un(window, "DOMContentLoaded", dom_onLoaded);
un(document, "DOMContentLoaded", dom_onLoaded);
if (isHostMethod(window, "ActiveXObject")) {
un(window, "load", dom_onLoaded);
}
}
function document_onReadyStateChange(){
if (document.readyState == "complete") {
dom_onLoaded();
un(document, "readystatechange", document_onReadyStateChange);
}
}
if (!isReady) {
on(window, "DOMContentLoaded", dom_onLoaded);
on(document, "DOMContentLoaded", dom_onLoaded);
if (isHostMethod(window, "ActiveXObject")) {
on(document, "readystatechange", document_onReadyStateChange);
on(window, "load", dom_onLoaded);
if (window === top) {
(function doScrollCheck(){
if (isReady) {
return;
}
// http://javascript.nwbox.com/IEContentLoaded/
try {
document.documentElement.doScroll("left");
}
catch (e) {
setTimeout(doScrollCheck, 1);
return;
}
dom_onLoaded();
}());
}
}
}
/**
* This will add a function to the queue of functions to be run once the DOM reaches a ready state.
* If functions are added after this event then they will be executed immediately.
* @param {function} fn The function to add
* @param {Object} scope An optional scope for the function to be called with.
*/
function whenReady(fn, scope){
if (isReady) {
fn.call(scope);
return;
}
domReadyQueue.push(function(){
fn.call(scope);
});
}
/*
* Methods for working with URLs
*/
/**
* Get the domain name from a url.
* @param {String} url The url to extract the domain from.
* @returns The domain part of the url.
* @type {String}
*/
function getDomainName(url){
if (!url) {
throw new Error("url is undefined or empty");
}
return url.match(reURI)[2];
}
/**
* Returns a string containing the schema, domain and if present the port
* @param {String} url The url to extract the location from
* @return {String} The location part of the url
*/
function getLocation(url){
if (!url) {
throw new Error("url is undefined or empty");
}
return url.match(reURI)[1];
}
/**
* Resolves a relative url into an absolute one.
* @param {String} url The path to resolve.
* @return {String} The resolved url.
*/
function resolveUrl(url){
if (!url) {
throw new Error("url is undefined or empty");
}
// replace all // except the one in proto with /
url = url.replace(reDoubleSlash, "$1/");
// If the url is a valid url we do nothing
if (!url.match(/^(http||https):\/\//)) {
// If this is a relative path
var path = (url.substring(0, 1) === "/") ? "" : location.pathname;
if (path.substring(path.length - 1) !== "/") {
path = path.substring(0, path.lastIndexOf("/") + 1);
}
url = location.protocol + "//" + location.host + path + url;
}
// reduce all 'xyz/../' to just ''
while (reParent.test(url)) {
url = url.replace(reParent, "");
}
_trace("resolved url '" + url + "'");
return url;
}
/**
* Appends the parameters to the given url.
* The base url can contain existing query parameters.
* @param {String} url The base url.
* @param {Object} parameters The parameters to add.
* @return {String} A new valid url with the parameters appended.
*/
function appendQueryParameters(url, parameters){
if (!parameters) {
throw new Error("parameters is undefined or null");
}
var hash = "", indexOf = url.indexOf("#");
if (indexOf !== -1) {
hash = url.substring(indexOf);
url = url.substring(0, indexOf);
}
var q = [];
for (var key in parameters) {
if (parameters.hasOwnProperty(key)) {
q.push(key + "=" + encodeURIComponent(parameters[key]));
}
}
return url + ((url.indexOf("?") === -1) ? "?" : "&") + q.join("&") + hash;
}
var query = (function(){
var query = {}, pair, search = location.search.substring(1).split("&"), i = search.length;
while (i--) {
pair = search[i].split("=");
query[pair[0]] = decodeURIComponent(pair[1]);
}
return query;
}());
/*
* Helper methods
*/
/**
* Helper for checking if a variable/property is undefined
* @param {Object} v The variable to test
* @return {Boolean} True if the passed variable is undefined
*/
function undef(v){
return typeof v === "undefined";
}
/**
* A safe implementation of HTML5 JSON. Feature testing is used to make sure the implementation works.
* @return {JSON} A valid JSON conforming object, or null if not found.
*/
function getJSON(){
var cached = {};
var obj = {
a: [1, 2, 3]
}, json = "{\"a\":[1,2,3]}";
if (JSON && typeof JSON.stringify === "function" && JSON.stringify(obj).replace((/\s/g), "") === json) {
// this is a working JSON instance
return JSON;
}
if (Object.toJSON) {
if (Object.toJSON(obj).replace((/\s/g), "") === json) {
// this is a working stringify method
cached.stringify = Object.toJSON;
}
}
if (typeof String.prototype.evalJSON === "function") {
obj = json.evalJSON();
if (obj.a && obj.a.length === 3 && obj.a[2] === 3) {
// this is a working parse method
cached.parse = function(str){
return str.evalJSON();
};
}
}
if (cached.stringify && cached.parse) {
// Only memoize the result if we have valid instance
getJSON = function(){
return cached;
};
return cached;
}
return null;
}
/**
* Applies properties from the source object to the target object.
* @param {Object} target The target of the properties.
* @param {Object} source The source of the properties.
* @param {Boolean} noOverwrite Set to True to only set non-existing properties.
*/
function apply(destination, source, noOverwrite){
var member;
for (var prop in source) {
if (source.hasOwnProperty(prop)) {
if (prop in destination) {
member = source[prop];
if (typeof member === "object") {
apply(destination[prop], member, noOverwrite);
}
else if (!noOverwrite) {
destination[prop] = source[prop];
}
}
else {
destination[prop] = source[prop];
}
}
}
return destination;
}
/**
* Creates a frame and appends it to the DOM.
* @param config {object} This object can have the following properties
*
* - {object} prop The properties that should be set on the frame. This should include the 'src' property.
* - {object} attr The attributes that should be set on the frame.
* - {DOMElement} container Its parent element (Optional).
* - {function} onLoad A method that should be called with the frames contentWindow as argument when the frame is fully loaded. (Optional)
*
* @return The frames DOMElement
* @type DOMElement
*/
function createFrame(config){
_trace("creating frame: " + config.props.src);
var frame;
// This is to work around the problems in IE6/7 with setting the name property.
// Internally this is set as 'submitName' instead when using 'iframe.name = ...'
// This is not required by easyXDM itself, but is to facilitate other use cases
if (config.props.name && CREATE_FRAME_USING_HTML) {
frame = document.createElement("");
}
else {
frame = document.createElement("IFRAME");
}
if (config.props.name) {
// We need to add these properties before adding the element to te DOM
frame.id = frame.name = config.props.name;
delete config.props.name;
}
if (config.onLoad) {
frame.loadFn = function(){
config.onLoad(frame.contentWindow);
};
on(frame, "load", frame.loadFn);
}
if (typeof config.container == "string") {
config.container = document.getElementById(config.container);
}
if (!config.container) {
// This needs to be hidden like this, simply setting display:none and the like will cause failures in some browsers.
frame.style.position = "absolute";
frame.style.left = "-2000px";
frame.style.top = "0px";
config.container = document.body;
}
frame.border = frame.frameBorder = 0;
config.container.insertBefore(frame, config.container.firstChild);
// transfer properties to the frame
apply(frame, config.props);
return frame;
}
/*
* Methods related to AJAX
*/
/**
* Creates a cross-browser XMLHttpRequest object
* @return {XMLHttpRequest} A XMLHttpRequest object.
*/
var getXhr = (function(){
if (isHostMethod(window, "XMLHttpRequest")) {
return function(){
return new XMLHttpRequest();
};
}
else {
var item = (function(){
var list = ["Microsoft", "Msxml2", "Msxml3"], i = list.length;
while (i--) {
try {
item = list[i] + ".XMLHTTP";
var obj = new ActiveXObject(item);
return item;
}
catch (e) {
}
}
}());
return function(){
return new ActiveXObject(item);
};
}
}());
/**
* Runs an asynchronous request using XMLHttpRequest
* @param {object} config The configuration
*/
function ajax(config){
var req = getXhr(), pairs = [], data, isPOST;
apply(config, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-Requested-With": "XMLHttpRequest"
},
success: emptyFn,
error: function(msg){
throw new Error(msg);
},
data: {},
type: "plain"
}, true);
isPOST = config.method == "POST";
for (var key in config.data) {
if (config.data.hasOwnProperty(key)) {
pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(config.data[key]));
}
}
data = pairs.join("&");
req.open(config.method, config.url + (isPOST ? "" : "?" + data), true);
for (var prop in config.headers) {
if (config.headers.hasOwnProperty(prop)) {
req.setRequestHeader(prop, config.headers[prop]);
}
}
req.onreadystatechange = function(){
if (req.readyState == 4) {
var response, errorMsg;
if (config.type == "json") {
try {
response = req.responseText;
response = getJSON().parse(response);
}
catch (e) {
errorMsg = "An error occured while parsing the JSON: " + e.message;
}
}
else {
response = req.responseText;
}
if (req.status < 200 || req.status >= 300) {
errorMsg = "The server did not return a valid status code.";
}
if (errorMsg) {
config.error({
message: errorMsg,
status: req.status,
data: response,
toString: function(){
return this.message + " Status: " + this.status;
}
});
}
else {
config.success(response);
}
req.onreadystatechange = emptyFn;
req = null;
}
};
req.send(isPOST ? data : "");
}
/**
* Check whether a domain is allowed using an Access Control List.
* The ACL can contain * and ? as wildcards, or can be regular expressions.
* If regular expressions they need to begin with ^ and end with $.
* @param {Array/String} acl The list of allowed domains
* @param {String} domain The domain to test.
* @return {Boolean} True if the domain is allowed, false if not.
*/
function checkAcl(acl, domain){
// normalize into an array
if (typeof acl == "string") {
acl = [acl];
}
var re, i = acl.length;
while (i--) {
re = acl[i];
re = new RegExp(re.substr(0, 1) == "^" ? re : ("^" + re.replace(/(\*)/g, ".$1").replace(/\?/g, ".") + "$"));
if (re.test(domain)) {
return true;
}
}
return false;
}
/*
* Functions related to stacks
*/
/**
* Prepares an array of stack-elements suitable for the current configuration
* @param {Object} config The Transports configuration. See easyXDM.Socket for more.
* @return {Array} An array of stack-elements with the TransportElement at index 0.
*/
function prepareTransportStack(config){
var protocol = config.protocol, stackEls;
config.isHost = config.isHost || undef(query.xdm_p);
_trace("preparing transport stack");
if (!config.props) {
config.props = {};
}
if (!config.isHost) {
_trace("using parameters from query");
config.channel = query.xdm_c;
config.secret = query.xdm_s;
config.remote = query.xdm_e;
protocol = query.xdm_p;
if (config.acl && !checkAcl(config.acl, config.remote)) {
throw new Error("Access denied for " + config.remote);
}
}
else {
config.remote = resolveUrl(config.remote);
config.channel = config.channel || "default" + channelId++;
config.secret = Math.random().toString(16).substring(2);
if (undef(protocol)) {
if (getLocation(location.href) == getLocation(config.remote)) {
/*
* Both documents has the same origin, lets use direct access.
*/
protocol = "4";
}
else if (isHostMethod(window, "postMessage") || isHostMethod(document, "postMessage")) {
/*
* This is supported in IE8+, Firefox 3+, Opera 9+, Chrome 2+ and Safari 4+
*/
protocol = "1";
}
else if (isHostMethod(window, "ActiveXObject") && isHostMethod(window, "execScript")) {
/*
* This is supported in IE6 and IE7
*/
protocol = "3";
}
else if (navigator.product === "Gecko" && "frameElement" in window && navigator.userAgent.indexOf('WebKit') == -1) {
/*
* This is supported in Gecko (Firefox 1+)
*/
protocol = "5";
}
else if (config.remoteHelper) {
/*
* This is supported in all browsers that retains the value of window.name when
* navigating from one domain to another, and where parent.frames[foo] can be used
* to get access to a frame from the same domain
*/
config.remoteHelper = resolveUrl(config.remoteHelper);
protocol = "2";
}
else {
/*
* This is supported in all browsers where [window].location is writable for all
* The resize event will be used if resize is supported and the iframe is not put
* into a container, else polling will be used.
*/
protocol = "0";
}
_trace("selecting protocol: " + protocol);
}
else {
_trace("using protocol: " + protocol);
}
}
switch (protocol) {
case "0":// 0 = HashTransport
apply(config, {
interval: 100,
delay: 2000,
useResize: true,
useParent: false,
usePolling: false
}, true);
if (config.isHost) {
if (!config.local) {
_trace("looking for image to use as local");
// If no local is set then we need to find an image hosted on the current domain
var domain = location.protocol + "//" + location.host, images = document.body.getElementsByTagName("img"), image;
var i = images.length;
while (i--) {
image = images[i];
if (image.src.substring(0, domain.length) === domain) {
config.local = image.src;
break;
}
}
if (!config.local) {
_trace("no image found, defaulting to using the window");
// If no local was set, and we are unable to find a suitable file, then we resort to using the current window
config.local = window;
}
}
var parameters = {
xdm_c: config.channel,
xdm_p: 0
};
if (config.local === window) {
// We are using the current window to listen to
config.usePolling = true;
config.useParent = true;
config.local = location.protocol + "//" + location.host + location.pathname + location.search;
parameters.xdm_e = config.local;
parameters.xdm_pa = 1; // use parent
}
else {
parameters.xdm_e = resolveUrl(config.local);
}
if (config.container) {
config.useResize = false;
parameters.xdm_po = 1; // use polling
}
config.remote = appendQueryParameters(config.remote, parameters);
}
else {
apply(config, {
channel: query.xdm_c,
remote: query.xdm_e,
useParent: !undef(query.xdm_pa),
usePolling: !undef(query.xdm_po),
useResize: config.useParent ? false : config.useResize
});
}
stackEls = [new easyXDM.stack.HashTransport(config), new easyXDM.stack.ReliableBehavior({
timeout: config.interval * 1.5
}), new easyXDM.stack.QueueBehavior({
encode: true,
maxLength: 4000 - config.remote.length
}), new easyXDM.stack.VerifyBehavior({
initiate: config.isHost
})];
break;
case "1":
stackEls = [new easyXDM.stack.PostMessageTransport(config)];
break;
case "2":
stackEls = [new easyXDM.stack.NameTransport(config), new easyXDM.stack.QueueBehavior(), new easyXDM.stack.VerifyBehavior({
initiate: config.isHost
})];
break;
case "3":
stackEls = [new easyXDM.stack.NixTransport(config)];
break;
case "4":
stackEls = [new easyXDM.stack.SameOriginTransport(config)];
break;
case "5":
stackEls = [new easyXDM.stack.FrameElementTransport(config)];
break;
}
// this behavior is responsible for buffering outgoing messages, and for performing lazy initialization
stackEls.push(new easyXDM.stack.QueueBehavior({
lazy: config.lazy,
remove: true
}));
return stackEls;
}
/**
* Chains all the separate stack elements into a single usable stack.
* If an element is missing a necessary method then it will have a pass-through method applied.
* @param {Array} stackElements An array of stack elements to be linked.
* @return {easyXDM.stack.StackElement} The last element in the chain.
*/
function chainStack(stackElements){
var stackEl, defaults = {
incoming: function(message, origin){
this.up.incoming(message, origin);
},
outgoing: function(message, recipient){
this.down.outgoing(message, recipient);
},
callback: function(success){
this.up.callback(success);
},
init: function(){
this.down.init();
},
destroy: function(){
this.down.destroy();
}
};
for (var i = 0, len = stackElements.length; i < len; i++) {
stackEl = stackElements[i];
apply(stackEl, defaults, true);
if (i !== 0) {
stackEl.down = stackElements[i - 1];
}
if (i !== len - 1) {
stackEl.up = stackElements[i + 1];
}
}
return stackEl;
}
/**
* This will remove a stackelement from its stack while leaving the stack functional.
* @param {Object} element The elment to remove from the stack.
*/
function removeFromStack(element){
element.up.down = element.down;
element.down.up = element.up;
element.up = element.down = null;
}
/*
* Export the main object and any other methods applicable
*/
/**
* @class easyXDM
* A javascript library providing cross-browser, cross-domain messaging/RPC.
* @version 2.4.5.10
* @singleton
*/
global.easyXDM = {
/**
* The version of the library
* @type {string}
*/
version: "2.4.5.10",
/**
* This is a map containing all the query parameters passed to the document.
* All the values has been decoded using decodeURIComponent.
* @type {object}
*/
query: query,
/**
* @private
*/
stack: {},
/**
* Applies properties from the source object to the target object.
* @param {object} target The target of the properties.
* @param {object} source The source of the properties.
* @param {boolean} noOverwrite Set to True to only set non-existing properties.
*/
apply: apply,
/**
* Runs an asynchronous request using XMLHttpRequest
* @param {object} config This object can have the following properties
*
* - url: string
The url to request.
* - method: string
POST, HEAD or GET.
* - data: object
Any data that should be sent.
* - type: string
The type of data to retrieve. If set to 'json' then the result will be parsed.
* - success: function
The callback function for successfull requests.
* - error: function
The callback function for errors.
*
*/
ajax: ajax,
/**
* A safe implementation of HTML5 JSON. Feature testing is used to make sure the implementation works.
* @return {JSON} A valid JSON conforming object, or null if not found.
*/
getJSONObject: getJSON,
/**
* This will add a function to the queue of functions to be run once the DOM reaches a ready state.
* If functions are added after this event then they will be executed immediately.
* @param {function} fn The function to add
* @param {object} scope An optional scope for the function to be called with.
*/
whenReady: whenReady
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global console, _FirebugCommandLine, easyXDM, window, escape, unescape, isHostObject, undef, _trace, isReady, emptyFn */
var debug = {
_deferred: [],
flush: function(){
this.trace("... deferred messages ...");
for (var i = 0, len = this._deferred.length; i < len; i++) {
this.trace(this._deferred[i]);
}
this._deferred.length = 0;
this.trace("... end of deferred messages ...");
},
/**
* Logs the message to console.log if available
* @param {String} msg The message to log
*/
log: function(msg){
// Uses memoizing to cache the implementation
if (!isHostObject(window, "console") || undef(console.log)) {
/**
* Sets log to be an empty function since we have no output available
* @ignore
*/
this.log = emptyFn;
}
else {
/**
* Sets log to be a wrapper around console.log
* @ignore
* @param {String} msg
*/
this.log = function(msg){
console.log(location.host + "-" + new Date().valueOf() + ":" + msg);
};
}
this.log(msg);
},
/**
* Will try to trace the given message either to a DOMElement with the id "log",
* or by using console.info.
* @param {String} msg The message to trace
*/
trace: function(msg){
// Uses memoizing to cache the implementation
if (!isReady) {
if (this._deferred.length === 0) {
easyXDM.whenReady(debug.flush, debug);
}
this._deferred.push(msg);
this.log(msg);
}
else {
var el = document.getElementById("log");
// is there a log element present?
if (el) {
/**
* Sets trace to be a function that outputs the messages to the DOMElement with id "log"
* @ignore
* @param {String} msg
*/
this.trace = function(msg){
try {
el.appendChild(document.createElement("div")).appendChild(document.createTextNode(location.host + "-" + new Date().valueOf() + ":" + msg));
el.scrollTop = el.scrollHeight;
}
catch (e) {
//In case we are unloading
}
};
}
else if (isHostObject(window, "console") && !undef(console.info)) {
/**
* Sets trace to be a wrapper around console.info
* @ignore
* @param {String} msg
*/
this.trace = function(msg){
console.info(location.host + "-" + new Date().valueOf() + ":" + msg);
};
}
else {
/**
* Create log window
* @ignore
*/
var domain = location.host, windowname = domain.replace(/\.|:/g, "") + "easyxdm_log", logWin;
try {
logWin = window.open("", windowname, "width=800,height=200,status=0,navigation=0,scrollbars=1");
}
catch (e) {
}
if (logWin) {
var doc = logWin.document;
el = doc.getElementById("log");
if (!el) {
doc.write("easyXDM log " + domain + "");
doc.write("");
doc.close();
el = doc.getElementById("log");
}
this.trace = function(msg){
try {
el.appendChild(doc.createElement("div")).appendChild(doc.createTextNode(location.host + "-" + new Date().valueOf() + ":" + msg));
el.scrollTop = el.scrollHeight;
}
catch (e) {
//In case we are unloading
}
};
this.trace("---- new logger at " + location.href);
}
if (!el) {
// We are unable to use any logging
this.trace = emptyFn;
}
}
this.trace(msg);
}
},
/**
* Creates a method usable for tracing.
* @param {String} name The name the messages should be marked with
* @return {Function} A function that accepts a single string as argument.
*/
getTracer: function(name){
return function(msg){
debug.trace(name + ": " + msg);
};
}
};
debug.log("easyXDM present on '" + location.href);
easyXDM.Debug = debug;
_trace = debug.getTracer("{Private}");
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, isHostObject, isHostMethod, un, on, createFrame, debug */
/**
* @class easyXDM.DomHelper
* Contains methods for dealing with the DOM
* @singleton
*/
easyXDM.DomHelper = {
/**
* Provides a consistent interface for adding eventhandlers
* @param {Object} target The target to add the event to
* @param {String} type The name of the event
* @param {Function} listener The listener
*/
on: on,
/**
* Provides a consistent interface for removing eventhandlers
* @param {Object} target The target to remove the event from
* @param {String} type The name of the event
* @param {Function} listener The listener
*/
un: un,
/**
* Checks for the presence of the JSON object.
* If it is not present it will use the supplied path to load the JSON2 library.
* This should be called in the documents head right after the easyXDM script tag.
* http://json.org/json2.js
* @param {String} path A valid path to json2.js
*/
requiresJSON: function(path){
if (!isHostObject(window, "JSON")) {
debug.log("loading external JSON");
document.write('');
}
else {
debug.log("native JSON found");
}
}
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, debug */
(function(){
// The map containing the stored functions
var _map = {};
/**
* @class easyXDM.Fn
* This contains methods related to function handling, such as storing callbacks.
* @singleton
* @namespace easyXDM
*/
easyXDM.Fn = {
/**
* Stores a function using the given name for reference
* @param {String} name The name that the function should be referred by
* @param {Function} fn The function to store
* @namespace easyXDM.fn
*/
set: function(name, fn){
this._trace("storing function " + name);
_map[name] = fn;
},
/**
* Retrieves the function referred to by the given name
* @param {String} name The name of the function to retrieve
* @param {Boolean} del If the function should be deleted after retrieval
* @return {Function} The stored function
* @namespace easyXDM.fn
*/
get: function(name, del){
this._trace("retrieving function " + name);
var fn = _map[name];
if (!fn) {
this._trace(name + " not found");
}
if (del) {
delete _map[name];
}
return fn;
}
};
easyXDM.Fn._trace = debug.getTracer("easyXDM.Fn");
}());
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, chainStack, prepareTransportStack, getLocation, debug */
/**
* @class easyXDM.Socket
* This class creates a transport channel between two domains that is usable for sending and receiving string-based messages.
* The channel is reliable, supports queueing, and ensures that the message originates from the expected domain.
* Internally different stacks will be used depending on the browsers features and the available parameters.
* How to set up
* Setting up the provider:
*
* var socket = new easyXDM.Socket({
* local: "name.html",
* onReady: function(){
* // you need to wait for the onReady callback before using the socket
* socket.postMessage("foo-message");
* },
* onMessage: function(message, origin) {
* alert("received " + message + " from " + origin);
* }
* });
*
* Setting up the consumer:
*
* var socket = new easyXDM.Socket({
* remote: "http://remotedomain/page.html",
* remoteHelper: "http://remotedomain/name.html",
* onReady: function(){
* // you need to wait for the onReady callback before using the socket
* socket.postMessage("foo-message");
* },
* onMessage: function(message, origin) {
* alert("received " + message + " from " + origin);
* }
* });
*
* If you are unable to upload the name.html
file to the consumers domain then remove removeHelper
property
* and the transport will fall back to using FMI instead of the window.name to transport messages.
* @namespace easyXDM
* @constructor
* @cfg {String/Window} local The url to the local name.html document, a local static file, or a reference to the local window.
* @cfg {Boolean} lazy (Consumer only) Set this to true if you want easyXDM to defer creating the transport until really needed.
* @cfg {String} remote (Consumer only) The url to the providers document.
* @cfg {String} remoteHelper (Consumer only) The url to the remote name.html file. This is to support NameTransport as a fallback. Optional.
* @cfg {Number} delay The number of milliseconds easyXDM should try to get a reference to the local window. Optional, defaults to 2000.
* @cfg {Number} interval The interval used when polling for messages. Optional, defaults to 300.
* @cfg {String} channel (Consumer only) The name of the channel to use. Can be used to set consistent iframe names. Must be unique. Optional.
* @cfg {Function} onMessage The method that should handle incoming messages.
This method should accept two arguments, the message as a string, and the origin as a string. Optional.
* @cfg {Function} onReady A method that should be called when the transport is ready. Optional.
* @cfg {DOMElement|String} container (Consumer only) The element, or the id of the element that the primary iframe should be inserted into. If not set then the iframe will be positioned off-screen. Optional.
* @cfg {Array/String} acl (Provider only) Here you can specify which '[protocol]://[domain]' patterns that should be allowed to act as the consumer towards this provider.
* This can contain the wildcards ? and *. Examples are 'http://example.com', '*.foo.com' and '*dom?.com'. If you want to use reqular expressions then you pattern needs to start with ^ and end with $.
* If none of the patterns match an Error will be thrown.
* @cfg {Object} props (Consumer only) Additional properties that should be applied to the iframe. This can also contain nested objects e.g: {style:{width:"100px", height:"100px"}}
.
* Properties such as 'name' and 'src' will be overrided. Optional.
*/
easyXDM.Socket = function(config){
var trace = debug.getTracer("easyXDM.Socket");
trace("constructor");
var stack = chainStack(prepareTransportStack(config).concat([{
incoming: function(message, origin){
config.onMessage(message, origin);
},
callback: function(success){
if (config.onReady) {
config.onReady(success);
}
}
}])), recipient = getLocation(config.remote);
/**
* Initiates the destruction of the stack.
*/
this.destroy = function(){
stack.destroy();
};
/**
* Posts a message to the remote end of the channel
* @param {String} message The message to send
*/
this.postMessage = function(message){
stack.outgoing(message, recipient);
};
stack.init();
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, undef,, chainStack, prepareTransportStack, debug */
/**
* @class easyXDM.Rpc
* Creates a proxy object that can be used to call methods implemented on the remote end of the channel, and also to provide the implementation
* of methods to be called from the remote end.
* The instantiated object will have methods matching those specified in config.remote
.
* This requires the JSON object present in the document, either natively, using json.org's json2 or as a wrapper around library spesific methods.
* How to set up
*
* var rpc = new easyXDM.Rpc({
* // this configuration is equal to that used by the Socket.
* remote: "http://remotedomain/...",
* onReady: function(){
* // you need to wait for the onReady callback before using the proxy
* rpc.foo(...
* }
* },{
* local: {..},
* remote: {..}
* });
*
*
* Exposing functions (procedures)
*
* var rpc = new easyXDM.Rpc({
* ...
* },{
* local: {
* nameOfMethod: {
* method: function(arg1, arg2, success, error){
* ...
* }
* },
* // with shorthand notation
* nameOfAnotherMethod: function(arg1, arg2, success, error){
* }
* },
* remote: {...}
* });
*
* The function referenced by [method] will receive the passed arguments followed by the callback functions success
and error
.
* To send a successfull result back you can use
*
* return foo;
*
* or
*
* success(foo);
*
* To return an error you can use
*
* throw new Error("foo error");
*
* or
*
* error("foo error");
*
*
* Defining remotely exposed methods (procedures/notifications)
* The definition of the remote end is quite similar:
*
* var rpc = new easyXDM.Rpc({
* ...
* },{
* local: {...},
* remote: {
* nameOfMethod: {}
* }
* });
*
* To call a remote method use
*
* rpc.nameOfMethod("arg1", "arg2", function(value) {
* alert("success: " + value);
* }, function(message) {
* alert("error: " + message + );
* });
*
* Both the success
and errror
callbacks are optional.
* When called with no callback a JSON-RPC 2.0 notification will be executed.
* Be aware that you will not be notified of any errors with this method.
*
* Specifying a custom serializer
* If you do not want to use the JSON2 library for non-native JSON support, but instead capabilities provided by some other library
* then you can specify a custom serializer using serializer: foo
*
* var rpc = new easyXDM.Rpc({
* ...
* },{
* local: {...},
* remote: {...},
* serializer : {
* parse: function(string){ ... },
* stringify: function(object) {...}
* }
* });
*
* If serializer
is set then the class will not attempt to use the native implementation.
* @namespace easyXDM
* @constructor
* @param {Object} config The underlying transports configuration. See easyXDM.Socket for available parameters.
* @param {Object} jsonRpcConfig The description of the interface to implement.
*/
easyXDM.Rpc = function(config, jsonRpcConfig){
var trace = debug.getTracer("easyXDM.Rpc");
trace("constructor");
// expand shorthand notation
if (jsonRpcConfig.local) {
for (var method in jsonRpcConfig.local) {
if (jsonRpcConfig.local.hasOwnProperty(method)) {
var member = jsonRpcConfig.local[method];
if (typeof member === "function") {
jsonRpcConfig.local[method] = {
method: member
};
}
}
}
}
var stack = chainStack(prepareTransportStack(config).concat([new easyXDM.stack.RpcBehavior(this, jsonRpcConfig), {
callback: function(success){
if (config.onReady) {
config.onReady(success);
}
}
}]));
/**
* Initiates the destruction of the stack.
*/
this.destroy = function(){
stack.destroy();
};
stack.init();
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, getLocation, appendQueryParameters, createFrame, debug, un, on, apply, whenReady*/
/**
* @class easyXDM.stack.SameOriginTransport
* SameOriginTransport is a transport class that can be used when both domains have the same origin.
* This can be useful for testing and for when the main application supports both internal and external sources.
* @namespace easyXDM.stack
* @constructor
* @param {Object} config The transports configuration.
* @cfg {String} remote The remote document to communicate with.
*/
easyXDM.stack.SameOriginTransport = function(config){
var trace = debug.getTracer("easyXDM.stack.SameOriginTransport");
trace("constructor");
var pub, frame, send, targetOrigin;
return (pub = {
outgoing: function(message, domain, fn){
send(message);
if (fn) {
fn();
}
},
destroy: function(){
trace("destroy");
if (frame) {
frame.parentNode.removeChild(frame);
frame = null;
}
},
onDOMReady: function(){
trace("init");
targetOrigin = getLocation(config.remote);
if (config.isHost) {
// set up the iframe
apply(config.props, {
src: appendQueryParameters(config.remote, {
xdm_e: location.protocol + "//" + location.host + location.pathname,
xdm_c: config.channel,
xdm_p: 4 // 4 = SameOriginTransport
})
});
frame = createFrame(config);
easyXDM.Fn.set(config.channel, function(sendFn){
send = sendFn;
setTimeout(function(){
pub.up.callback(true);
}, 0);
return function(msg){
pub.up.incoming(msg, targetOrigin);
};
});
}
else {
send = parent.easyXDM.Fn.get(config.channel, true)(function(msg){
pub.up.incoming(msg, targetOrigin);
});
pub.up.callback(true);
}
},
init: function(){
whenReady(pub.onDOMReady, pub);
}
});
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, getLocation, appendQueryParameters, createFrame, debug, un, on, apply, whenReady*/
/**
* @class easyXDM.stack.PostMessageTransport
* PostMessageTransport is a transport class that uses HTML5 postMessage for communication.
* http://msdn.microsoft.com/en-us/library/ms644944(VS.85).aspx
* https://developer.mozilla.org/en/DOM/window.postMessage
* @namespace easyXDM.stack
* @constructor
* @param {Object} config The transports configuration.
* @cfg {String} remote The remote domain to communicate with.
*/
easyXDM.stack.PostMessageTransport = function(config){
var trace = debug.getTracer("easyXDM.stack.PostMessageTransport");
trace("constructor");
var pub, // the public interface
frame, // the remote frame, if any
callerWindow, // the window that we will call with
targetOrigin; // the domain to communicate with
/**
* Resolves the origin from the event object
* @private
* @param {Object} event The messageevent
* @return {String} The scheme, host and port of the origin
*/
function _getOrigin(event){
if (event.origin) {
// This is the HTML5 property
return event.origin;
}
if (event.uri) {
// From earlier implementations
return getLocation(event.uri);
}
if (event.domain) {
// This is the last option and will fail if the
// origin is not using the same schema as we are
return location.protocol + "//" + event.domain;
}
throw "Unable to retrieve the origin of the event";
}
/**
* This is the main implementation for the onMessage event.
* It checks the validity of the origin and passes the message on if appropriate.
* @private
* @param {Object} event The messageevent
*/
function _window_onMessage(event){
var origin = _getOrigin(event);
trace("received message '" + event.data + "' from " + origin);
if (origin == targetOrigin && event.data.substring(0, config.channel.length + 1) == config.channel + " ") {
pub.up.incoming(event.data.substring(config.channel.length + 1), origin);
}
}
return (pub = {
outgoing: function(message, domain, fn){
callerWindow.postMessage(config.channel + " " + message, domain || targetOrigin);
if (fn) {
fn();
}
},
destroy: function(){
trace("destroy");
un(window, "message", _window_onMessage);
if (frame) {
callerWindow = null;
frame.parentNode.removeChild(frame);
frame = null;
}
},
onDOMReady: function(){
trace("init");
targetOrigin = getLocation(config.remote);
if (config.isHost) {
// add the event handler for listening
on(window, "message", function waitForReady(event){
if (event.data == config.channel + "-ready") {
trace("firing onReady");
// replace the eventlistener
callerWindow = ("postMessage" in frame.contentWindow) ? frame.contentWindow : frame.contentWindow.document;
un(window, "message", waitForReady);
on(window, "message", _window_onMessage);
setTimeout(function(){
pub.up.callback(true);
}, 0);
}
});
// set up the iframe
apply(config.props, {
src: appendQueryParameters(config.remote, {
xdm_e: location.protocol + "//" + location.host,
xdm_c: config.channel,
xdm_p: 1 // 1 = PostMessage
})
});
frame = createFrame(config);
}
else {
// add the event handler for listening
on(window, "message", _window_onMessage);
callerWindow = ("postMessage" in window.parent) ? window.parent : window.parent.document;
callerWindow.postMessage(config.channel + "-ready", targetOrigin);
setTimeout(function(){
pub.up.callback(true);
}, 0);
}
},
init: function(){
whenReady(pub.onDOMReady, pub);
}
});
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, getLocation, appendQueryParameters, createFrame, debug, apply, query, whenReady*/
/**
* @class easyXDM.stack.FrameElementTransport
* FrameElementTransport is a transport class that can be used with Gecko-browser as these allow passing variables using the frameElement property.
* Security is maintained as Gecho uses Lexical Authorization to determine under which scope a function is running.
* @namespace easyXDM.stack
* @constructor
* @param {Object} config The transports configuration.
* @cfg {String} remote The remote document to communicate with.
*/
easyXDM.stack.FrameElementTransport = function(config){
var trace = debug.getTracer("easyXDM.stack.FrameElementTransport");
trace("constructor");
var pub, frame, send, targetOrigin;
return (pub = {
outgoing: function(message, domain, fn){
send.call(this, message);
if (fn) {
fn();
}
},
destroy: function(){
trace("destroy");
if (frame) {
frame.parentNode.removeChild(frame);
frame = null;
}
},
onDOMReady: function(){
trace("init");
targetOrigin = getLocation(config.remote);
if (config.isHost) {
// set up the iframe
apply(config.props, {
src: appendQueryParameters(config.remote, {
xdm_e: location.protocol + "//" + location.host + location.pathname,
xdm_c: config.channel,
xdm_p: 5 // 5 = FrameElementTransport
})
});
frame = createFrame(config);
frame.fn = function(sendFn){
delete frame.fn;
send = sendFn;
setTimeout(function(){
pub.up.callback(true);
}, 0);
// remove the function so that it cannot be used to overwrite the send function later on
return function(msg){
pub.up.incoming(msg, targetOrigin);
};
};
}
else {
if (document.referrer && document.referrer != query.xdm_e) {
window.parent.location = query.xdm_e;
}
else {
if (document.referrer != query.xdm_e) {
// This is to mitigate origin-spoofing
window.parent.location = query.xdm_e;
}
send = window.frameElement.fn(function(msg){
pub.up.incoming(msg, targetOrigin);
});
pub.up.callback(true);
}
}
},
init: function(){
whenReady(pub.onDOMReady, pub);
}
});
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global global, getNixProxy, easyXDM, window, escape, unescape, getLocation, appendQueryParameters, createFrame, debug, un, on, isHostMethod, apply, query, whenReady*/
/**
* @class easyXDM.stack.NixTransport
* NixTransport is a transport class that uses the strange fact that in IE <8, the window.opener property can be written to and read from all windows.
* This is used to pass methods that are able to relay messages back and forth. To avoid context-leakage a VBScript (COM) object is used to relay all the strings.
* This transport is loosely based on the work done by Shindig
* @namespace easyXDM.stack
* @constructor
* @param {Object} config The transports configuration.
* @cfg {String} remote The remote domain to communicate with.
* @cfg {String} secret the pre-shared secret used to secure the communication.
*/
easyXDM.stack.NixTransport = function(config){
var trace = debug.getTracer("easyXDM.stack.NixMessageTransport");
trace("constructor");
var pub, // the public interface
frame, send, targetOrigin, proxy;
return (pub = {
outgoing: function(message, domain, fn){
send(message);
if (fn) {
fn();
}
},
destroy: function(){
trace("destroy");
proxy = null;
if (frame) {
frame.parentNode.removeChild(frame);
frame = null;
}
},
onDOMReady: function(){
trace("init");
targetOrigin = getLocation(config.remote);
if (config.isHost) {
try {
if (!isHostMethod(window, "getNixProxy")) {
window.execScript('Class NixProxy\n' +
' Private m_parent, m_child, m_Auth\n' +
'\n' +
' Public Sub SetParent(obj, auth)\n' +
' If isEmpty(m_Auth) Then m_Auth = auth\n' +
' SET m_parent = obj\n' +
' End Sub\n' +
' Public Sub SetChild(obj)\n' +
' SET m_child = obj\n' +
' m_parent.ready()\n' +
' End Sub\n' +
'\n' +
// The auth string, which is a pre-shared key between the parent and the child,
// and that can only be set once by the parent, secures the communication, and also serves to provide
// 'proof' of the origin of the messages.
// Before passing the message on to the recipent we convert the message into a primitive,
// this mitigates modifying .toString as an attack vector.
' Public Sub SendToParent(data, auth)\n' +
' If m_Auth = auth Then m_parent.send(CStr(data))\n' +
' End Sub\n' +
' Public Sub SendToChild(data, auth)\n' +
' If m_Auth = auth Then m_child.send(CStr(data))\n' +
' End Sub\n' +
'End Class\n' +
'Function getNixProxy()\n' +
' Set GetNixProxy = New NixProxy\n' +
'End Function\n', 'vbscript');
}
proxy = getNixProxy();
proxy.SetParent({
send: function(msg){
trace("received message");
pub.up.incoming(msg, targetOrigin);
},
ready: function(){
setTimeout(function(){
trace("firing onReady");
pub.up.callback(true);
}, 0);
}
}, config.secret);
send = function(msg){
trace("sending message");
proxy.SendToChild(msg, config.secret);
};
}
catch (e1) {
throw new Error("Could not set up VBScript NixProxy:" + e1.message);
}
// set up the iframe
apply(config.props, {
src: appendQueryParameters(config.remote, {
xdm_e: location.protocol + "//" + location.host + location.pathname + location.search,
xdm_c: config.channel,
xdm_s: config.secret,
xdm_p: 3 // 3 = NixTransport
})
});
frame = createFrame(config);
frame.contentWindow.opener = proxy;
}
else {
if (document.referrer && document.referrer != query.xdm_e) {
window.parent.location = query.xdm_e;
}
else {
if (document.referrer != query.xdm_e) {
// This is to mitigate origin-spoofing
window.parent.location = query.xdm_e;
}
try {
// by storing this in a variable we negate replacement attacks
proxy = window.opener;
}
catch (e2) {
throw new Error("Cannot access window.opener");
}
proxy.SetChild({
send: function(msg){
// the timeout is necessary to have execution continue in the correct context
global.setTimeout(function(){
trace("received message");
pub.up.incoming(msg, targetOrigin);
}, 0);
}
});
send = function(msg){
trace("sending");
proxy.SendToParent(msg, config.secret);
};
setTimeout(function(){
trace("firing onReady");
pub.up.callback(true);
}, 0);
}
}
},
init: function(){
whenReady(pub.onDOMReady, pub);
}
});
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, undef, getLocation, appendQueryParameters, resolveUrl, createFrame, debug, un, apply, whenReady*/
/**
* @class easyXDM.stack.NameTransport
* NameTransport uses the window.name property to relay data.
* The local
parameter needs to be set on both the consumer and provider,
* and the remoteHelper
parameter needs to be set on the consumer.
* @constructor
* @param {Object} config The transports configuration.
* @cfg {String} remoteHelper The url to the remote instance of hash.html - this is only needed for the host.
* @namespace easyXDM.stack
*/
easyXDM.stack.NameTransport = function(config){
var trace = debug.getTracer("easyXDM.stack.NameTransport");
trace("constructor");
if (config.isHost && undef(config.remoteHelper)) {
trace("missing remoteHelper");
throw new Error("missing remoteHelper");
}
var pub; // the public interface
var isHost, callerWindow, remoteWindow, readyCount, callback, remoteOrigin, remoteUrl;
function _sendMessage(message){
var url = config.remoteHelper + (isHost ? "#_3" : "#_2") + config.channel;
trace("sending message " + message);
trace("navigating to '" + url + "'");
callerWindow.contentWindow.sendMessage(message, url);
}
function _onReady(){
if (isHost) {
if (++readyCount === 2 || !isHost) {
pub.up.callback(true);
}
}
else {
_sendMessage("ready");
trace("calling onReady");
pub.up.callback(true);
}
}
function _onMessage(message){
trace("received message " + message);
pub.up.incoming(message, remoteOrigin);
}
function _onLoad(){
if (callback) {
setTimeout(function(){
callback(true);
}, 0);
}
}
return (pub = {
outgoing: function(message, domain, fn){
callback = fn;
_sendMessage(message);
},
destroy: function(){
trace("destroy");
callerWindow.parentNode.removeChild(callerWindow);
callerWindow = null;
if (isHost) {
remoteWindow.parentNode.removeChild(remoteWindow);
remoteWindow = null;
}
},
onDOMReady: function(){
trace("init");
isHost = config.isHost;
readyCount = 0;
remoteOrigin = getLocation(config.remote);
config.local = resolveUrl(config.local);
if (isHost) {
// Register the callback
easyXDM.Fn.set(config.channel, function(message){
trace("received initial message " + message);
if (isHost && message === "ready") {
// Replace the handler
easyXDM.Fn.set(config.channel, _onMessage);
_onReady();
}
});
// Set up the frame that points to the remote instance
remoteUrl = appendQueryParameters(config.remote, {
xdm_e: config.local,
xdm_c: config.channel,
xdm_p: 2
});
apply(config.props, {
src: remoteUrl + '#' + config.channel,
name: config.channel
});
remoteWindow = createFrame(config);
}
else {
config.remoteHelper = config.remote;
easyXDM.Fn.set(config.channel, _onMessage);
}
// Set up the iframe that will be used for the transport
callerWindow = createFrame({
props: {
src: config.local + "#_4" + config.channel
},
onLoad: function(){
// Remove the handler
un(callerWindow, "load", callerWindow.loadFn);
easyXDM.Fn.set(config.channel + "_load", _onLoad);
(function test(){
if (typeof callerWindow.contentWindow.sendMessage == "function") {
_onReady();
}
else {
setTimeout(test, 50);
}
}());
}
});
},
init: function(){
whenReady(pub.onDOMReady, pub);
}
});
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, getLocation, createFrame, debug, un, on, apply, whenReady*/
/**
* @class easyXDM.stack.HashTransport
* HashTransport is a transport class that uses the IFrame URL Technique for communication.
* http://msdn.microsoft.com/en-us/library/bb735305.aspx
* @namespace easyXDM.stack
* @constructor
* @param {Object} config The transports configuration.
* @cfg {String/Window} local The url to the local file used for proxying messages, or the local window.
* @cfg {Number} delay The number of milliseconds easyXDM should try to get a reference to the local window.
* @cfg {Number} interval The interval used when polling for messages.
*/
easyXDM.stack.HashTransport = function(config){
var trace = debug.getTracer("easyXDM.stack.HashTransport");
trace("constructor");
var pub;
var me = this, isHost, _timer, pollInterval, _lastMsg, _msgNr, _listenerWindow, _callerWindow;
var useParent, _remoteOrigin;
function _sendMessage(message){
trace("sending message '" + (_msgNr + 1) + " " + message + "' to " + _remoteOrigin);
if (!_callerWindow) {
trace("no caller window");
return;
}
var url = config.remote + "#" + (_msgNr++) + "_" + message;
((isHost || !useParent) ? _callerWindow.contentWindow : _callerWindow).location = url;
}
function _handleHash(hash){
_lastMsg = hash;
trace("received message '" + _lastMsg + "' from " + _remoteOrigin);
pub.up.incoming(_lastMsg.substring(_lastMsg.indexOf("_") + 1), _remoteOrigin);
}
/**
* Checks location.hash for a new message and relays this to the receiver.
* @private
*/
function _pollHash(){
if (!_listenerWindow) {
return;
}
var href = _listenerWindow.location.href, hash = "", indexOf = href.indexOf("#");
if (indexOf != -1) {
hash = href.substring(indexOf);
}
if (hash && hash != _lastMsg) {
trace("poll: new message");
_handleHash(hash);
}
}
function _attachListeners(){
trace("starting polling");
_timer = setInterval(_pollHash, pollInterval);
}
return (pub = {
outgoing: function(message, domain){
_sendMessage(message);
},
destroy: function(){
window.clearInterval(_timer);
if (isHost || !useParent) {
_callerWindow.parentNode.removeChild(_callerWindow);
}
_callerWindow = null;
},
onDOMReady: function(){
isHost = config.isHost;
pollInterval = config.interval;
_lastMsg = "#" + config.channel;
_msgNr = 0;
useParent = config.useParent;
_remoteOrigin = getLocation(config.remote);
if (isHost) {
config.props = {
src: config.remote,
name: "local_" + config.channel
};
if (useParent) {
config.onLoad = function(){
_listenerWindow = window;
_attachListeners();
pub.up.callback(true);
};
}
else {
var tries = 0, max = config.delay / 50;
(function getRef(){
if (++tries > max) {
trace("unable to get reference to _listenerWindow, giving up");
throw new Error("Unable to reference listenerwindow");
}
try {
_listenerWindow = _callerWindow.contentWindow.frames["remote_" + config.channel];
}
catch (ex) {
}
if (_listenerWindow) {
_attachListeners();
trace("got a reference to _listenerWindow");
pub.up.callback(true);
}
else {
setTimeout(getRef, 50);
}
}());
}
_callerWindow = createFrame(config);
}
else {
_listenerWindow = window;
_attachListeners();
if (useParent) {
_callerWindow = parent;
pub.up.callback(true);
}
else {
apply(config, {
props: {
src: config.remote + "#" + config.channel + new Date(),
name: "remote_" + config.channel
},
onLoad: function(){
pub.up.callback(true);
}
});
_callerWindow = createFrame(config);
}
}
},
init: function(){
whenReady(pub.onDOMReady, pub);
}
});
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, debug */
/**
* @class easyXDM.stack.ReliableBehavior
* This is a behavior that tries to make the underlying transport reliable by using acknowledgements.
* @namespace easyXDM.stack
* @constructor
* @param {Object} config The behaviors configuration.
* @cfg {Number} timeout How long it should wait before resending. Default is 5. Optional.
* @cfg {Number} tries How many times it should try before giving up.
*/
easyXDM.stack.ReliableBehavior = function(config){
var trace = debug.getTracer("easyXDM.stack.ReliableBehavior");
trace("constructor");
var pub, // the public interface
timer, // timer to wait for acks
current, // the current message beging sent
next, // the next message to be sent, to support piggybacking acks
sendId = 0, // the id of the last message sent
sendCount = 0, // how many times we hav tried resending
maxTries = config.tries || 5, timeout = config.timeout, //
receiveId = 0, // the id of the last message received
callback; // the callback to execute when we have a confirmed success/failure
return (pub = {
incoming: function(message, origin){
var indexOf = message.indexOf("_"), ack = parseInt(message.substring(0, indexOf), 10), id;
trace("received ack: " + ack + ", last sent was: " + sendId);
message = message.substring(indexOf + 1);
indexOf = message.indexOf("_");
id = parseInt(message.substring(0, indexOf), 10);
indexOf = message.indexOf("_");
message = message.substring(indexOf + 1);
if (timer && ack === sendId) {
window.clearTimeout(timer);
timer = null;
trace("message delivered");
if (callback) {
setTimeout(function(){
callback(true);
}, 0);
}
}
if (id !== 0) {
if (id !== receiveId) {
receiveId = id;
message = message.substring(id.length + 1);
trace("sending ack, passing on " + message);
pub.down.outgoing(id + "_0_ack", origin);
// we must give the other end time to pick up the ack
setTimeout(function(){
pub.up.incoming(message, origin);
}, config.timeout / 2);
}
else {
trace("duplicate msgid " + id + ", resending ack");
pub.down.outgoing(id + "_0_ack", origin);
}
}
},
outgoing: function(message, origin, fn){
callback = fn;
sendCount = 0;
current = {
data: receiveId + "_" + (++sendId) + "_" + message,
origin: origin
};
// Keep resending until we have an ack
(function send(){
timer = null;
if (++sendCount > maxTries) {
if (callback) {
trace("delivery failed");
setTimeout(function(){
callback(false);
}, 0);
}
}
else {
trace((sendCount === 1 ? "sending " : "resending ") + sendId + ", tryCount " + sendCount);
pub.down.outgoing(current.data, current.origin);
timer = setTimeout(send, config.timeout);
}
}());
},
destroy: function(){
if (timer) {
window.clearInterval(timer);
}
pub.down.destroy();
}
});
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, debug, undef, removeFromStack*/
/**
* @class easyXDM.stack.QueueBehavior
* This is a behavior that enables queueing of messages.
* It will buffer incoming messages and dispach these as fast as the underlying transport allows.
* This will also fragment/defragment messages so that the outgoing message is never bigger than the
* set length.
* @namespace easyXDM.stack
* @constructor
* @param {Object} config The behaviors configuration. Optional.
* @cfg {Number} maxLength The maximum length of each outgoing message. Set this to enable fragmentation.
*/
easyXDM.stack.QueueBehavior = function(config){
var trace = debug.getTracer("easyXDM.stack.QueueBehavior");
trace("constructor");
var pub, queue = [], waiting = true, incoming = "", destroying, maxLength = 0, lazy = false, doFragment = false;
function dispatch(){
if (config.remove && queue.length === 0) {
trace("removing myself from the stack");
removeFromStack(pub);
return;
}
if (waiting || queue.length === 0 || destroying) {
return;
}
trace("dispatching from queue");
waiting = true;
var message = queue.shift();
pub.down.outgoing(message.data, message.origin, function(success){
waiting = false;
if (message.callback) {
setTimeout(function(){
message.callback(success);
}, 0);
}
dispatch();
});
}
return (pub = {
init: function(){
if (undef(config)) {
config = {};
}
if (config.maxLength) {
maxLength = config.maxLength;
doFragment = true;
}
if (config.lazy) {
lazy = true;
}
else {
pub.down.init();
}
},
callback: function(success){
waiting = false;
var up = pub.up; // in case dispatch calls removeFromStack
dispatch();
up.callback(success);
},
incoming: function(message, origin){
if (doFragment) {
var indexOf = message.indexOf("_"), seq = parseInt(message.substring(0, indexOf), 10);
incoming += message.substring(indexOf + 1);
if (seq === 0) {
trace("received the last fragment");
if (config.encode) {
incoming = decodeURIComponent(incoming);
}
pub.up.incoming(incoming, origin);
incoming = "";
}
else {
trace("waiting for more fragments, seq=" + message);
}
}
else {
pub.up.incoming(message, origin);
}
},
outgoing: function(message, origin, fn){
if (config.encode) {
message = encodeURIComponent(message);
}
var fragments = [], fragment;
if (doFragment) {
// fragment into chunks
while (message.length !== 0) {
fragment = message.substring(0, maxLength);
message = message.substring(fragment.length);
fragments.push(fragment);
}
// enqueue the chunks
while ((fragment = fragments.shift())) {
trace("enqueuing");
queue.push({
data: fragments.length + "_" + fragment,
origin: origin,
callback: fragments.length === 0 ? fn : null
});
}
}
else {
queue.push({
data: message,
origin: origin,
callback: fn
});
}
if (lazy) {
pub.down.init();
}
else {
dispatch();
}
},
destroy: function(){
trace("destroy");
destroying = true;
pub.down.destroy();
}
});
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, undef, debug */
/**
* @class easyXDM.stack.VerifyBehavior
* This behavior will verify that communication with the remote end is possible, and will also sign all outgoing,
* and verify all incoming messages. This removes the risk of someone hijacking the iframe to send malicious messages.
* @namespace easyXDM.stack
* @constructor
* @param {Object} config The behaviors configuration.
* @cfg {Boolean} initiate If the verification should be initiated from this end.
*/
easyXDM.stack.VerifyBehavior = function(config){
var trace = debug.getTracer("easyXDM.stack.VerifyBehavior");
trace("constructor");
if (undef(config.initiate)) {
throw new Error("settings.initiate is not set");
}
var pub, mySecret, theirSecret, verified = false;
function startVerification(){
trace("requesting verification");
mySecret = Math.random().toString(16).substring(2);
pub.down.outgoing(mySecret);
}
return (pub = {
incoming: function(message, origin){
var indexOf = message.indexOf("_");
if (indexOf === -1) {
if (message === mySecret) {
trace("verified, calling callback");
pub.up.callback(true);
}
else if (!theirSecret) {
trace("returning secret");
theirSecret = message;
if (!config.initiate) {
startVerification();
}
pub.down.outgoing(message);
}
}
else {
if (message.substring(0, indexOf) === theirSecret) {
pub.up.incoming(message.substring(indexOf + 1), origin);
}
}
},
outgoing: function(message, origin, fn){
pub.down.outgoing(mySecret + "_" + message, origin, fn);
},
callback: function(success){
if (config.initiate) {
startVerification();
}
}
});
};
/*jslint evil: true, browser: true, immed: true, passfail: true, undef: true, newcap: true*/
/*global easyXDM, window, escape, unescape, undef, getJSON, debug, emptyFn */
/**
* @class easyXDM.stack.RpcBehavior
* This uses JSON-RPC 2.0 to expose local methods and to invoke remote methods and have responses returned over the the string based transport stack.
* Exposed methods can return values synchronous, asyncronous, or bet set up to not return anything.
* @namespace easyXDM.stack
* @constructor
* @param {Object} proxy The object to apply the methods to.
* @param {Object} config The definition of the local and remote interface to implement.
* @cfg {Object} local The local interface to expose.
* @cfg {Object} remote The remote methods to expose through the proxy.
* @cfg {Object} serializer The serializer to use for serializing and deserializing the JSON. Should be compatible with the HTML5 JSON object. Optional, will default to JSON.
*/
easyXDM.stack.RpcBehavior = function(proxy, config){
var trace = debug.getTracer("easyXDM.stack.RpcBehavior");
var pub, serializer = config.serializer || getJSON();
var _callbackCounter = 0, _callbacks = {};
/**
* Serializes and sends the message
* @private
* @param {Object} data The JSON-RPC message to be sent. The jsonrpc property will be added.
*/
function _send(data){
data.jsonrpc = "2.0";
pub.down.outgoing(serializer.stringify(data));
}
/**
* Creates a method that implements the given definition
* @private
* @param {Object} The method configuration
* @param {String} method The name of the method
* @return {Function} A stub capable of proxying the requested method call
*/
function _createMethod(definition, method){
var slice = Array.prototype.slice;
trace("creating method " + method);
return function(){
trace("executing method " + method);
var l = arguments.length, callback, message = {
method: method
};
if (l > 0 && typeof arguments[l - 1] === "function") {
//with callback, procedure
if (l > 1 && typeof arguments[l - 2] === "function") {
// two callbacks, success and error
callback = {
success: arguments[l - 2],
error: arguments[l - 1]
};
message.params = slice.call(arguments, 0, l - 2);
}
else {
// single callback, success
callback = {
success: arguments[l - 1]
};
message.params = slice.call(arguments, 0, l - 1);
}
_callbacks["" + (++_callbackCounter)] = callback;
message.id = _callbackCounter;
}
else {
// no callbacks, a notification
message.params = slice.call(arguments, 0);
}
// Send the method request
_send(message);
};
}
/**
* Executes the exposed method
* @private
* @param {String} method The name of the method
* @param {Number} id The callback id to use
* @param {Function} method The exposed implementation
* @param {Array} params The parameters supplied by the remote end
*/
function _executeMethod(method, id, fn, params){
if (!fn) {
trace("requested to execute non-existent procedure " + method);
if (id) {
_send({
id: id,
error: {
code: -32601,
message: "Procedure not found."
}
});
}
return;
}
trace("requested to execute procedure " + method);
var used = false, success, error;
if (id) {
success = function(result){
if (used) {
return;
}
used = true;
_send({
id: id,
result: result
});
};
error = function(message){
if (used) {
return;
}
used = true;
var msg = {
id: id,
error: {
code: -32099,
message: "Application error: " + message
}
};
if (typeof message == "object" && "data" in message) {
msg.data = message.data;
}
_send(msg);
};
}
else {
success = error = emptyFn;
}
// Call local method
try {
var result = fn.method.apply(fn.scope, params.concat([success, error]));
if (!undef(result)) {
success(result);
}
}
catch (ex1) {
error(ex1.message);
}
}
return (pub = {
incoming: function(message, origin){
var data = serializer.parse(message);
if (data.method) {
trace("received request to execute method " + data.method + (data.id ? (" using callback id " + data.id) : ""));
// A method call from the remote end
if (config.handle) {
config.handle(data, _send);
}
else {
_executeMethod(data.method, data.id, config.local[data.method], data.params);
}
}
else {
trace("received return value destined to callback with id " + data.id);
// A method response from the other end
var callback = _callbacks[data.id];
if (data.error) {
if (callback.error) {
callback.error(data.error);
}
else {
trace("unhandled error returned.");
}
}
else if (callback.success) {
callback.success(data.result);
}
delete _callbacks[data.id];
}
},
init: function(){
trace("init");
if (config.remote) {
trace("creating stubs");
// Implement the remote sides exposed methods
for (var method in config.remote) {
if (config.remote.hasOwnProperty(method)) {
proxy[method] = _createMethod(config.remote[method], method);
}
}
}
pub.down.init();
},
destroy: function(){
trace("destroy");
for (var method in config.remote) {
if (config.remote.hasOwnProperty(method) && proxy.hasOwnProperty(method)) {
delete proxy[method];
}
}
pub.down.destroy();
}
});
};
})(window, document, location, window.setTimeout, decodeURIComponent, encodeURIComponent);