blob: 9adf9a8c5e80375f3dccffaa949feb8d92683cba [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Remote procedure call library for gadget-to-container,
* container-to-gadget, and gadget-to-gadget (thru container) communication.
*
*
*/
var gadgets = gadgets || {};
/**
* @static
* @class Provides operations for making rpc calls.
* @name gadgets.rpc
*/
gadgets.rpc = function() {
// General constants.
var CALLBACK_NAME = '__cb';
var DEFAULT_NAME = '';
// Consts for FrameElement.
var FE_G2C_CHANNEL = '__g2c_rpc';
var FE_C2G_CHANNEL = '__c2g_rpc';
var services = {};
var iframePool = [];
var relayUrl = {};
var useLegacyProtocol = {};
var authToken = {};
var callId = 0;
var callbacks = {};
var setup = {};
var sameDomain = {};
var params = {};
// Load the authentication token for speaking to the container
// from the gadget's parameters, or default to '0' if not found.
if (gadgets.util) {
params = gadgets.util.getUrlParameters();
}
authToken['..'] = params.rpctoken || params.ifpctok || 0;
/*
* Return a short code representing the best available cross-domain
* message transport available to the browser.
*
* + For those browsers that support native messaging (various implementations
* of the HTML5 postMessage method), use that. Officially defined at
* http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html.
*
* postMessage is a native implementation of XDC. A page registers that
* it would like to receive messages by listening the the "message" event
* on the window (document in DPM) object. In turn, another page can
* raise that event by calling window.postMessage (document.postMessage
* in DPM) with a string representing the message and a string
* indicating on which domain the receiving page must be to receive
* the message. The target page will then have its "message" event raised
* if the domain matches and can, in turn, check the origin of the message
* and process the data contained within.
*
* wpm: postMessage on the window object.
* - Internet Explorer 8+
* - Safari (latest nightlies as of 26/6/2008)
* - Firefox 3+
* - Opera 9+
*
* dpm: postMessage on the document object.
* - Opera 8+
*
* + For Gecko-based browsers, the security model allows a child to call a
* function on the frameElement of the iframe, even if the child is in
* a different domain. This method is dubbed "frameElement" (fe).
*
* The ability to add and call such functions on the frameElement allows
* a bidirectional channel to be setup via the adding of simple function
* references on the frameElement object itself. In this implementation,
* when the container sets up the authentication information for that gadget
* (by calling setAuth(...)) it as well adds a special function on the
* gadget's iframe. This function can then be used by the gadget to send
* messages to the container. In turn, when the gadget tries to send a
* message, it checks to see if this function has its own function stored
* that can be used by the container to call the gadget. If not, the
* function is created and subsequently used by the container.
* Note that as a result, FE can only be used by a container to call a
* particular gadget *after* that gadget has called the container at
* least once via FE.
*
* fe: Gecko-specific frameElement trick.
* - Firefox 1+
*
* + For all others, we have a fallback mechanism known as "ifpc". IFPC
* exploits the fact that while same-origin policy prohibits a frame from
* accessing members on a window not in the same domain, that frame can,
* however, navigate the window heirarchy (via parent). This is exploited by
* having a page on domain A that wants to talk to domain B create an iframe
* on domain B pointing to a special relay file and with a message encoded
* after the hash (#). This relay, in turn, finds the page on domain B, and
* can call a receipt function with the message given to it. The relay URL
* used by each caller is set via the gadgets.rpc.setRelayUrl(..) and
* *must* be called before the call method is used.
*
* ifpc: Iframe-based method, utilizing a relay page, to send a message.
*/
function getRelayChannel() {
return typeof window.postMessage === 'function' ? 'wpm' :
typeof document.postMessage === 'function' ? 'dpm' :
navigator.product === 'Gecko' ? 'fe' :
'ifpc';
}
/**
* Conducts any initial global work necessary to setup the
* channel type chosen.
*/
function setupChannel() {
// If the channel type is one of the native
// postMessage based ones, setup the handler to receive
// messages.
if (relayChannel === 'dpm' || relayChannel === 'wpm') {
window.addEventListener('message', function(packet) {
// TODO validate packet.domain for security reasons
process(gadgets.json.parse(packet.data));
}, false);
}
}
// Pick the most efficient RPC relay mechanism
var relayChannel = getRelayChannel();
// Conduct any setup necessary for the chosen channel.
setupChannel();
// Create the Default RPC handler.
services[DEFAULT_NAME] = function() {
throw new Error('Unknown RPC service: ' + this.s);
};
// Create a Special RPC handler for callbacks.
services[CALLBACK_NAME] = function(callbackId, result) {
var callback = callbacks[callbackId];
if (callback) {
delete callbacks[callbackId];
callback(result);
}
};
/**
* Conducts any frame-specific work necessary to setup
* the channel type chosen. This method is called when
* the container page first registers the gadget in the
* RPC mechanism. Gadgets, in turn, will complete the setup
* of the channel once they send their first messages.
*/
function setupFrame(frameId) {
if (setup[frameId]) {
return;
}
if (relayChannel === 'fe') {
try {
var frame = document.getElementById(frameId);
frame[FE_G2C_CHANNEL] = function(args) {
process(gadgets.json.parse(args));
};
} catch (e) {
// Something went wrong. System will fallback to
// IFPC.
}
}
setup[frameId] = true;
}
/**
* Encodes arguments for the legacy IFPC wire format.
*
* @param {Object} args
* @return {String} the encoded args
*/
function encodeLegacyData(args) {
var stringify = gadgets.json.stringify;
var argsEscaped = [];
for(var i = 0, j = args.length; i < j; ++i) {
argsEscaped.push(encodeURIComponent(stringify(args[i])));
}
return argsEscaped.join('&');
}
/**
* Helper function to process an RPC request
* @param {Object} rpc RPC request object
* @private
*/
function process(rpc) {
//
// RPC object contents:
// s: Service Name
// f: From
// c: The callback ID or 0 if none.
// a: The arguments for this RPC call.
// t: The authentication token.
//
if (rpc && typeof rpc.s === 'string' && typeof rpc.f === 'string' &&
rpc.a instanceof Array) {
// Validate auth token.
if (authToken[rpc.f]) {
// We allow type coercion here because all the url params are strings.
if (authToken[rpc.f] != rpc.t) {
throw new Error("Invalid auth token.");
}
}
// If there is a callback for this service, attach a callback function
// to the rpc context object for asynchronous rpc services.
//
// Synchronous rpc request handlers should simply ignore it and return a
// value as usual.
// Asynchronous rpc request handlers, on the other hand, should pass its
// result to this callback function and not return a value on exit.
//
// For example, the following rpc handler passes the first parameter back
// to its rpc client with a one-second delay.
//
// function asyncRpcHandler(param) {
// var me = this;
// setTimeout(function() {
// me.callback(param);
// }, 1000);
// }
if (rpc.c) {
rpc.callback = function(result) {
gadgets.rpc.call(rpc.f, CALLBACK_NAME, null, rpc.c, result);
};
}
// Call the requested RPC service.
var result = (services[rpc.s] ||
services[DEFAULT_NAME]).apply(rpc, rpc.a);
// If the rpc request handler returns a value, immediately pass it back
// to the callback. Otherwise, do nothing, assuming that the rpc handler
// will make an asynchronous call later.
if (rpc.c && typeof result != 'undefined') {
gadgets.rpc.call(rpc.f, CALLBACK_NAME, null, rpc.c, result);
}
}
}
/**
* Attempts to conduct an RPC call to the specified
* target with the specified data via the FrameElement
* method. If this method fails, the system attempts again
* using the known default of IFPC.
*
* @param {String} targetId Module Id of the RPC service provider.
* @param {String} serviceName Service name to call.
* @param {String} from Module Id of the calling provider.
* @param {Object} rpcData The RPC data for this call.
* @param {Array.<Object>} callArgs Original arguments to call()
*/
function callFrameElement(targetId, serviceName, from, rpcData, callArgs) {
try {
if (from != '..') {
// Call from gadget to the container.
var fe = window.frameElement;
if (typeof fe[FE_G2C_CHANNEL] === 'function') {
// Complete the setup of the FE channel if need be.
if (typeof fe[FE_G2C_CHANNEL][FE_C2G_CHANNEL] !== 'function') {
fe[FE_G2C_CHANNEL][FE_C2G_CHANNEL] = function(args) {
process(gadgets.json.parse(args));
};
}
// Conduct the RPC call.
fe[FE_G2C_CHANNEL](rpcData);
return;
}
} else {
// Call from container to gadget[targetId].
var frame = document.getElementById(targetId);
if (typeof frame[FE_G2C_CHANNEL] === 'function' &&
typeof frame[FE_G2C_CHANNEL][FE_C2G_CHANNEL] === 'function') {
// Conduct the RPC call.
frame[FE_G2C_CHANNEL][FE_C2G_CHANNEL](rpcData);
return;
}
}
} catch (e) {
}
// If we have reached this point, something has failed
// with the FrameElement method, so we default to using
// IFPC for this call.
callIfpc(targetId, serviceName, from, rpcData, callArgs);
}
/**
* Conducts an RPC call to the specified
* target with the specified data via the IFPC
* method.
*
* @param {String} targetId Module Id of the RPC service provider.
* @param {String} serviceName Service name to call.
* @param {String} from Module Id of the calling provider.
* @param {Object} rpcData The RPC data for this call.
* @param {Array.<Object>} callArgs Original arguments to call()
*/
function callIfpc(targetId, serviceName, from, rpcData, callArgs) {
// Retrieve the relay file used by IFPC. Note that
// this must be set before the call, and so we conduct
// an extra check to ensure it is not blank.
var relay = gadgets.rpc.getRelayUrl(targetId);
if (!relay) {
throw new Error('No relay file assigned for IFPC');
}
// The RPC mechanism supports two formats for IFPC (legacy and current).
var src = null;
if (useLegacyProtocol[targetId]) {
// Format: #iframe_id&callId&num_packets&packet_num&block_of_data
src = [relay, '#', encodeLegacyData([from, callId, 1, 0,
encodeLegacyData([from, serviceName, '', '', from].concat(
callArgs))])].join('');
} else {
// Format: #targetId & sourceId@callId & packetNum & packetId & packetData
src = [relay, '#', targetId, '&', from, '@', callId,
'&1&0&', encodeURIComponent(rpcData)].join('');
}
// Conduct the IFPC call by creating the Iframe with
// the relay URL and appended message.
emitInvisibleIframe(src);
}
/**
* Helper function to emit an invisible IFrame.
* @param {String} src SRC attribute of the IFrame to emit.
* @private
*/
function emitInvisibleIframe(src) {
var iframe;
// Recycle IFrames
for (var i = iframePool.length - 1; i >=0; --i) {
var ifr = iframePool[i];
try {
if (ifr && (ifr.recyclable || ifr.readyState === 'complete')) {
ifr.parentNode.removeChild(ifr);
if (window.ActiveXObject) {
// For MSIE, delete any iframes that are no longer being used. MSIE
// cannot reuse the IFRAME because a navigational click sound will
// be triggered when we set the SRC attribute.
// Other browsers scan the pool for a free iframe to reuse.
iframePool[i] = ifr = null;
iframePool.splice(i, 1);
} else {
ifr.recyclable = false;
iframe = ifr;
break;
}
}
} catch (e) {
// Ignore; IE7 throws an exception when trying to read readyState and
// readyState isn't set.
}
}
// Create IFrame if necessary
if (!iframe) {
iframe = document.createElement('iframe');
iframe.style.border = iframe.style.width = iframe.style.height = '0px';
iframe.style.visibility = 'hidden';
iframe.style.position = 'absolute';
iframe.onload = function() { this.recyclable = true; };
iframePool.push(iframe);
}
iframe.src = src;
setTimeout(function() { document.body.appendChild(iframe); }, 0);
}
/**
* Attempts to make an rpc by calling the target's receive method directly.
* This works when gadgets are rendered on the same domain as their container,
* a potentially useful optimization for trusted content which keeps
* RPC behind a consistent interface.
* @param {String} target Module id of the rpc service provider
* @param {String} from Module id of the caller (this)
* @param {String} callbackId Id of the call
* @param {String} rpcData JSON-encoded RPC payload
* @return
*/
function callSameDomain(target, rpc) {
if (typeof sameDomain[target] === 'undefined') {
// Seed with a negative, typed value to avoid
// hitting this code path repeatedly
sameDomain[target] = false;
var targetEl = null;
if (target === '..') {
targetEl = parent;
} else {
targetEl = frames[target];
}
try {
// If this succeeds, then same-domain policy applied
sameDomain[target] = targetEl.gadgets.rpc.receiveSameDomain;
} catch (e) {
// Usual case: different domains
}
}
if (typeof sameDomain[target] === 'function') {
// Call target's receive method
sameDomain[target](rpc);
return true;
}
return false;
}
// gadgets.config might not be available, such as when serving container js.
if (gadgets.config) {
/**
* Initializes RPC from the provided configuration.
*/
function init(config) {
// Allow for wild card parent relay files as long as it's from a
// white listed domain. This is enforced by the rendering servlet.
if (config.rpc.parentRelayUrl.substring(0, 7) === 'http://') {
relayUrl['..'] = config.rpc.parentRelayUrl;
} else {
// It's a relative path, and we must append to the parent.
// We're relying on the server validating the parent parameter in this
// case. Because of this, parent may only be passed in the query, not
// the fragment.
var params = document.location.search.substring(0).split("&");
var parentParam = "";
for (var i = 0, param; param = params[i]; ++i) {
// Only the first parent can be validated.
if (param.indexOf("parent=") === 0) {
parentParam = decodeURIComponent(param.substring(7));
break;
}
}
relayUrl['..'] = parentParam + config.rpc.parentRelayUrl;
}
useLegacyProtocol['..'] = !!config.rpc.useLegacyProtocol;
}
var requiredConfig = {
parentRelayUrl : gadgets.config.NonEmptyStringValidator
};
gadgets.config.register("rpc", requiredConfig, init);
}
return /** @scope gadgets.rpc */ {
/**
* Registers an RPC service.
* @param {String} serviceName Service name to register.
* @param {Function} handler Service handler.
*
* @member gadgets.rpc
*/
register: function(serviceName, handler) {
if (serviceName == CALLBACK_NAME) {
throw new Error("Cannot overwrite callback service");
}
if (serviceName == DEFAULT_NAME) {
throw new Error("Cannot overwrite default service:"
+ " use registerDefault");
}
services[serviceName] = handler;
},
/**
* Unregisters an RPC service.
* @param {String} serviceName Service name to unregister.
*
* @member gadgets.rpc
*/
unregister: function(serviceName) {
if (serviceName == CALLBACK_NAME) {
throw new Error("Cannot delete callback service");
}
if (serviceName == DEFAULT_NAME) {
throw new Error("Cannot delete default service:"
+ " use unregisterDefault");
}
delete services[serviceName];
},
/**
* Registers a default service handler to processes all unknown
* RPC calls which raise an exception by default.
* @param {Function} handler Service handler.
*
* @member gadgets.rpc
*/
registerDefault: function(handler) {
services[''] = handler;
},
/**
* Unregisters the default service handler. Future unknown RPC
* calls will fail silently.
*
* @member gadgets.rpc
*/
unregisterDefault: function() {
delete services[''];
},
/**
* Calls an RPC service.
* @param {String} targetId Module Id of the RPC service provider.
* Empty if calling the parent container.
* @param {String} serviceName Service name to call.
* @param {Function|null} callback Callback function (if any) to process
* the return value of the RPC request.
* @param {*} var_args Parameters for the RPC request.
*
* @member gadgets.rpc
*/
call: function(targetId, serviceName, callback, var_args) {
++callId;
targetId = targetId || '..';
if (callback) {
callbacks[callId] = callback;
}
// Default to the container calling.
var from = '..';
if (targetId === '..') {
from = window.name;
}
// Not used by legacy, create it anyway...
var rpc = {
s: serviceName,
f: from,
c: callback ? callId : 0,
a: Array.prototype.slice.call(arguments, 3),
t: authToken[targetId]
};
// If target is on the same domain, call method directly
if (callSameDomain(targetId, rpc)) {
return;
}
var rpcData = gadgets.json.stringify(rpc);
var channelType = relayChannel;
// If we are told to use the legacy format, then we must
// default to IFPC.
if (useLegacyProtocol[targetId]) {
channelType = 'ifpc';
}
switch (channelType) {
case 'dpm': // use document.postMessage.
var targetDoc = targetId === '..' ? parent.document :
frames[targetId].document;
targetDoc.postMessage(rpcData);
break;
case 'wpm': // use window.postMessage.
var targetWin = targetId === '..' ? parent : frames[targetId];
targetWin.postMessage(rpcData, relayUrl[targetId]);
break;
case 'fe': // use FrameElement.
callFrameElement(targetId, serviceName, from, rpcData, rpc.a);
break;
default: // use 'ifpc' as a fallback mechanism.
callIfpc(targetId, serviceName, from, rpcData, rpc.a);
break;
}
},
/**
* Gets the relay URL of a target frame.
* @param {String} targetId Name of the target frame.
* @return {String|undefined} Relay URL of the target frame.
*
* @member gadgets.rpc
*/
getRelayUrl: function(targetId) {
return relayUrl[targetId];
},
/**
* Sets the relay URL of a target frame.
* @param {String} targetId Name of the target frame.
* @param {String} url Full relay URL of the target frame.
* @param {Boolean} opt_useLegacy True if this relay needs the legacy IFPC
* wire format.
*
* @member gadgets.rpc
*/
setRelayUrl: function(targetId, url, opt_useLegacy) {
relayUrl[targetId] = url;
useLegacyProtocol[targetId] = !!opt_useLegacy;
},
/**
* Sets the auth token of a target frame.
* @param {String} targetId Name of the target frame.
* @param {String} token The authentication token to use for all
* calls to or from this target id.
*
* @member gadgets.rpc
*/
setAuthToken: function(targetId, token) {
authToken[targetId] = token;
setupFrame(targetId);
},
/**
* Gets the RPC relay mechanism.
* @return {String} RPC relay mechanism. See above for
* a list of supported types.
*
* @member gadgets.rpc
*/
getRelayChannel: function() {
return relayChannel;
},
/**
* Receives and processes an RPC request. (Not to be used directly.)
* @param {Array.<String>} fragment An RPC request fragment encoded as
* an array. The first 4 elements are target id, source id & call id,
* total packet number, packet id. The last element stores the actual
* JSON-encoded and URI escaped packet data.
*
* @member gadgets.rpc
*/
receive: function(fragment) {
if (fragment.length > 4) {
// TODO parse fragment[1..3] to merge multi-fragment messages
process(gadgets.json.parse(
decodeURIComponent(fragment[fragment.length - 1])));
}
},
/**
* Receives and processes an RPC request sent via the same domain.
* (Not to be used directly). Converts the inbound rpc object's
* Array into a local Array to pass the process() Array test.
* @param {Object} rpc RPC object containing all request params
*/
receiveSameDomain: function(rpc) {
// Pass through to local process method but converting to a local Array
rpc.a = Array.prototype.slice.call(rpc.a);
window.setTimeout(function() { process(rpc) }, 0);
}
};
}();
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
/**
* @fileoverview Utility functions for the Open Gadget Container
*/
Function.prototype.inherits = function(parentCtor) {
function tempCtor() {};
tempCtor.prototype = parentCtor.prototype;
this.superClass_ = parentCtor.prototype;
this.prototype = new tempCtor();
this.prototype.constructor = this;
};
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* @fileoverview
* The global object gadgets.json contains two methods.
*
* gadgets.json.stringify(value) takes a JavaScript value and produces a JSON
* text. The value must not be cyclical.
*
* gadgets.json.parse(text) takes a JSON text and produces a JavaScript value.
* It will return false if there is an error.
*/
var gadgets = gadgets || {};
/**
* @static
* @class Provides operations for translating objects to and from JSON.
* @name gadgets.json
*/
/**
* Port of the public domain JSON library by Douglas Crockford.
* See: http://www.json.org/json2.js
*/
gadgets.json = function () {
/**
* Formats integers to 2 digits.
* @param {Number} n
*/
function f(n) {
return n < 10 ? '0' + n : n;
}
Date.prototype.toJSON = function () {
return [this.getUTCFullYear(), '-',
f(this.getUTCMonth() + 1), '-',
f(this.getUTCDate()), 'T',
f(this.getUTCHours()), ':',
f(this.getUTCMinutes()), ':',
f(this.getUTCSeconds()), 'Z'].join("");
};
// table of character substitutions
var m = {
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
};
/**
* Converts a json object into a string.
*/
function stringify(value) {
var a, // The array holding the partial texts.
i, // The loop counter.
k, // The member key.
l, // Length.
r = /["\\\x00-\x1f\x7f-\x9f]/g,
v; // The member value.
switch (typeof value) {
case 'string':
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe ones.
return r.test(value) ?
'"' + value.replace(r, function (a) {
var c = m[a];
if (c) {
return c;
}
c = a.charCodeAt();
return '\\u00' + Math.floor(c / 16).toString(16) +
(c % 16).toString(16);
}) + '"'
: '"' + value + '"';
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
return String(value);
case 'object':
// Due to a specification blunder in ECMAScript,
// typeof null is 'object', so watch out for that case.
if (!value) {
return 'null';
}
// toJSON check removed; re-implement when it doesn't break other libs.
a = [];
if (typeof value.length === 'number' &&
!(value.propertyIsEnumerable('length'))) {
// The object is an array. Stringify every element. Use null as a
// placeholder for non-JSON values.
l = value.length;
for (i = 0; i < l; i += 1) {
a.push(stringify(value[i]) || 'null');
}
// Join all of the elements together and wrap them in brackets.
return '[' + a.join(',') + ']';
}
// Otherwise, iterate through all of the keys in the object.
for (k in value) if (value.hasOwnProperty(k)) {
if (typeof k === 'string') {
v = stringify(value[k]);
if (v) {
a.push(stringify(k) + ':' + v);
}
}
}
// Join all of the member texts together and wrap them in braces.
return '{' + a.join(',') + '}';
}
}
return {
stringify: stringify,
parse: function (text) {
// Parsing happens in three stages. In the first stage, we run the text against
// regular expressions that look for non-JSON patterns. We are especially
// concerned with '()' and 'new' because they can cause invocation, and '='
// because it can cause mutation. But just to be safe, we want to reject all
// unexpected forms.
// We split the first stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace all backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/b-u]/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
return eval('(' + text + ')');
}
// If the text is not JSON parseable, then return false.
return false;
}
};
}();
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
var gadgets = gadgets || {};
gadgets.error = {};
gadgets.error.SUBCLASS_RESPONSIBILITY = 'subclass responsibility';
gadgets.error.TO_BE_DONE = 'to be done';
gadgets.log = function(message) {
if (window.console && console.log) {
console.log(message);
} else {
var logEntry = document.createElement('div');
logEntry.className = 'gadgets-log-entry';
logEntry.innerHTML = message;
document.body.appendChild(logEntry);
}
};
//----------
//Extensible
gadgets.Extensible = function() {
};
/**
* Sets the dependencies.
* @param {Object} dependencies Object whose properties are set on this
* container as dependencies
*/
gadgets.Extensible.prototype.setDependencies = function(dependencies) {
for (var p in dependencies) {
this[p] = dependencies[p];
}
};
/**
* Returns a dependency given its name.
* @param {String} name Name of dependency
* @return {Object} Dependency with that name or undefined if not found
*/
gadgets.Extensible.prototype.getDependencies = function(name) {
return this[name];
};
//-------------
//UserPrefStore
/**
* User preference store interface.
* @constructor
*/
gadgets.UserPrefStore = function() {
};
/**
* Gets all user preferences of a gadget.
* @param {Object} gadget Gadget object
* @return {Object} All user preference of given gadget
*/
gadgets.UserPrefStore.prototype.getPrefs = function(gadget) {
throw Error(gadgets.error.SUBCLASS_RESPONSIBILITY);
};
/**
* Saves user preferences of a gadget in the store.
* @param {Object} gadget Gadget object
* @param {Object} prefs User preferences
*/
gadgets.UserPrefStore.prototype.savePrefs = function(gadget) {
throw Error(gadgets.error.SUBCLASS_RESPONSIBILITY);
};
//-------------
//DefaultUserPrefStore
/**
* User preference store implementation.
* TODO: Turn this into a real implementation that is production safe
* @constructor
*/
gadgets.DefaultUserPrefStore = function() {
gadgets.UserPrefStore.call(this);
};
gadgets.DefaultUserPrefStore.inherits(gadgets.UserPrefStore);
gadgets.DefaultUserPrefStore.prototype.getPrefs = function(gadget) { };
gadgets.DefaultUserPrefStore.prototype.savePrefs = function(gadget) { };
//-------------
//GadgetService
/**
* Interface of service provided to gadgets for resizing gadgets,
* setting title, etc.
* @constructor
*/
gadgets.GadgetService = function() {
};
gadgets.GadgetService.prototype.setHeight = function(elementId, height) {
throw Error(gadgets.error.SUBCLASS_RESPONSIBILITY);
};
gadgets.GadgetService.prototype.setTitle = function(gadget, title) {
throw Error(gadgets.error.SUBCLASS_RESPONSIBILITY);
};
gadgets.GadgetService.prototype.setUserPref = function(id) {
throw Error(gadgets.error.SUBCLASS_RESPONSIBILITY);
};
//----------------
//IfrGadgetService
/**
* Base implementation of GadgetService. This implementation does not implement setting the title or user prefs as it
* is meant for gadgets that are standalone on a page.
* @constructor
*/
gadgets.IfrGadgetService = function() {
gadgets.GadgetService.call(this);
/**
* This is a bit funky looking but we need to be sure we always call the right method, even if the implementation
* has been overridden after an IfrGadgetService is instantiated.
*/
var service = this;
gadgets.rpc.register('resize_iframe', function() { service.setHeight.apply(this, arguments); });
gadgets.rpc.register('set_pref', function() { service.setUserPref.apply(this, arguments); });
gadgets.rpc.register('set_title', function() { service.setTitle.apply(this, arguments); });
gadgets.rpc.register('requestNavigateTo', function() { service.requestNavigateTo.apply(this, arguments); });
};
gadgets.IfrGadgetService.inherits(gadgets.GadgetService);
gadgets.IfrGadgetService.prototype.setHeight = function(height) {
if (height > gadgets.container.maxheight_) {
height = gadgets.container.maxheight_;
}
var element = document.getElementById(this.f);
if (element) {
element.style.height = height + 'px';
}
};
/**
* Navigates the page to a new url based on a gadgets requested view and
* parameters.
*/
gadgets.IfrGadgetService.prototype.requestNavigateTo = function(view, opt_params) {
var id = this.getGadgetIdFromModuleId(this.f);
var url = this.getUrlForView(view);
if (opt_params) {
var paramStr = JSON.stringify(opt_params);
if (paramStr.length > 0) {
url += '&appParams=' + encodeURIComponent(paramStr);
}
}
if (url && document.location.href.indexOf(url) == -1) {
document.location.href = url;
}
};
/**
* This is a silly implementation that will need to be overriden by almost all
* real containers.
* TODO: Find a better default for this function
*
* @param view The view name to get the url for
*/
gadgets.IfrGadgetService.prototype.getUrlForView = function(view) {
if (view === 'canvas') {
return '/canvas';
} else if (view === 'profile') {
return '/profile';
} else {
return null;
}
};
gadgets.IfrGadgetService.prototype.getGadgetIdFromModuleId = function(moduleId) {
// Quick hack to extract the gadget id from module id
return parseInt(moduleId.match(/_([0-9]+)$/)[1], 10);
};
//-------------
//LayoutManager
/**
* Layout manager interface.
* @constructor
*/
gadgets.LayoutManager = function() {
};
/**
* Gets the HTML element that is the chrome of a gadget into which the content
* of the gadget can be rendered.
* @param {Object} gadget Gadget instance
* @return {Object} HTML element that is the chrome for the given gadget
*/
gadgets.LayoutManager.prototype.getGadgetChrome = function(gadget) {
throw Error(gadgets.error.SUBCLASS_RESPONSIBILITY);
};
//-------------------
//StaticLayoutManager
/**
* Static layout manager where gadget ids have a 1:1 mapping to chrome ids.
* @constructor
*/
gadgets.StaticLayoutManager = function() {
gadgets.LayoutManager.call(this);
};
gadgets.StaticLayoutManager.inherits(gadgets.LayoutManager);
/**
* Sets chrome ids, whose indexes are gadget instance ids (starting from 0).
* @param {Array} gadgetIdToChromeIdMap Gadget id to chrome id map
*/
gadgets.StaticLayoutManager.prototype.setGadgetChromeIds = function(gadgetChromeIds) {
this.gadgetChromeIds_ = gadgetChromeIds;
};
gadgets.StaticLayoutManager.prototype.getGadgetChrome = function(gadget) {
var chromeId = this.gadgetChromeIds_[gadget.id];
return chromeId ? document.getElementById(chromeId) : null;
};
//----------------------
//FloatLeftLayoutManager
/**
* FloatLeft layout manager where gadget ids have a 1:1 mapping to chrome ids.
* @constructor
* @param {String} layoutRootId Id of the element that is the parent of all
* gadgets.
*/
gadgets.FloatLeftLayoutManager = function(layoutRootId) {
gadgets.LayoutManager.call(this);
this.layoutRootId_ = layoutRootId;
};
gadgets.FloatLeftLayoutManager.inherits(gadgets.LayoutManager);
gadgets.FloatLeftLayoutManager.prototype.getGadgetChrome = function(gadget) {
var layoutRoot = document.getElementById(this.layoutRootId_);
if (layoutRoot) {
var chrome = document.createElement('div');
chrome.className = 'gadgets-gadget-chrome';
chrome.style.cssFloat = 'left';
layoutRoot.appendChild(chrome);
return chrome;
} else {
return null;
}
};