// Copyright (c) 2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Parts Copyright 2005-2009, the Dojo Foundation. Used under the terms of the // "New" BSD License: // // http://download.dojotoolkit.org/release-1.3.2/dojo-release-1.3.2/dojo/LICENSE // /** * @fileoverview CFInstance.js provides a set of utilities for managing * ChromeFrame plugins, including creation, RPC services, and a singleton to * use for communicating from ChromeFrame hosted content to an external * CFInstance wrapper. CFInstance.js is stand-alone, designed to be served from * a CDN, and built to not create side-effects for other hosted content. * @author slightlyoff@google.com (Alex Russell) */ (function(scope) { // TODO: // * figure out if there's any way to install w/o a browser restart, and if // so, where and how // * slim down Deferred and RPC scripts // * determine what debugging APIs should be exposed and how they should be // surfaced. What about content authoring in Chrome instances? Stubbing // the other side of RPC's? // bail if we'd be over-writing an existing CFInstance object if (scope['CFInstance']) { return; } ///////////////////////////////////////////////////////////////////////////// // Utiliity and Cross-Browser Functions ///////////////////////////////////////////////////////////////////////////// // a monotonically incrementing counter var _counter = 0; var undefStr = 'undefined'; // // Browser detection: ua.isIE, ua.isSafari, ua.isOpera, etc. // /** * An object for User Agent detection * @type {!Object} * @protected */ var ua = {}; var n = navigator; var dua = String(n.userAgent); var dav = String(n.appVersion); var tv = parseFloat(dav); var duaParse = function(s){ var c = 0; try { return parseFloat( dua.split(s)[1].replace(/\./g, function() { c++; return (c > 1) ? '' : '.'; } ) ); } catch(e) { // squelch to intentionally return undefined } }; /** @type {number} */ ua.isOpera = dua.indexOf('Opera') >= 0 ? tv: undefined; /** @type {number} */ ua.isWebKit = duaParse('WebKit/'); /** @type {number} */ ua.isChrome = duaParse('Chrome/'); /** @type {number} */ ua.isKhtml = dav.indexOf('KHTML') >= 0 ? tv : undefined; var index = Math.max(dav.indexOf('WebKit'), dav.indexOf('Safari'), 0); if (index && !ua.isChrome) { /** @type {number} */ ua.isSafari = parseFloat(dav.split('Version/')[1]); if(!ua.isSafari || parseFloat(dav.substr(index + 7)) <= 419.3){ ua.isSafari = 2; } } if (dua.indexOf('Gecko') >= 0 && !ua.isKhtml) { /** @type {number} */ ua.isGecko = duaParse(' rv:'); } if (ua.isGecko) { /** @type {number} */ ua.isFF = parseFloat(dua.split('Firefox/')[1]) || undefined; } if (document.all && !ua.isOpera) { /** @type {number} */ ua.isIE = parseFloat(dav.split('MSIE ')[1]) || undefined; } /** * Log out varargs to a browser-provided console object (if available). Else * a no-op. * @param {*} var_args Optional Things to log. * @protected **/ var log = function() { if (window['console']) { try { if (ua.isSafari || ua.isChrome) { throw Error(); } console.log.apply(console, arguments); } catch(e) { try { console.log(toArray(arguments).join(' ')); } catch(e2) { // squelch } } } }; // // Language utility methods // /** * Determine if the passed item is a String * @param {*} item Item to test. * @protected **/ var isString = function(item) { return typeof item == 'string'; }; /** * Determine if the passed item is a Function object * @param {*} item Item to test. * @protected **/ var isFunction = function(item) { return ( item && ( typeof item == 'function' || item instanceof Function ) ); }; /** * Determine if the passed item is an array. * @param {*} item Item to test. * @protected **/ var isArray = function(item){ return ( item && ( item instanceof Array || ( typeof item == 'object' && typeof item.length != undefStr ) ) ); }; /** * A toArray version which takes advantage of builtins * @param {*} obj The array-like object to convert to a real array. * @param {number} opt_offset An index to being copying from in the source. * @param {Array} opt_startWith An array to extend with elements of obj in * lieu of creating a new array to return. * @private **/ var _efficientToArray = function(obj, opt_offset, opt_startWith){ return (opt_startWith || []).concat( Array.prototype.slice.call(obj, opt_offset || 0 ) ); }; /** * A version of toArray that iterates in lieu of using array generics. * @param {*} obj The array-like object to convert to a real array. * @param {number} opt_offset An index to being copying from in the source. * @param {Array} opt_startWith An array to extend with elements of obj in * @private **/ var _slowToArray = function(obj, opt_offset, opt_startWith){ var arr = opt_startWith || []; for(var x = opt_offset || 0; x < obj.length; x++){ arr.push(obj[x]); } return arr; }; /** * Converts an array-like object (e.g., an "arguments" object) to a real * Array. * @param {*} obj The array-like object to convert to a real array. * @param {number} opt_offset An index to being copying from in the source. * @param {Array} opt_startWith An array to extend with elements of obj in * @protected */ var toArray = ua.isIE ? function(obj){ return ( obj.item ? _slowToArray : _efficientToArray ).apply(this, arguments); } : _efficientToArray; var _getParts = function(arr, obj, cb){ return [ isString(arr) ? arr.split('') : arr, obj || window, isString(cb) ? new Function('item', 'index', 'array', cb) : cb ]; }; /** * like JS1.6 Array.forEach() * @param {Array} arr the array to iterate * @param {function(Object, number, Array)} callback the method to invoke for * each item in the array * @param {function?} thisObject Optional a scope to use with callback * @return {array} the original arr * @protected */ var forEach = function(arr, callback, thisObject) { if(!arr || !arr.length){ return arr; } var parts = _getParts(arr, thisObject, callback); // parts has a structure of: // [ // array, // scope, // function // ] arr = parts[0]; for (var i = 0, l = arr.length; i < l; ++i) { parts[2].call( parts[1], arr[i], i, arr ); } return arr; }; /** * returns a new function bound to scope with a variable number of positional * params pre-filled * @private */ var _hitchArgs = function(scope, method /*,...*/) { var pre = toArray(arguments, 2); var named = isString(method); return function() { var args = toArray(arguments); var f = named ? (scope || window)[method] : method; return f && f.apply(scope || this, pre.concat(args)); } }; /** * Like goog.bind(). Hitches the method (named or provided as a function * object) to scope, optionally partially applying positional arguments. * @param {Object} scope the object to hitch the method to * @param {string|function} method the method to be bound * @return {function} The bound method * @protected */ var hitch = function(scope, method){ if (arguments.length > 2) { return _hitchArgs.apply(window, arguments); // Function } if (!method) { method = scope; scope = null; } if (isString(method)) { scope = scope || window; if (!scope[method]) { throw( ['scope["', method, '"] is null (scope="', scope, '")'].join('') ); } return function() { return scope[method].apply(scope, arguments || []); }; } return !scope ? method : function() { return method.apply(scope, arguments || []); }; }; /** * A version of addEventListener that works on IE too. *sigh*. * @param {!Object} obj The object to attach to * @param {!String} type Name of the event to attach to * @param {!Function} handler The function to connect * @protected */ var listen = function(obj, type, handler) { if (obj['attachEvent']) { obj.attachEvent('on' + type, handler); } else { obj.addEventListener(type, handler, false); } }; /** * Adds "listen" and "_dispatch" methods to the passed object, taking * advantage of native event hanlding if it's available. * @param {Object} instance The object to install the event system on * @protected */ var installEvtSys = function(instance) { var eventsMap = {}; var isNative = ( (typeof instance.addEventListener != undefStr) && ((instance['tagName'] || '').toLowerCase() != 'iframe') ); instance.listen = function(type, func) { var t = eventsMap[type]; if (!t) { t = eventsMap[type] = []; if (isNative) { listen(instance, type, hitch(instance, "_dispatch", type)); } } t.push(func); return t; }; instance._dispatch = function(type, evt) { var stopped = false; var stopper = function() { stopped = true; }; forEach(eventsMap[type], function(f) { if (!stopped) { f(evt, stopper); } }); }; return instance; }; /** * Deserialize the passed JSON string * @param {!String} json A string to be deserialized * @return {Object} * @protected */ var fromJson = window['JSON'] ? function(json) { return JSON.parse(json); } : function(json) { return eval('(' + (json || undefStr) + ')'); }; /** * String escaping for use in JSON serialization * @param {string} str The string to escape * @return {string} * @private */ var _escapeString = function(str) { return ('"' + str.replace(/(["\\])/g, '\\$1') + '"'). replace(/[\f]/g, '\\f'). replace(/[\b]/g, '\\b'). replace(/[\n]/g, '\\n'). replace(/[\t]/g, '\\t'). replace(/[\r]/g, '\\r'). replace(/[\x0B]/g, '\\u000b'); // '\v' is not supported in JScript; }; /** * JSON serialization for arbitrary objects. Circular references or strong * typing information are not handled. * @param {Object} it Any valid JavaScript object or type * @return {string} the serialized representation of the passed object * @protected */ var toJson = window['JSON'] ? function(it) { return JSON.stringify(it); } : function(it) { if (it === undefined) { return undefStr; } var objtype = typeof it; if (objtype == 'number' || objtype == 'boolean') { return it + ''; } if (it === null) { return 'null'; } if (isString(it)) { return _escapeString(it); } // recurse var recurse = arguments.callee; if(it.nodeType && it.cloneNode){ // isNode // we can't seriailize DOM nodes as regular objects because they have // cycles DOM nodes could be serialized with something like outerHTML, // but that can be provided by users in the form of .json or .__json__ // function. throw new Error('Cannot serialize DOM nodes'); } // array if (isArray(it)) { var res = []; forEach(it, function(obj) { var val = recurse(obj); if (typeof val != 'string') { val = undefStr; } res.push(val); }); return '[' + res.join(',') + ']'; } if (objtype == 'function') { return null; } // generic object code path var output = []; for (var key in it) { var keyStr, val; if (typeof key == 'number') { keyStr = '"' + key + '"'; } else if(typeof key == 'string') { keyStr = _escapeString(key); } else { // skip non-string or number keys continue; } val = recurse(it[key]); if (typeof val != 'string') { // skip non-serializable values continue; } // TODO(slightlyoff): use += on Moz since it's faster there output.push(keyStr + ':' + val); } return '{' + output.join(',') + '}'; // String }; // code to register with the earliest safe onload-style handler var _loadedListenerList = []; var _loadedFired = false; /** * a default handler for document onload. When called (the first time), * iterates over the list of registered listeners, calling them in turn. * @private */ var documentLoaded = function() { if (!_loadedFired) { _loadedFired = true; forEach(_loadedListenerList, 'item();'); } }; if (document.addEventListener) { // NOTE: // due to a threading issue in Firefox 2.0, we can't enable // DOMContentLoaded on that platform. For more information, see: // http://trac.dojotoolkit.org/ticket/1704 if (ua.isWebKit > 525 || ua.isOpera || ua.isFF >= 3) { listen(document, 'DOMContentLoaded', documentLoaded); } // mainly for Opera 8.5, won't be fired if DOMContentLoaded fired already. // also used for FF < 3.0 due to nasty DOM race condition listen(window, 'load', documentLoaded); } else { // crazy hack for IE that relies on the "deferred" behavior of script // tags document.write( '' + '' ); } // TODO(slightlyoff): known KHTML init issues are ignored for now // // DOM utility methods // /** * returns an item based on DOM ID. Optionally a doucment may be provided to * specify the scope to search in. If a node is passed, it's returned as-is. * @param {string|Node} id The ID of the node to be located or a node * @param {Node} doc Optional A document to search for id. * @return {Node} * @protected */ var byId = (ua.isIE || ua.isOpera) ? function(id, doc) { if (isString(id)) { doc = doc || document; var te = doc.getElementById(id); // attributes.id.value is better than just id in case the // user has a name=id inside a form if (te && te.attributes.id.value == id) { return te; } else { var elements = doc.all[id]; if (!elements || !elements.length) { return elements; } // if more than 1, choose first with the correct id var i=0; while (te = elements[i++]) { if (te.attributes.id.value == id) { return te; } } } } else { return id; // DomNode } } : function(id, doc) { return isString(id) ? (doc || document).getElementById(id) : id; }; /** * returns a unique DOM id which can be used to locate the node via byId(). * If the node already has an ID, it's used. If not, one is generated. Like * IE's uniqueID property. * @param {Node} node The element to create or fetch a unique ID for * @return {String} * @protected */ var getUid = function(node) { var u = 'cfUnique' + (_counter++); return (!node) ? u : ( node.id || node.uniqueID || (node.id = u) ); }; // // the Deferred class, borrowed from Twisted Python and Dojo // /** * A class that models a single response (past or future) to a question. * Multiple callbacks and error handlers may be added. If the response was * added in the past, adding callbacks has the effect of calling them * immediately. In this way, Deferreds simplify thinking about synchronous * vs. asynchronous programming in languages which don't have continuations * or generators which might otherwise provide syntax for deferring * operations. * @param {function} canceller Optional A function to be called when the * Deferred is canceled. * @param {number} timeout Optional How long to wait (in ms) before errback * is called with a timeout error. If no timeout is passed, the default * is 1hr. Passing -1 will disable any timeout. * @constructor * @public */ Deferred = function(/*Function?*/ canceller, timeout){ // example: // var deferred = new Deferred(); // setTimeout(function(){ deferred.callback({success: true}); }, 1000); // return deferred; this.chain = []; this.id = _counter++; this.fired = -1; this.paused = 0; this.results = [ null, null ]; this.canceller = canceller; // FIXME(slightlyoff): is it really smart to be creating this many timers? if (typeof timeout == 'number') { if (timeout <= 0) { timeout = 216000; // give it an hour } } this._timer = setTimeout( hitch(this, 'errback', new Error('timeout')), (timeout || 10000) ); this.silentlyCancelled = false; }; /** * Cancels a Deferred that has not yet received a value, or is waiting on * another Deferred as its value. If a canceller is defined, the canceller * is called. If the canceller did not return an error, or there was no * canceller, then the errback chain is started. * @public */ Deferred.prototype.cancel = function() { var err; if (this.fired == -1) { if (this.canceller) { err = this.canceller(this); } else { this.silentlyCancelled = true; } if (this.fired == -1) { if ( !(err instanceof Error) ) { var res = err; var msg = 'Deferred Cancelled'; if (err && err.toString) { msg += ': ' + err.toString(); } err = new Error(msg); err.dType = 'cancel'; err.cancelResult = res; } this.errback(err); } } else if ( (this.fired == 0) && (this.results[0] instanceof Deferred) ) { this.results[0].cancel(); } }; /** * internal function for providing a result. If res is an instance of Error, * we treat it like such and start the error chain. * @param {Object|Error} res the result * @private */ Deferred.prototype._resback = function(res) { if (this._timer) { clearTimeout(this._timer); } this.fired = res instanceof Error ? 1 : 0; this.results[this.fired] = res; this._fire(); }; /** * determine if the deferred has already been resolved * @return {boolean} * @private */ Deferred.prototype._check = function() { if (this.fired != -1) { if (!this.silentlyCancelled) { return 0; } this.silentlyCancelled = 0; return 1; } return 0; }; /** * Begin the callback sequence with a non-error value. * @param {Object|Error} res the result * @public */ Deferred.prototype.callback = function(res) { this._check(); this._resback(res); }; /** * Begin the callback sequence with an error result. * @param {Error|string} res the result. If not an Error, it's treated as the * message for a new Error. * @public */ Deferred.prototype.errback = function(res) { this._check(); if ( !(res instanceof Error) ) { res = new Error(res); } this._resback(res); }; /** * Add a single function as the handler for both callback and errback, * allowing you to specify a scope (unlike addCallbacks). * @param {function|Object} cb A function. If cbfn is passed, the value of cb * is treated as a scope * @param {function|string} cbfn Optional A function or name of a function in * the scope cb. * @return {Deferred} this * @public */ Deferred.prototype.addBoth = function(cb, cbfn) { var enclosed = hitch.apply(window, arguments); return this.addCallbacks(enclosed, enclosed); }; /** * Add a single callback to the end of the callback sequence. Add a function * as the handler for successful resolution of the Deferred. May be called * multiple times to register many handlers. Note that return values are * chained if provided, so it's best for callback handlers not to return * anything. * @param {function|Object} cb A function. If cbfn is passed, the value of cb * is treated as a scope * @param {function|string} cbfn Optional A function or name of a function in * the scope cb. * @return {Deferred} this * @public */ Deferred.prototype.addCallback = function(cb, cbfn /*...*/) { return this.addCallbacks(hitch.apply(window, arguments)); }; /** * Add a function as the handler for errors in the Deferred. May be called * multiple times to add multiple error handlers. * @param {function|Object} cb A function. If cbfn is passed, the value of cb * is treated as a scope * @param {function|string} cbfn Optional A function or name of a function in * the scope cb. * @return {Deferred} this * @public */ Deferred.prototype.addErrback = function(cb, cbfn) { return this.addCallbacks(null, hitch.apply(window, arguments)); }; /** * Add a functions as handlers for callback and errback in a single shot. * @param {function} callback A function * @param {function} errback A function * @return {Deferred} this * @public */ Deferred.prototype.addCallbacks = function(callback, errback) { this.chain.push([callback, errback]); if (this.fired >= 0) { this._fire(); } return this; }; /** * when this Deferred is satisfied, pass it on to def, allowing it to run. * @param {Deferred} def A deferred to add to the end of this Deferred in a chain * @return {Deferred} this * @public */ Deferred.prototype.chain = function(def) { this.addCallbacks(def.callback, def.errback); return this; }; /** * Used internally to exhaust the callback sequence when a result is * available. * @private */ Deferred.prototype._fire = function() { var chain = this.chain; var fired = this.fired; var res = this.results[fired]; var cb = null; while ((chain.length) && (!this.paused)) { var f = chain.shift()[fired]; if (!f) { continue; } var func = hitch(this, function() { var ret = f(res); //If no response, then use previous response. if (typeof ret != undefStr) { res = ret; } fired = res instanceof Error ? 1 : 0; if (res instanceof Deferred) { cb = function(res) { this._resback(res); // inlined from _pause() this.paused--; if ( (this.paused == 0) && (this.fired >= 0)) { this._fire(); } } // inlined from _unpause this.paused++; } }); try { func.call(this); } catch(err) { fired = 1; res = err; } } this.fired = fired; this.results[fired] = res; if (cb && this.paused ) { // this is for "tail recursion" in case the dependent // deferred is already fired res.addBoth(cb); } }; ///////////////////////////////////////////////////////////////////////////// // Plugin Initialization Class and Helper Functions ///////////////////////////////////////////////////////////////////////////// var returnFalse = function() { return false; }; var cachedHasVideo; var cachedHasAudio; var contentTests = { canvas: function() { return !!( ua.isChrome || ua.isSafari >= 3 || ua.isFF >= 3 || ua.isOpera >= 9.2 ); }, svg: function() { return !!(ua.isChrome || ua.isSafari || ua.isFF || ua.isOpera); }, postMessage: function() { return ( !!window['postMessage'] || ua.isChrome || ua.isIE >= 8 || ua.isSafari >= 3 || ua.isFF >= 3 || ua.isOpera >= 9.2 ); }, // the spec isn't settled and nothing currently supports it websocket: returnFalse, 'css-anim': function() { // pretty much limited to WebKit's special transition and animation // properties. Need to figure out a better way to triangulate this as // FF3.x adds more of these properties in parallel. return ua.isWebKit > 500; }, // "working" video/audio tag? video: function() { if (typeof cachedHasVideo != undefStr) { return cachedHasVideo; } // We haven't figured it out yet, so probe the