diff options
author | slightlyoff@chromium.org <slightlyoff@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-09-24 05:11:58 +0000 |
---|---|---|
committer | slightlyoff@chromium.org <slightlyoff@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-09-24 05:11:58 +0000 |
commit | f781782dd67077478e117c61dca4ea5eefce3544 (patch) | |
tree | 4801f724123cfdcbb69c4e7fe40a565b331723ae /chrome_frame/CFInstance.js | |
parent | 63cf4759efa2373e33436fb5df6849f930081226 (diff) | |
download | chromium_src-f781782dd67077478e117c61dca4ea5eefce3544.zip chromium_src-f781782dd67077478e117c61dca4ea5eefce3544.tar.gz chromium_src-f781782dd67077478e117c61dca4ea5eefce3544.tar.bz2 |
Initial import of the Chrome Frame codebase. Integration in chrome.gyp coming in a separate CL.
BUG=None
TEST=None
Review URL: http://codereview.chromium.org/218019
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@27042 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome_frame/CFInstance.js')
-rw-r--r-- | chrome_frame/CFInstance.js | 1656 |
1 files changed, 1656 insertions, 0 deletions
diff --git a/chrome_frame/CFInstance.js b/chrome_frame/CFInstance.js new file mode 100644 index 0000000..90a28f5 --- /dev/null +++ b/chrome_frame/CFInstance.js @@ -0,0 +1,1656 @@ +// 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( + '<scr' + 'ipt defer src="//:" ' + + 'onreadystatechange="if(this.readyState==\'complete\')' + + '{ CFInstance._documentLoaded();}">' + + '</scr' + 'ipt>' + ); + } + + // 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 || 1000) + ); + 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 > 0) && (this.paused == 0)) { + 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 <video> tag and cache the + // result. + var video = document.createElement('video'); + return cachedHasVideo = (typeof video['play'] != undefStr); + }, + + audio: function() { + if (typeof cachedHasAudio != undefStr) { + return cachedHasAudio; + } + + var audio = document.createElement('audio'); + return cachedHasAudio = (typeof audio['play'] != undefStr); + }, + + 'video-theora': function() { + return contentTests.video() && (ua.isChrome || ua.isFF > 3); + }, + + 'video-h264': function() { + return contentTests.video() && (ua.isChrome || ua.isSafari >= 4); + }, + + 'audio-vorbis': function() { + return contentTests.audio() && (ua.isChrome || ua.isFF > 3); + }, + + 'audio-mp3': function() { + return contentTests.audio() && (ua.isChrome || ua.isSafari >= 4); + }, + + // can we implement RPC over available primitives? + rpc: function() { + // on IE we need the src to be on the same domain or we need postMessage + // to work. Since we can't count on the src being same-domain, we look + // for things that have postMessage. We may re-visit this later and add + // same-domain checking and cross-window-call-as-postMessage-replacement + // code. + + // use "!!" to avoid null-is-an-object weirdness + return !!window['postMessage']; + }, + + sql: function() { + // HTML 5 databases + return !!window['openDatabase']; + }, + + storage: function(){ + // DOM storage + + // IE8, Safari, etc. support "localStorage", FF supported "globalStorage" + return !!window['globalStorage'] || !!window['localStorage']; + } + }; + + // isIE, isFF, isWebKit, etc. + forEach([ + 'isOpera', 'isWebKit', 'isChrome', 'isKhtml', 'isSafari', + 'isGecko', 'isFF', 'isIE' + ], + function(name) { + contentTests[name] = function() { + return !!ua[name]; + }; + } + ); + + /** + * Checks the list of requirements to determine if the current host browser + * meets them natively. Primarialy relies on the contentTests array. + * @param {Array} reqs A list of tests, either names of test functions in + * contentTests or functions to execute. + * @return {boolean} + * @private + */ + var testRequirements = function(reqs) { + // never use CF on Chrome or Safari + if (ua.isChrome || ua.isSafari) { + return true; + } + + var allMatch = true; + if (!reqs) { + return false; + } + forEach(reqs, function(i) { + var matches = false; + if (isFunction(i)) { + // support custom test functions + matches = i(); + } else { + // else it's a lookup by name + matches = (!!contentTests[i] && contentTests[i]()); + } + allMatch = allMatch && matches; + }); + return allMatch; + }; + + var cachedAvailable; + + /** + * Checks to find out if ChromeFrame is available as a plugin + * @return {Boolean} + * @private + */ + var isCfAvailable = function() { + if (typeof cachedAvailable != undefStr) { + return cachedAvailable; + } + + cachedAvailable = false; + var p = n.plugins; + if (typeof window['ActiveXObject'] != undefStr) { + try { + var i = new ActiveXObject('ChromeTab.ChromeFrame'); + if (i) { + cachedAvailable = true; + } + } catch(e) { + log('ChromeFrame not available, error:', e.message); + // squelch + } + } else { + for (var x = 0; x < p.length; x++) { + if (p[x].name.indexOf('Google Chrome Frame') == 0) { + cachedAvailable = true; + break; + } + } + } + return cachedAvailable; + }; + + /** + * Creates a <param> element with the specified name and value. If a parent + * is provided, the <param> element is appended to it. + * @param {string} name The name of the param + * @param {string} value The value + * @param {Node} parent Optional parent element + * @return {Boolean} + * @private + */ + var param = function(name, value, parent) { + var p = document.createElement('param'); + p.setAttribute('name', name); + p.setAttribute('value', value); + if (parent) { + parent.appendChild(p); + } + return p; + }; + + /** @type {boolean} */ + var cfStyleTagInjected = false; + + /** + * Creates a style sheet in the document which provides default styling for + * ChromeFrame instances. Successive calls should have no additive effect. + * @private + */ + var injectCFStyleTag = function() { + if (cfStyleTagInjected) { + // once and only once + return; + } + try { + var rule = ['.chromeFrameDefaultStyle {', + 'width: 400px;', + 'height: 300px;', + 'padding: 0;', + 'margin: 0;', + '}'].join(''); + var ss = document.createElement('style'); + ss.setAttribute('type', 'text/css'); + if (ss.styleSheet) { + ss.styleSheet.cssText = rule; + } else { + ss.appendChild(document.createTextNode(rule)); + } + var h = document.getElementsByTagName('head')[0]; + if (h.firstChild) { + h.insertBefore(ss, h.firstChild); + } else { + h.appendChild(ss); + } + cfStyleTagInjected = true; + } catch (e) { + // squelch + + // FIXME(slightlyoff): log? retry? + } + }; + + /** + * Plucks properties from the passed arguments and sets them on the passed + * DOM node + * @param {Node} node The node to set properties on + * @param {Object} args A map of user-specified properties to set + * @private + */ + var setProperties = function(node, args) { + injectCFStyleTag(); + + var srcNode = byId(args['node']); + + node.id = args['id'] || (srcNode ? srcNode['id'] || getUid(srcNode) : ''); + + // TODO(slightlyoff): Opera compat? need to test there + var cssText = args['cssText'] || ''; + node.style.cssText = ' ' + cssText; + + var classText = args['className'] || ''; + node.className = 'chromeFrameDefaultStyle ' + classText; + + // default if the browser doesn't so we don't show sad-tab + var src = args['src'] || 'about:blank'; + + if (ua.isIE || ua.isOpera) { + node.src = src; + } else { + // crazyness regarding when things are set in NPAPI + node.setAttribute('src', src); + } + + if (srcNode) { + srcNode.parentNode.replaceChild(node, srcNode); + } + }; + + /** + * Creates a plugin instance, taking named parameters from the passed args. + * @param {Object} args A bag of configuration properties, including values + * like 'node', 'cssText', 'className', 'id', 'src', etc. + * @return {Node} + * @private + */ + var makeCFPlugin = function(args) { + var el; // the element + if (!ua.isIE) { + el = document.createElement('object'); + el.setAttribute("type", "application/chromeframe"); + } else { + var dummy = document.createElement('span'); + dummy.innerHTML = [ + '<object codeBase="//www.google.com"', + "type='application/chromeframe'", + 'classid="CLSID:E0A900DF-9611-4446-86BD-4B1D47E7DB2A"></object>' + ].join(' '); + el = dummy.firstChild; + } + setProperties(el, args); + return el; + }; + + /** + * Creates an iframe in lieu of a ChromeFrame plugin, taking named parameters + * from the passed args. + * @param {Object} args A bag of configuration properties, including values + * like 'node', 'cssText', 'className', 'id', 'src', etc. + * @return {Node} + * @private + */ + var makeCFIframe = function(args) { + var el = document.createElement('iframe'); + setProperties(el, args); + // FIXME(slightlyoff): + // This is where we'll need to slot in "don't fire load events for + // fallback URL" logic. + listen(el, 'load', hitch(el, '_dispatch', 'load')); + return el; + }; + + + var msgPrefix = 'CFInstance.rpc:'; + + /** + * A class that provides the ability for widget-mode hosted content to more + * easily call hosting-page exposed APIs (and vice versa). It builds upon the + * message-passing nature of ChromeFrame to route messages to the other + * side's RPC host and coordinate events such as 'RPC readyness', buffering + * calls until both sides indicate they are ready to participate. + * @constructor + * @public + */ + var RPC = function(instance) { + this.initDeferred = new Deferred(); + + this.instance = instance; + + instance.listen('message', hitch(this, '_handleMessage')); + + this._localExposed = {}; + this._doWithAckCallbacks = {}; + + this._open = false; + this._msgBacklog = []; + + this._initialized = false; + this._exposeMsgBacklog = []; + + this._exposed = false; + this._callRemoteMsgBacklog = []; + + this._inFlight = {}; + + var sendLoadMsg = hitch(this, function(evt) { + this.doWithAck('load').addCallback(this, function() { + this._open = true; + this._postMessageBacklog(); + }); + }); + + if (instance['tagName']) { + instance.listen('load', sendLoadMsg); + } else { + sendLoadMsg(); + } + }; + + RPC.prototype._postMessageBacklog = function() { + if (this._open) { + forEach(this._msgBacklog, this._postMessage, this); + this._msgBacklog = []; + } + }; + + RPC.prototype._postMessage = function(msg, force) { + if (!force && !this._open) { + this._msgBacklog.push(msg); + } else { + // FIXME(slightlyoff): need to check domains list here! + this.instance.postMessage(msgPrefix + msg, '*'); + } + }; + + // currently no-ops. We may need them in the future + // RPC.prototype._doWithAck_load = function() { }; + // RPC.prototype._doWithAck_init = function() { }; + + RPC.prototype._doWithAck = function(what) { + var f = this['_doWithAck_' + what]; + if (f) { + f.call(this); + } + + this._postMessage('doWithAckCallback:' + what, what == 'load'); + }; + + RPC.prototype.doWithAck = function(what) { + var d = new Deferred(); + this._doWithAckCallbacks[what] = d; + this._postMessage('doWithAck:' + what, what == 'load'); + return d; + }; + + RPC.prototype._handleMessage = function(evt, stopper) { + var d = String(evt.data); + + if (d.indexOf(msgPrefix) != 0) { + // not for us, allow the event dispatch to continue... + return; + } + + // ...else we're the end of the line for this event + stopper(); + + // see if we know what type of message it is + d = d.substr(msgPrefix.length); + + var cIndex = d.indexOf(':'); + + var type = d.substr(0, cIndex); + + if (type == 'doWithAck') { + this._doWithAck(d.substr(cIndex + 1)); + return; + } + + var msgBody = d.substr(cIndex + 1); + + if (type == 'doWithAckCallback') { + this._doWithAckCallbacks[msgBody].callback(1); + return; + } + + if (type == 'init') { + return; + } + + // All the other stuff we can do uses a JSON payload. + var obj = fromJson(msgBody); + + if (type == 'callRemote') { + + if (obj.method && obj.params && obj.id) { + + var ret = { + success: 0, + returnId: obj.id + }; + + try { + // Undefined isn't valid JSON, so use null as default value. + ret.value = this._localExposed[ obj.method ](evt, obj) || null; + ret.success = 1; + } catch(e) { + ret.error = e.message; + } + + this._postMessage('callReturn:' + toJson(ret)); + } + } + + if (type == 'callReturn') { + // see if we're waiting on an outstanding RPC call, which + // would be identified by returnId. + var rid = obj['returnId']; + if (!rid) { + // throw an error? + return; + } + var callWrap = this._inFlight[rid]; + if (!callWrap) { + return; + } + + if (obj.success) { + callWrap.d.callback(obj['value'] || 1); + } else { + callWrap.d.errback(new Error(obj['error'] || 'unspecified RPC error')); + } + delete this._inFlight[rid]; + } + + }; + + /** + * Makes a method visible to be called + * @param {string} name The name to expose the method at. + * @param {Function|string} method The function (or name of the function) to + * expose. If a name is provided, it's looked up from the passed scope. + * If no scope is provided, the global scope is queried for a function + * with that name. + * @param {Object} scope Optional A scope to bind the passed method to. If + * the method parameter is specified by a string, the method is both + * located on the passed scope and bound to it. + * @param {Array} domains Optional A list of domains in + * 'http://example.com:8080' format which may call the given method. + * Currently un-implemented. + * @public + */ + RPC.prototype.expose = function(name, method, scope, domains) { + scope = scope || window; + method = isString(method) ? scope[method] : method; + + // local call proxy that the other side will hit when calling our method + this._localExposed[name] = function(evt, obj) { + return method.apply(scope, obj.params); + }; + + if (!this._initialized) { + this._exposeMsgBacklog.push(arguments); + return; + } + + var a = [name, method, scope, domains]; + this._sendExpose.apply(this, a); + }; + + RPC.prototype._sendExpose = function(name) { + // now tell the other side that we're advertising this method + this._postMessage('expose:' + toJson({ name: name })); + }; + + + /** + * Calls a remote method asynchronously and returns a Deferred object + * representing the response. + * @param {string} method Name of the method to call. Should be the same name + * which the other side has expose()'d. + * @param {Array} params Optional A list of arguments to pass to the called + * method. All elements in the list must be cleanly serializable to + * JSON. + * @param {CFInstance.Deferred} deferred Optional A Deferred to use for + * reporting the response of the call. If no deferred is passed, a new + * Deferred is created and returned. + * @return {CFInstance.Deferred} + * @public + */ + RPC.prototype.callRemote = function(method, params, timeout, deferred) { + var d = deferred || new Deferred(null, timeout || -1); + + if (!this._exposed) { + var args = toArray(arguments); + args.length = 3; + args.push(d); + this._callRemoteMsgBacklog.push(args); + return d; + } + + + if (!method) { + d.errback('no method provided!'); + return d; + } + + var id = msgPrefix + (_counter++); + + // JSON-ify the whole bundle + var callWrapper = { + method: String(method), + params: params || [], + id: id + }; + var callJson = toJson(callWrapper); + callWrapper.d = d; + this._inFlight[id] = callWrapper; + this._postMessage('callRemote:' + callJson); + return d; + }; + + + /** + * Tells the other side of the connection that we're ready to start receiving + * calls. Returns a Deferred that is called back when both sides have + * initialized and any backlogged messages have been sent. RPC users should + * generally work to make sure that they call expose() on all of the methods + * they'd like to make available to the other side *before* calling init() + * @return {CFInstance.Deferred} + * @public + */ + RPC.prototype.init = function() { + var d = this.initDeferred; + this.doWithAck('init').addCallback(this, function() { + // once the init floodgates are open, send our backlogs one at a time, + // with a little lag in the middle to prevent ordering problems + + this._initialized = true; + while (this._exposeMsgBacklog.length) { + this.expose.apply(this, this._exposeMsgBacklog.shift()); + } + + setTimeout(hitch(this, function(){ + + this._exposed = true; + while (this._callRemoteMsgBacklog.length) { + this.callRemote.apply(this, this._callRemoteMsgBacklog.shift()); + } + + d.callback(1); + + }), 30); + + }); + return d; + }; + + // CFInstance design notes: + // + // The CFInstance constructor is only ever used in host environments. In + // content pages (things hosted by a ChromeFrame instance), CFInstance + // acts as a singleton which provides services like RPC for communicating + // with it's mirror-image object in the hosting environment. We want the + // same methods and properties to be available on *instances* of + // CFInstance objects in the host env as on the singleton in the hosted + // content, despite divergent implementation. + // + // Further complicating things, CFInstance may specialize behavior + // internally based on whether or not it is communicationg with a fallback + // iframe or a 'real' ChromeFrame instance. + + var CFInstance; // forward declaration + var h = window['externalHost']; + var inIframe = (window.parent != window); + + if (inIframe) { + h = window.parent; + } + + var normalizeTarget = function(targetOrigin) { + var l = window.location; + if (!targetOrigin) { + if (l.protocol != 'file:') { + targetOrigin = l.protocol + '//' + l.host + "/"; + } else { + // TODO(slightlyoff): + // is this secure enough? Is there another way to get messages + // flowing reliably across file-hosted documents? + targetOrigin = '*'; + } + } + return targetOrigin; + }; + + var postMessageToDest = function(dest, msg, targetOrigin) { + return dest.postMessage(msg, normalizeTarget(targetOrigin)); + }; + + if (h) { + // + // We're loaded inside a ChromeFrame widget (or something that should look + // like we were). + // + + CFInstance = {}; + + installEvtSys(CFInstance); + + // FIXME(slightlyoff): + // passing a target origin to externalHost's postMessage seems b0rked + // right now, so pass null instead. Will re-enable hitch()'d variant + // once that's fixed. + + // CFInstance.postMessage = hitch(null, postMessageToDest, h); + + CFInstance.postMessage = function(msg, targetOrigin) { + return h.postMessage(msg, + (inIframe ? normalizeTarget(targetOrigin) : null) ); + }; + + // Attach to the externalHost's onmessage to proxy it in to CFInstance's + // onmessage. + var dispatchMsg = function(evt) { + try { + CFInstance._dispatch('message', evt); + } catch(e) { + log(e); + // squelch + } + }; + if (inIframe) { + listen(window, 'message', dispatchMsg); + } else { + h.onmessage = dispatchMsg; + } + + CFInstance.rpc = new RPC(CFInstance); + + _loadedListenerList.push(function(evt) { + CFInstance._dispatch('load', evt); + }); + + } else { + // + // We're the host document. + // + + var installProperties = function(instance, args) { + var s = instance.supportedEvents = ['load', 'message']; + instance._msgPrefix = 'CFMessage:'; + + installEvtSys(instance); + + instance.log = log; + + // set up an RPC instance + instance.rpc = new RPC(instance); + + forEach(s, function(evt) { + var l = args['on' + evt]; + if (l) { + instance.listen(evt, l); + } + }); + + var contentWindow = instance.contentWindow; + + // if it doesn't have a postMessage, route to/from the iframe's built-in + if (typeof instance['postMessage'] == undefStr && !!contentWindow) { + + instance.postMessage = hitch(null, postMessageToDest, contentWindow); + + listen(window, 'message', function(evt) { + if (evt.source == contentWindow) { + instance._dispatch('message', evt); + } + }); + } + + return instance; + }; + + /** + * A class whose instances correspond to ChromeFrame instances. Passing an + * arguments object to CFInstance helps parameterize the instance. + * @constructor + * @public + */ + CFInstance = function(args) { + args = args || {}; + var instance; + var success = false; + + // If we've been passed a CFInstance object as our source node, just + // re-use it. + if (args['node']) { + var n = byId(args['node']); + // Look for CF-specific properties. + if (n && n.tagName == 'OBJECT' && n.success && n.rpc) { + // Navigate, set styles, etc. + setProperties(n, args); + return n; + } + } + + var force = !!args['forcePlugin']; + + if (!force && testRequirements(args['requirements'])) { + instance = makeCFIframe(args); + success = true; + } else if (isCfAvailable()) { + instance = makeCFPlugin(args); + success = true; + } else { + // else create an iframe but load the failure content and + // not the 'normal' content + + // grab the fallback URL, and if none, use the 'click here + // to install ChromeFrame' URL. Note that we only support + // polling for install success if we're using the default + // URL + + var fallback = '//www.google.com/chromeframe'; + + args.src = args['fallback'] || fallback; + instance = makeCFIframe(args); + + if (args.src == fallback) { + // begin polling for install success. + + // TODO(slightlyoff): need to prevent firing of onload hooks! + // TODO(slightlyoff): implement polling + // TODO(slightlyoff): replacement callback? + // TODO(slightlyoff): add flag to disable this behavior + } + } + instance.success = success; + + installProperties(instance, args); + + return instance; + }; + + // compatibility shims for development-time. These mirror the methods that + // are created on the CFInstance singleton if we detect that we're running + // inside of CF. + if (!CFInstance['postMessage']) { + CFInstance.postMessage = function() { + var args = toArray(arguments); + args.unshift('CFInstance.postMessage:'); + log.apply(null, args); + }; + CFInstance.listen = function() { + // this space intentionally left blank + }; + } + } + + // expose some properties + CFInstance.ua = ua; + CFInstance._documentLoaded = documentLoaded; + CFInstance.contentTests = contentTests; + CFInstance.isAvailable = function(requirements) { + var hasCf = isCfAvailable(); + return requirements ? (hasCf || testRequirements(requirements)) : hasCf; + + }; + CFInstance.Deferred = Deferred; + CFInstance.toJson = toJson; + CFInstance.fromJson = fromJson; + CFInstance.log = log; + + // expose CFInstance to the external scope. We've already checked to make + // sure we're not going to blow existing objects away. + scope.CFInstance = CFInstance; + +})( this['ChromeFrameScope'] || this ); + +// vim: shiftwidth=2:et:ai:tabstop=2 |