OLD | NEW |
| (Empty) |
1 // Copyright (c) 2009 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 // Parts Copyright 2005-2009, the Dojo Foundation. Used under the terms of the | |
6 // "New" BSD License: | |
7 // | |
8 // http://download.dojotoolkit.org/release-1.3.2/dojo-release-1.3.2/dojo/LICENS
E | |
9 // | |
10 | |
11 /** | |
12 * @fileoverview CFInstance.js provides a set of utilities for managing | |
13 * ChromeFrame plugins, including creation, RPC services, and a singleton to | |
14 * use for communicating from ChromeFrame hosted content to an external | |
15 * CFInstance wrapper. CFInstance.js is stand-alone, designed to be served from | |
16 * a CDN, and built to not create side-effects for other hosted content. | |
17 * @author slightlyoff@google.com (Alex Russell) | |
18 */ | |
19 | |
20 (function(scope) { | |
21 // TODO: | |
22 // * figure out if there's any way to install w/o a browser restart, and if | |
23 // so, where and how | |
24 // * slim down Deferred and RPC scripts | |
25 // * determine what debugging APIs should be exposed and how they should be | |
26 // surfaced. What about content authoring in Chrome instances? Stubbing | |
27 // the other side of RPC's? | |
28 | |
29 // bail if we'd be over-writing an existing CFInstance object | |
30 if (scope['CFInstance']) { | |
31 return; | |
32 } | |
33 | |
34 ///////////////////////////////////////////////////////////////////////////// | |
35 // Utiliity and Cross-Browser Functions | |
36 ///////////////////////////////////////////////////////////////////////////// | |
37 | |
38 // a monotonically incrementing counter | |
39 var _counter = 0; | |
40 | |
41 var undefStr = 'undefined'; | |
42 | |
43 // | |
44 // Browser detection: ua.isIE, ua.isSafari, ua.isOpera, etc. | |
45 // | |
46 | |
47 /** | |
48 * An object for User Agent detection | |
49 * @type {!Object} | |
50 * @protected | |
51 */ | |
52 var ua = {}; | |
53 var n = navigator; | |
54 var dua = String(n.userAgent); | |
55 var dav = String(n.appVersion); | |
56 var tv = parseFloat(dav); | |
57 var duaParse = function(s){ | |
58 var c = 0; | |
59 try { | |
60 return parseFloat( | |
61 dua.split(s)[1].replace(/\./g, function() { | |
62 c++; | |
63 return (c > 1) ? '' : '.'; | |
64 } ) | |
65 ); | |
66 } catch(e) { | |
67 // squelch to intentionally return undefined | |
68 } | |
69 }; | |
70 /** @type {number} */ | |
71 ua.isOpera = dua.indexOf('Opera') >= 0 ? tv: undefined; | |
72 /** @type {number} */ | |
73 ua.isWebKit = duaParse('WebKit/'); | |
74 /** @type {number} */ | |
75 ua.isChrome = duaParse('Chrome/'); | |
76 /** @type {number} */ | |
77 ua.isKhtml = dav.indexOf('KHTML') >= 0 ? tv : undefined; | |
78 | |
79 var index = Math.max(dav.indexOf('WebKit'), dav.indexOf('Safari'), 0); | |
80 | |
81 if (index && !ua.isChrome) { | |
82 /** @type {number} */ | |
83 ua.isSafari = parseFloat(dav.split('Version/')[1]); | |
84 if(!ua.isSafari || parseFloat(dav.substr(index + 7)) <= 419.3){ | |
85 ua.isSafari = 2; | |
86 } | |
87 } | |
88 | |
89 if (dua.indexOf('Gecko') >= 0 && !ua.isKhtml) { | |
90 /** @type {number} */ | |
91 ua.isGecko = duaParse(' rv:'); | |
92 } | |
93 | |
94 if (ua.isGecko) { | |
95 /** @type {number} */ | |
96 ua.isFF = parseFloat(dua.split('Firefox/')[1]) || undefined; | |
97 } | |
98 | |
99 if (document.all && !ua.isOpera) { | |
100 /** @type {number} */ | |
101 ua.isIE = parseFloat(dav.split('MSIE ')[1]) || undefined; | |
102 } | |
103 | |
104 | |
105 /** | |
106 * Log out varargs to a browser-provided console object (if available). Else | |
107 * a no-op. | |
108 * @param {*} var_args Optional Things to log. | |
109 * @protected | |
110 **/ | |
111 var log = function() { | |
112 if (window['console']) { | |
113 try { | |
114 if (ua.isSafari || ua.isChrome) { | |
115 throw Error(); | |
116 } | |
117 console.log.apply(console, arguments); | |
118 } catch(e) { | |
119 try { | |
120 console.log(toArray(arguments).join(' ')); | |
121 } catch(e2) { | |
122 // squelch | |
123 } | |
124 } | |
125 } | |
126 }; | |
127 | |
128 // | |
129 // Language utility methods | |
130 // | |
131 | |
132 /** | |
133 * Determine if the passed item is a String | |
134 * @param {*} item Item to test. | |
135 * @protected | |
136 **/ | |
137 var isString = function(item) { | |
138 return typeof item == 'string'; | |
139 }; | |
140 | |
141 /** | |
142 * Determine if the passed item is a Function object | |
143 * @param {*} item Item to test. | |
144 * @protected | |
145 **/ | |
146 var isFunction = function(item) { | |
147 return ( | |
148 item && ( | |
149 typeof item == 'function' || item instanceof Function | |
150 ) | |
151 ); | |
152 }; | |
153 | |
154 /** | |
155 * Determine if the passed item is an array. | |
156 * @param {*} item Item to test. | |
157 * @protected | |
158 **/ | |
159 var isArray = function(item){ | |
160 return ( | |
161 item && ( | |
162 item instanceof Array || ( | |
163 typeof item == 'object' && | |
164 typeof item.length != undefStr | |
165 ) | |
166 ) | |
167 ); | |
168 }; | |
169 | |
170 /** | |
171 * A toArray version which takes advantage of builtins | |
172 * @param {*} obj The array-like object to convert to a real array. | |
173 * @param {number} opt_offset An index to being copying from in the source. | |
174 * @param {Array} opt_startWith An array to extend with elements of obj in | |
175 * lieu of creating a new array to return. | |
176 * @private | |
177 **/ | |
178 var _efficientToArray = function(obj, opt_offset, opt_startWith){ | |
179 return (opt_startWith || []).concat( | |
180 Array.prototype.slice.call(obj, opt_offset || 0 ) | |
181 ); | |
182 }; | |
183 | |
184 /** | |
185 * A version of toArray that iterates in lieu of using array generics. | |
186 * @param {*} obj The array-like object to convert to a real array. | |
187 * @param {number} opt_offset An index to being copying from in the source. | |
188 * @param {Array} opt_startWith An array to extend with elements of obj in | |
189 * @private | |
190 **/ | |
191 var _slowToArray = function(obj, opt_offset, opt_startWith){ | |
192 var arr = opt_startWith || []; | |
193 for(var x = opt_offset || 0; x < obj.length; x++){ | |
194 arr.push(obj[x]); | |
195 } | |
196 return arr; | |
197 }; | |
198 | |
199 /** | |
200 * Converts an array-like object (e.g., an "arguments" object) to a real | |
201 * Array. | |
202 * @param {*} obj The array-like object to convert to a real array. | |
203 * @param {number} opt_offset An index to being copying from in the source. | |
204 * @param {Array} opt_startWith An array to extend with elements of obj in | |
205 * @protected | |
206 */ | |
207 var toArray = ua.isIE ? | |
208 function(obj){ | |
209 return ( | |
210 obj.item ? _slowToArray : _efficientToArray | |
211 ).apply(this, arguments); | |
212 } : | |
213 _efficientToArray; | |
214 | |
215 var _getParts = function(arr, obj, cb){ | |
216 return [ | |
217 isString(arr) ? arr.split('') : arr, | |
218 obj || window, | |
219 isString(cb) ? new Function('item', 'index', 'array', cb) : cb | |
220 ]; | |
221 }; | |
222 | |
223 /** | |
224 * like JS1.6 Array.forEach() | |
225 * @param {Array} arr the array to iterate | |
226 * @param {function(Object, number, Array)} callback the method to invoke for | |
227 * each item in the array | |
228 * @param {function?} thisObject Optional a scope to use with callback | |
229 * @return {array} the original arr | |
230 * @protected | |
231 */ | |
232 var forEach = function(arr, callback, thisObject) { | |
233 if(!arr || !arr.length){ | |
234 return arr; | |
235 } | |
236 var parts = _getParts(arr, thisObject, callback); | |
237 // parts has a structure of: | |
238 // [ | |
239 // array, | |
240 // scope, | |
241 // function | |
242 // ] | |
243 arr = parts[0]; | |
244 for (var i = 0, l = arr.length; i < l; ++i) { | |
245 parts[2].call( parts[1], arr[i], i, arr ); | |
246 } | |
247 return arr; | |
248 }; | |
249 | |
250 /** | |
251 * returns a new function bound to scope with a variable number of positional | |
252 * params pre-filled | |
253 * @private | |
254 */ | |
255 var _hitchArgs = function(scope, method /*,...*/) { | |
256 var pre = toArray(arguments, 2); | |
257 var named = isString(method); | |
258 return function() { | |
259 var args = toArray(arguments); | |
260 var f = named ? (scope || window)[method] : method; | |
261 return f && f.apply(scope || this, pre.concat(args)); | |
262 } | |
263 }; | |
264 | |
265 /** | |
266 * Like goog.bind(). Hitches the method (named or provided as a function | |
267 * object) to scope, optionally partially applying positional arguments. | |
268 * @param {Object} scope the object to hitch the method to | |
269 * @param {string|function} method the method to be bound | |
270 * @return {function} The bound method | |
271 * @protected | |
272 */ | |
273 var hitch = function(scope, method){ | |
274 if (arguments.length > 2) { | |
275 return _hitchArgs.apply(window, arguments); // Function | |
276 } | |
277 | |
278 if (!method) { | |
279 method = scope; | |
280 scope = null; | |
281 } | |
282 | |
283 if (isString(method)) { | |
284 scope = scope || window; | |
285 if (!scope[method]) { | |
286 throw( | |
287 ['scope["', method, '"] is null (scope="', scope, '")'].join('') | |
288 ); | |
289 } | |
290 return function() { | |
291 return scope[method].apply(scope, arguments || []); | |
292 }; | |
293 } | |
294 | |
295 return !scope ? | |
296 method : | |
297 function() { | |
298 return method.apply(scope, arguments || []); | |
299 }; | |
300 }; | |
301 | |
302 /** | |
303 * A version of addEventListener that works on IE too. *sigh*. | |
304 * @param {!Object} obj The object to attach to | |
305 * @param {!String} type Name of the event to attach to | |
306 * @param {!Function} handler The function to connect | |
307 * @protected | |
308 */ | |
309 var listen = function(obj, type, handler) { | |
310 if (obj['attachEvent']) { | |
311 obj.attachEvent('on' + type, handler); | |
312 } else { | |
313 obj.addEventListener(type, handler, false); | |
314 } | |
315 }; | |
316 | |
317 /** | |
318 * Adds "listen" and "_dispatch" methods to the passed object, taking | |
319 * advantage of native event hanlding if it's available. | |
320 * @param {Object} instance The object to install the event system on | |
321 * @protected | |
322 */ | |
323 var installEvtSys = function(instance) { | |
324 var eventsMap = {}; | |
325 | |
326 var isNative = ( | |
327 (typeof instance.addEventListener != undefStr) && | |
328 ((instance['tagName'] || '').toLowerCase() != 'iframe') | |
329 ); | |
330 | |
331 instance.listen = function(type, func) { | |
332 var t = eventsMap[type]; | |
333 if (!t) { | |
334 t = eventsMap[type] = []; | |
335 if (isNative) { | |
336 listen(instance, type, hitch(instance, "_dispatch", type)); | |
337 } | |
338 } | |
339 t.push(func); | |
340 return t; | |
341 }; | |
342 | |
343 instance._dispatch = function(type, evt) { | |
344 var stopped = false; | |
345 var stopper = function() { | |
346 stopped = true; | |
347 }; | |
348 | |
349 forEach(eventsMap[type], function(f) { | |
350 if (!stopped) { | |
351 f(evt, stopper); | |
352 } | |
353 }); | |
354 }; | |
355 | |
356 return instance; | |
357 }; | |
358 | |
359 /** | |
360 * Deserialize the passed JSON string | |
361 * @param {!String} json A string to be deserialized | |
362 * @return {Object} | |
363 * @protected | |
364 */ | |
365 var fromJson = window['JSON'] ? function(json) { | |
366 return JSON.parse(json); | |
367 } : | |
368 function(json) { | |
369 return eval('(' + (json || undefStr) + ')'); | |
370 }; | |
371 | |
372 /** | |
373 * String escaping for use in JSON serialization | |
374 * @param {string} str The string to escape | |
375 * @return {string} | |
376 * @private | |
377 */ | |
378 var _escapeString = function(str) { | |
379 return ('"' + str.replace(/(["\\])/g, '\\$1') + '"'). | |
380 replace(/[\f]/g, '\\f'). | |
381 replace(/[\b]/g, '\\b'). | |
382 replace(/[\n]/g, '\\n'). | |
383 replace(/[\t]/g, '\\t'). | |
384 replace(/[\r]/g, '\\r'). | |
385 replace(/[\x0B]/g, '\\u000b'); // '\v' is not supported in JScript; | |
386 }; | |
387 | |
388 /** | |
389 * JSON serialization for arbitrary objects. Circular references or strong | |
390 * typing information are not handled. | |
391 * @param {Object} it Any valid JavaScript object or type | |
392 * @return {string} the serialized representation of the passed object | |
393 * @protected | |
394 */ | |
395 var toJson = window['JSON'] ? function(it) { | |
396 return JSON.stringify(it); | |
397 } : | |
398 function(it) { | |
399 | |
400 if (it === undefined) { | |
401 return undefStr; | |
402 } | |
403 | |
404 var objtype = typeof it; | |
405 if (objtype == 'number' || objtype == 'boolean') { | |
406 return it + ''; | |
407 } | |
408 | |
409 if (it === null) { | |
410 return 'null'; | |
411 } | |
412 | |
413 if (isString(it)) { | |
414 return _escapeString(it); | |
415 } | |
416 | |
417 // recurse | |
418 var recurse = arguments.callee; | |
419 | |
420 if(it.nodeType && it.cloneNode){ // isNode | |
421 // we can't seriailize DOM nodes as regular objects because they have | |
422 // cycles DOM nodes could be serialized with something like outerHTML, | |
423 // but that can be provided by users in the form of .json or .__json__ | |
424 // function. | |
425 throw new Error('Cannot serialize DOM nodes'); | |
426 } | |
427 | |
428 // array | |
429 if (isArray(it)) { | |
430 var res = []; | |
431 forEach(it, function(obj) { | |
432 var val = recurse(obj); | |
433 if (typeof val != 'string') { | |
434 val = undefStr; | |
435 } | |
436 res.push(val); | |
437 }); | |
438 return '[' + res.join(',') + ']'; | |
439 } | |
440 | |
441 if (objtype == 'function') { | |
442 return null; | |
443 } | |
444 | |
445 // generic object code path | |
446 var output = []; | |
447 for (var key in it) { | |
448 var keyStr, val; | |
449 if (typeof key == 'number') { | |
450 keyStr = '"' + key + '"'; | |
451 } else if(typeof key == 'string') { | |
452 keyStr = _escapeString(key); | |
453 } else { | |
454 // skip non-string or number keys | |
455 continue; | |
456 } | |
457 val = recurse(it[key]); | |
458 if (typeof val != 'string') { | |
459 // skip non-serializable values | |
460 continue; | |
461 } | |
462 // TODO(slightlyoff): use += on Moz since it's faster there | |
463 output.push(keyStr + ':' + val); | |
464 } | |
465 return '{' + output.join(',') + '}'; // String | |
466 }; | |
467 | |
468 // code to register with the earliest safe onload-style handler | |
469 | |
470 var _loadedListenerList = []; | |
471 var _loadedFired = false; | |
472 | |
473 /** | |
474 * a default handler for document onload. When called (the first time), | |
475 * iterates over the list of registered listeners, calling them in turn. | |
476 * @private | |
477 */ | |
478 var documentLoaded = function() { | |
479 if (!_loadedFired) { | |
480 _loadedFired = true; | |
481 forEach(_loadedListenerList, 'item();'); | |
482 } | |
483 }; | |
484 | |
485 if (document.addEventListener) { | |
486 // NOTE: | |
487 // due to a threading issue in Firefox 2.0, we can't enable | |
488 // DOMContentLoaded on that platform. For more information, see: | |
489 // http://trac.dojotoolkit.org/ticket/1704 | |
490 if (ua.isWebKit > 525 || ua.isOpera || ua.isFF >= 3) { | |
491 listen(document, 'DOMContentLoaded', documentLoaded); | |
492 } | |
493 // mainly for Opera 8.5, won't be fired if DOMContentLoaded fired already. | |
494 // also used for FF < 3.0 due to nasty DOM race condition | |
495 listen(window, 'load', documentLoaded); | |
496 } else { | |
497 // crazy hack for IE that relies on the "deferred" behavior of script | |
498 // tags | |
499 document.write( | |
500 '<scr' + 'ipt defer src="//:" ' | |
501 + 'onreadystatechange="if(this.readyState==\'complete\')' | |
502 + '{ CFInstance._documentLoaded();}">' | |
503 + '</scr' + 'ipt>' | |
504 ); | |
505 } | |
506 | |
507 // TODO(slightlyoff): known KHTML init issues are ignored for now | |
508 | |
509 // | |
510 // DOM utility methods | |
511 // | |
512 | |
513 /** | |
514 * returns an item based on DOM ID. Optionally a doucment may be provided to | |
515 * specify the scope to search in. If a node is passed, it's returned as-is. | |
516 * @param {string|Node} id The ID of the node to be located or a node | |
517 * @param {Node} doc Optional A document to search for id. | |
518 * @return {Node} | |
519 * @protected | |
520 */ | |
521 var byId = (ua.isIE || ua.isOpera) ? | |
522 function(id, doc) { | |
523 if (isString(id)) { | |
524 doc = doc || document; | |
525 var te = doc.getElementById(id); | |
526 // attributes.id.value is better than just id in case the | |
527 // user has a name=id inside a form | |
528 if (te && te.attributes.id.value == id) { | |
529 return te; | |
530 } else { | |
531 var elements = doc.all[id]; | |
532 if (!elements || !elements.length) { | |
533 return elements; | |
534 } | |
535 // if more than 1, choose first with the correct id | |
536 var i=0; | |
537 while (te = elements[i++]) { | |
538 if (te.attributes.id.value == id) { | |
539 return te; | |
540 } | |
541 } | |
542 } | |
543 } else { | |
544 return id; // DomNode | |
545 } | |
546 } : | |
547 function(id, doc) { | |
548 return isString(id) ? (doc || document).getElementById(id) : id; | |
549 }; | |
550 | |
551 | |
552 /** | |
553 * returns a unique DOM id which can be used to locate the node via byId(). | |
554 * If the node already has an ID, it's used. If not, one is generated. Like | |
555 * IE's uniqueID property. | |
556 * @param {Node} node The element to create or fetch a unique ID for | |
557 * @return {String} | |
558 * @protected | |
559 */ | |
560 var getUid = function(node) { | |
561 var u = 'cfUnique' + (_counter++); | |
562 return (!node) ? u : ( node.id || node.uniqueID || (node.id = u) ); | |
563 }; | |
564 | |
565 // | |
566 // the Deferred class, borrowed from Twisted Python and Dojo | |
567 // | |
568 | |
569 /** | |
570 * A class that models a single response (past or future) to a question. | |
571 * Multiple callbacks and error handlers may be added. If the response was | |
572 * added in the past, adding callbacks has the effect of calling them | |
573 * immediately. In this way, Deferreds simplify thinking about synchronous | |
574 * vs. asynchronous programming in languages which don't have continuations | |
575 * or generators which might otherwise provide syntax for deferring | |
576 * operations. | |
577 * @param {function} canceller Optional A function to be called when the | |
578 * Deferred is canceled. | |
579 * @param {number} timeout Optional How long to wait (in ms) before errback | |
580 * is called with a timeout error. If no timeout is passed, the default | |
581 * is 1hr. Passing -1 will disable any timeout. | |
582 * @constructor | |
583 * @public | |
584 */ | |
585 Deferred = function(/*Function?*/ canceller, timeout){ | |
586 // example: | |
587 // var deferred = new Deferred(); | |
588 // setTimeout(function(){ deferred.callback({success: true}); }, 1000); | |
589 // return deferred; | |
590 this.chain = []; | |
591 this.id = _counter++; | |
592 this.fired = -1; | |
593 this.paused = 0; | |
594 this.results = [ null, null ]; | |
595 this.canceller = canceller; | |
596 // FIXME(slightlyoff): is it really smart to be creating this many timers? | |
597 if (typeof timeout == 'number') { | |
598 if (timeout <= 0) { | |
599 timeout = 216000; // give it an hour | |
600 } | |
601 } | |
602 this._timer = setTimeout( | |
603 hitch(this, 'errback', new Error('timeout')), | |
604 (timeout || 10000) | |
605 ); | |
606 this.silentlyCancelled = false; | |
607 }; | |
608 | |
609 /** | |
610 * Cancels a Deferred that has not yet received a value, or is waiting on | |
611 * another Deferred as its value. If a canceller is defined, the canceller | |
612 * is called. If the canceller did not return an error, or there was no | |
613 * canceller, then the errback chain is started. | |
614 * @public | |
615 */ | |
616 Deferred.prototype.cancel = function() { | |
617 var err; | |
618 if (this.fired == -1) { | |
619 if (this.canceller) { | |
620 err = this.canceller(this); | |
621 } else { | |
622 this.silentlyCancelled = true; | |
623 } | |
624 if (this.fired == -1) { | |
625 if ( !(err instanceof Error) ) { | |
626 var res = err; | |
627 var msg = 'Deferred Cancelled'; | |
628 if (err && err.toString) { | |
629 msg += ': ' + err.toString(); | |
630 } | |
631 err = new Error(msg); | |
632 err.dType = 'cancel'; | |
633 err.cancelResult = res; | |
634 } | |
635 this.errback(err); | |
636 } | |
637 } else if ( | |
638 (this.fired == 0) && | |
639 (this.results[0] instanceof Deferred) | |
640 ) { | |
641 this.results[0].cancel(); | |
642 } | |
643 }; | |
644 | |
645 | |
646 /** | |
647 * internal function for providing a result. If res is an instance of Error, | |
648 * we treat it like such and start the error chain. | |
649 * @param {Object|Error} res the result | |
650 * @private | |
651 */ | |
652 Deferred.prototype._resback = function(res) { | |
653 if (this._timer) { | |
654 clearTimeout(this._timer); | |
655 } | |
656 this.fired = res instanceof Error ? 1 : 0; | |
657 this.results[this.fired] = res; | |
658 this._fire(); | |
659 }; | |
660 | |
661 /** | |
662 * determine if the deferred has already been resolved | |
663 * @return {boolean} | |
664 * @private | |
665 */ | |
666 Deferred.prototype._check = function() { | |
667 if (this.fired != -1) { | |
668 if (!this.silentlyCancelled) { | |
669 return 0; | |
670 } | |
671 this.silentlyCancelled = 0; | |
672 return 1; | |
673 } | |
674 return 0; | |
675 }; | |
676 | |
677 /** | |
678 * Begin the callback sequence with a non-error value. | |
679 * @param {Object|Error} res the result | |
680 * @public | |
681 */ | |
682 Deferred.prototype.callback = function(res) { | |
683 this._check(); | |
684 this._resback(res); | |
685 }; | |
686 | |
687 /** | |
688 * Begin the callback sequence with an error result. | |
689 * @param {Error|string} res the result. If not an Error, it's treated as the | |
690 * message for a new Error. | |
691 * @public | |
692 */ | |
693 Deferred.prototype.errback = function(res) { | |
694 this._check(); | |
695 if ( !(res instanceof Error) ) { | |
696 res = new Error(res); | |
697 } | |
698 this._resback(res); | |
699 }; | |
700 | |
701 /** | |
702 * Add a single function as the handler for both callback and errback, | |
703 * allowing you to specify a scope (unlike addCallbacks). | |
704 * @param {function|Object} cb A function. If cbfn is passed, the value of cb | |
705 * is treated as a scope | |
706 * @param {function|string} cbfn Optional A function or name of a function in | |
707 * the scope cb. | |
708 * @return {Deferred} this | |
709 * @public | |
710 */ | |
711 Deferred.prototype.addBoth = function(cb, cbfn) { | |
712 var enclosed = hitch.apply(window, arguments); | |
713 return this.addCallbacks(enclosed, enclosed); | |
714 }; | |
715 | |
716 /** | |
717 * Add a single callback to the end of the callback sequence. Add a function | |
718 * as the handler for successful resolution of the Deferred. May be called | |
719 * multiple times to register many handlers. Note that return values are | |
720 * chained if provided, so it's best for callback handlers not to return | |
721 * anything. | |
722 * @param {function|Object} cb A function. If cbfn is passed, the value of cb | |
723 * is treated as a scope | |
724 * @param {function|string} cbfn Optional A function or name of a function in | |
725 * the scope cb. | |
726 * @return {Deferred} this | |
727 * @public | |
728 */ | |
729 Deferred.prototype.addCallback = function(cb, cbfn /*...*/) { | |
730 return this.addCallbacks(hitch.apply(window, arguments)); | |
731 }; | |
732 | |
733 | |
734 /** | |
735 * Add a function as the handler for errors in the Deferred. May be called | |
736 * multiple times to add multiple error handlers. | |
737 * @param {function|Object} cb A function. If cbfn is passed, the value of cb | |
738 * is treated as a scope | |
739 * @param {function|string} cbfn Optional A function or name of a function in | |
740 * the scope cb. | |
741 * @return {Deferred} this | |
742 * @public | |
743 */ | |
744 Deferred.prototype.addErrback = function(cb, cbfn) { | |
745 return this.addCallbacks(null, hitch.apply(window, arguments)); | |
746 }; | |
747 | |
748 /** | |
749 * Add a functions as handlers for callback and errback in a single shot. | |
750 * @param {function} callback A function | |
751 * @param {function} errback A function | |
752 * @return {Deferred} this | |
753 * @public | |
754 */ | |
755 Deferred.prototype.addCallbacks = function(callback, errback) { | |
756 this.chain.push([callback, errback]); | |
757 if (this.fired >= 0) { | |
758 this._fire(); | |
759 } | |
760 return this; | |
761 }; | |
762 | |
763 /** | |
764 * when this Deferred is satisfied, pass it on to def, allowing it to run. | |
765 * @param {Deferred} def A deferred to add to the end of this Deferred in a ch
ain | |
766 * @return {Deferred} this | |
767 * @public | |
768 */ | |
769 Deferred.prototype.chain = function(def) { | |
770 this.addCallbacks(def.callback, def.errback); | |
771 return this; | |
772 }; | |
773 | |
774 /** | |
775 * Used internally to exhaust the callback sequence when a result is | |
776 * available. | |
777 * @private | |
778 */ | |
779 Deferred.prototype._fire = function() { | |
780 var chain = this.chain; | |
781 var fired = this.fired; | |
782 var res = this.results[fired]; | |
783 var cb = null; | |
784 while ((chain.length) && (!this.paused)) { | |
785 var f = chain.shift()[fired]; | |
786 if (!f) { | |
787 continue; | |
788 } | |
789 var func = hitch(this, function() { | |
790 var ret = f(res); | |
791 //If no response, then use previous response. | |
792 if (typeof ret != undefStr) { | |
793 res = ret; | |
794 } | |
795 fired = res instanceof Error ? 1 : 0; | |
796 if (res instanceof Deferred) { | |
797 cb = function(res) { | |
798 this._resback(res); | |
799 // inlined from _pause() | |
800 this.paused--; | |
801 if ( (this.paused == 0) && (this.fired >= 0)) { | |
802 this._fire(); | |
803 } | |
804 } | |
805 // inlined from _unpause | |
806 this.paused++; | |
807 } | |
808 }); | |
809 | |
810 try { | |
811 func.call(this); | |
812 } catch(err) { | |
813 fired = 1; | |
814 res = err; | |
815 } | |
816 } | |
817 | |
818 this.fired = fired; | |
819 this.results[fired] = res; | |
820 if (cb && this.paused ) { | |
821 // this is for "tail recursion" in case the dependent | |
822 // deferred is already fired | |
823 res.addBoth(cb); | |
824 } | |
825 }; | |
826 | |
827 ///////////////////////////////////////////////////////////////////////////// | |
828 // Plugin Initialization Class and Helper Functions | |
829 ///////////////////////////////////////////////////////////////////////////// | |
830 | |
831 var returnFalse = function() { | |
832 return false; | |
833 }; | |
834 | |
835 var cachedHasVideo; | |
836 var cachedHasAudio; | |
837 | |
838 var contentTests = { | |
839 canvas: function() { | |
840 return !!( | |
841 ua.isChrome || ua.isSafari >= 3 || ua.isFF >= 3 || ua.isOpera >= 9.2 | |
842 ); | |
843 }, | |
844 | |
845 svg: function() { | |
846 return !!(ua.isChrome || ua.isSafari || ua.isFF || ua.isOpera); | |
847 }, | |
848 | |
849 postMessage: function() { | |
850 return ( | |
851 !!window['postMessage'] || | |
852 ua.isChrome || | |
853 ua.isIE >= 8 || | |
854 ua.isSafari >= 3 || | |
855 ua.isFF >= 3 || | |
856 ua.isOpera >= 9.2 | |
857 ); | |
858 }, | |
859 | |
860 // the spec isn't settled and nothing currently supports it | |
861 websocket: returnFalse, | |
862 | |
863 'css-anim': function() { | |
864 // pretty much limited to WebKit's special transition and animation | |
865 // properties. Need to figure out a better way to triangulate this as | |
866 // FF3.x adds more of these properties in parallel. | |
867 return ua.isWebKit > 500; | |
868 }, | |
869 | |
870 // "working" video/audio tag? | |
871 video: function() { | |
872 if (typeof cachedHasVideo != undefStr) { | |
873 return cachedHasVideo; | |
874 } | |
875 | |
876 // We haven't figured it out yet, so probe the <video> tag and cache the | |
877 // result. | |
878 var video = document.createElement('video'); | |
879 return cachedHasVideo = (typeof video['play'] != undefStr); | |
880 }, | |
881 | |
882 audio: function() { | |
883 if (typeof cachedHasAudio != undefStr) { | |
884 return cachedHasAudio; | |
885 } | |
886 | |
887 var audio = document.createElement('audio'); | |
888 return cachedHasAudio = (typeof audio['play'] != undefStr); | |
889 }, | |
890 | |
891 'video-theora': function() { | |
892 return contentTests.video() && (ua.isChrome || ua.isFF > 3); | |
893 }, | |
894 | |
895 'video-h264': function() { | |
896 return contentTests.video() && (ua.isChrome || ua.isSafari >= 4); | |
897 }, | |
898 | |
899 'audio-vorbis': function() { | |
900 return contentTests.audio() && (ua.isChrome || ua.isFF > 3); | |
901 }, | |
902 | |
903 'audio-mp3': function() { | |
904 return contentTests.audio() && (ua.isChrome || ua.isSafari >= 4); | |
905 }, | |
906 | |
907 // can we implement RPC over available primitives? | |
908 rpc: function() { | |
909 // on IE we need the src to be on the same domain or we need postMessage | |
910 // to work. Since we can't count on the src being same-domain, we look | |
911 // for things that have postMessage. We may re-visit this later and add | |
912 // same-domain checking and cross-window-call-as-postMessage-replacement | |
913 // code. | |
914 | |
915 // use "!!" to avoid null-is-an-object weirdness | |
916 return !!window['postMessage']; | |
917 }, | |
918 | |
919 sql: function() { | |
920 // HTML 5 databases | |
921 return !!window['openDatabase']; | |
922 }, | |
923 | |
924 storage: function(){ | |
925 // DOM storage | |
926 | |
927 // IE8, Safari, etc. support "localStorage", FF supported "globalStorage" | |
928 return !!window['globalStorage'] || !!window['localStorage']; | |
929 } | |
930 }; | |
931 | |
932 // isIE, isFF, isWebKit, etc. | |
933 forEach([ | |
934 'isOpera', 'isWebKit', 'isChrome', 'isKhtml', 'isSafari', | |
935 'isGecko', 'isFF', 'isIE' | |
936 ], | |
937 function(name) { | |
938 contentTests[name] = function() { | |
939 return !!ua[name]; | |
940 }; | |
941 } | |
942 ); | |
943 | |
944 /** | |
945 * Checks the list of requirements to determine if the current host browser | |
946 * meets them natively. Primarialy relies on the contentTests array. | |
947 * @param {Array} reqs A list of tests, either names of test functions in | |
948 * contentTests or functions to execute. | |
949 * @return {boolean} | |
950 * @private | |
951 */ | |
952 var testRequirements = function(reqs) { | |
953 // never use CF on Chrome or Safari | |
954 if (ua.isChrome || ua.isSafari) { | |
955 return true; | |
956 } | |
957 | |
958 var allMatch = true; | |
959 if (!reqs) { | |
960 return false; | |
961 } | |
962 forEach(reqs, function(i) { | |
963 var matches = false; | |
964 if (isFunction(i)) { | |
965 // support custom test functions | |
966 matches = i(); | |
967 } else { | |
968 // else it's a lookup by name | |
969 matches = (!!contentTests[i] && contentTests[i]()); | |
970 } | |
971 allMatch = allMatch && matches; | |
972 }); | |
973 return allMatch; | |
974 }; | |
975 | |
976 var cachedAvailable; | |
977 | |
978 /** | |
979 * Checks to find out if ChromeFrame is available as a plugin | |
980 * @return {Boolean} | |
981 * @private | |
982 */ | |
983 var isCfAvailable = function() { | |
984 if (typeof cachedAvailable != undefStr) { | |
985 return cachedAvailable; | |
986 } | |
987 | |
988 cachedAvailable = false; | |
989 var p = n.plugins; | |
990 if (typeof window['ActiveXObject'] != undefStr) { | |
991 try { | |
992 var i = new ActiveXObject('ChromeTab.ChromeFrame'); | |
993 if (i) { | |
994 cachedAvailable = true; | |
995 } | |
996 } catch(e) { | |
997 log('ChromeFrame not available, error:', e.message); | |
998 // squelch | |
999 } | |
1000 } else { | |
1001 for (var x = 0; x < p.length; x++) { | |
1002 if (p[x].name.indexOf('Google Chrome Frame') == 0) { | |
1003 cachedAvailable = true; | |
1004 break; | |
1005 } | |
1006 } | |
1007 } | |
1008 return cachedAvailable; | |
1009 }; | |
1010 | |
1011 /** | |
1012 * Creates a <param> element with the specified name and value. If a parent | |
1013 * is provided, the <param> element is appended to it. | |
1014 * @param {string} name The name of the param | |
1015 * @param {string} value The value | |
1016 * @param {Node} parent Optional parent element | |
1017 * @return {Boolean} | |
1018 * @private | |
1019 */ | |
1020 var param = function(name, value, parent) { | |
1021 var p = document.createElement('param'); | |
1022 p.setAttribute('name', name); | |
1023 p.setAttribute('value', value); | |
1024 if (parent) { | |
1025 parent.appendChild(p); | |
1026 } | |
1027 return p; | |
1028 }; | |
1029 | |
1030 /** @type {boolean} */ | |
1031 var cfStyleTagInjected = false; | |
1032 | |
1033 /** | |
1034 * Creates a style sheet in the document which provides default styling for | |
1035 * ChromeFrame instances. Successive calls should have no additive effect. | |
1036 * @private | |
1037 */ | |
1038 var injectCFStyleTag = function() { | |
1039 if (cfStyleTagInjected) { | |
1040 // once and only once | |
1041 return; | |
1042 } | |
1043 try { | |
1044 var rule = ['.chromeFrameDefaultStyle {', | |
1045 'width: 400px;', | |
1046 'height: 300px;', | |
1047 'padding: 0;', | |
1048 'margin: 0;', | |
1049 '}'].join(''); | |
1050 var ss = document.createElement('style'); | |
1051 ss.setAttribute('type', 'text/css'); | |
1052 if (ss.styleSheet) { | |
1053 ss.styleSheet.cssText = rule; | |
1054 } else { | |
1055 ss.appendChild(document.createTextNode(rule)); | |
1056 } | |
1057 var h = document.getElementsByTagName('head')[0]; | |
1058 if (h.firstChild) { | |
1059 h.insertBefore(ss, h.firstChild); | |
1060 } else { | |
1061 h.appendChild(ss); | |
1062 } | |
1063 cfStyleTagInjected = true; | |
1064 } catch (e) { | |
1065 // squelch | |
1066 | |
1067 // FIXME(slightlyoff): log? retry? | |
1068 } | |
1069 }; | |
1070 | |
1071 /** | |
1072 * Plucks properties from the passed arguments and sets them on the passed | |
1073 * DOM node | |
1074 * @param {Node} node The node to set properties on | |
1075 * @param {Object} args A map of user-specified properties to set | |
1076 * @private | |
1077 */ | |
1078 var setProperties = function(node, args) { | |
1079 injectCFStyleTag(); | |
1080 | |
1081 var srcNode = byId(args['node']); | |
1082 | |
1083 node.id = args['id'] || (srcNode ? srcNode['id'] || getUid(srcNode) : ''); | |
1084 | |
1085 // TODO(slightlyoff): Opera compat? need to test there | |
1086 var cssText = args['cssText'] || ''; | |
1087 node.style.cssText = ' ' + cssText; | |
1088 | |
1089 var classText = args['className'] || ''; | |
1090 node.className = 'chromeFrameDefaultStyle ' + classText; | |
1091 | |
1092 // default if the browser doesn't so we don't show sad-tab | |
1093 var src = args['src'] || 'about:blank'; | |
1094 | |
1095 if (ua.isIE || ua.isOpera) { | |
1096 node.src = src; | |
1097 } else { | |
1098 // crazyness regarding when things are set in NPAPI | |
1099 node.setAttribute('src', src); | |
1100 } | |
1101 | |
1102 if (srcNode) { | |
1103 srcNode.parentNode.replaceChild(node, srcNode); | |
1104 } | |
1105 }; | |
1106 | |
1107 /** | |
1108 * Creates a plugin instance, taking named parameters from the passed args. | |
1109 * @param {Object} args A bag of configuration properties, including values | |
1110 * like 'node', 'cssText', 'className', 'id', 'src', etc. | |
1111 * @return {Node} | |
1112 * @private | |
1113 */ | |
1114 var makeCFPlugin = function(args) { | |
1115 var el; // the element | |
1116 if (!ua.isIE) { | |
1117 el = document.createElement('object'); | |
1118 el.setAttribute("type", "application/chromeframe"); | |
1119 } else { | |
1120 var dummy = document.createElement('span'); | |
1121 dummy.innerHTML = [ | |
1122 '<object codeBase="//www.google.com"', | |
1123 "type='application/chromeframe'", | |
1124 'classid="CLSID:E0A900DF-9611-4446-86BD-4B1D47E7DB2A"></object>' | |
1125 ].join(' '); | |
1126 el = dummy.firstChild; | |
1127 } | |
1128 setProperties(el, args); | |
1129 return el; | |
1130 }; | |
1131 | |
1132 /** | |
1133 * Creates an iframe in lieu of a ChromeFrame plugin, taking named parameters | |
1134 * from the passed args. | |
1135 * @param {Object} args A bag of configuration properties, including values | |
1136 * like 'node', 'cssText', 'className', 'id', 'src', etc. | |
1137 * @return {Node} | |
1138 * @private | |
1139 */ | |
1140 var makeCFIframe = function(args) { | |
1141 var el = document.createElement('iframe'); | |
1142 setProperties(el, args); | |
1143 // FIXME(slightlyoff): | |
1144 // This is where we'll need to slot in "don't fire load events for | |
1145 // fallback URL" logic. | |
1146 listen(el, 'load', hitch(el, '_dispatch', 'load')); | |
1147 return el; | |
1148 }; | |
1149 | |
1150 | |
1151 var msgPrefix = 'CFInstance.rpc:'; | |
1152 | |
1153 /** | |
1154 * A class that provides the ability for widget-mode hosted content to more | |
1155 * easily call hosting-page exposed APIs (and vice versa). It builds upon the | |
1156 * message-passing nature of ChromeFrame to route messages to the other | |
1157 * side's RPC host and coordinate events such as 'RPC readyness', buffering | |
1158 * calls until both sides indicate they are ready to participate. | |
1159 * @constructor | |
1160 * @public | |
1161 */ | |
1162 var RPC = function(instance) { | |
1163 this.initDeferred = new Deferred(); | |
1164 | |
1165 this.instance = instance; | |
1166 | |
1167 instance.listen('message', hitch(this, '_handleMessage')); | |
1168 | |
1169 this._localExposed = {}; | |
1170 this._doWithAckCallbacks = {}; | |
1171 | |
1172 this._open = false; | |
1173 this._msgBacklog = []; | |
1174 | |
1175 this._initialized = false; | |
1176 this._exposeMsgBacklog = []; | |
1177 | |
1178 this._exposed = false; | |
1179 this._callRemoteMsgBacklog = []; | |
1180 | |
1181 this._inFlight = {}; | |
1182 | |
1183 var sendLoadMsg = hitch(this, function(evt) { | |
1184 this.doWithAck('load').addCallback(this, function() { | |
1185 this._open = true; | |
1186 this._postMessageBacklog(); | |
1187 }); | |
1188 }); | |
1189 | |
1190 if (instance['tagName']) { | |
1191 instance.listen('load', sendLoadMsg); | |
1192 } else { | |
1193 sendLoadMsg(); | |
1194 } | |
1195 }; | |
1196 | |
1197 RPC.prototype._postMessageBacklog = function() { | |
1198 if (this._open) { | |
1199 // forEach(this._msgBacklog, this._postMessage, this); | |
1200 // this._msgBacklog = []; | |
1201 while (this._msgBacklog.length) { | |
1202 var msg = this._msgBacklog.shift(); | |
1203 this._postMessage(msg); | |
1204 } | |
1205 } | |
1206 }; | |
1207 | |
1208 RPC.prototype._postMessage = function(msg, force) { | |
1209 if (!force && !this._open) { | |
1210 this._msgBacklog.push(msg); | |
1211 } else { | |
1212 // FIXME(slightlyoff): need to check domains list here! | |
1213 this.instance.postMessage(msgPrefix + msg, '*'); | |
1214 } | |
1215 }; | |
1216 | |
1217 RPC.prototype._doWithAck = function(what) { | |
1218 this._postMessage('doWithAckCallback:' + what, what == 'load'); | |
1219 }; | |
1220 | |
1221 RPC.prototype.doWithAck = function(what) { | |
1222 var d = new Deferred(); | |
1223 this._doWithAckCallbacks[what] = d; | |
1224 this._postMessage('doWithAck:' + what, what == 'load'); | |
1225 return d; | |
1226 }; | |
1227 | |
1228 RPC.prototype._handleMessage = function(evt, stopper) { | |
1229 var d = String(evt.data); | |
1230 | |
1231 if (d.indexOf(msgPrefix) != 0) { | |
1232 // not for us, allow the event dispatch to continue... | |
1233 return; | |
1234 } | |
1235 | |
1236 // ...else we're the end of the line for this event | |
1237 stopper(); | |
1238 | |
1239 // see if we know what type of message it is | |
1240 d = d.substr(msgPrefix.length); | |
1241 | |
1242 var cIndex = d.indexOf(':'); | |
1243 | |
1244 var type = d.substr(0, cIndex); | |
1245 | |
1246 if (type == 'doWithAck') { | |
1247 this._doWithAck(d.substr(cIndex + 1)); | |
1248 return; | |
1249 } | |
1250 | |
1251 var msgBody = d.substr(cIndex + 1); | |
1252 | |
1253 if (type == 'doWithAckCallback') { | |
1254 this._doWithAckCallbacks[msgBody].callback(1); | |
1255 return; | |
1256 } | |
1257 | |
1258 if (type == 'init') { | |
1259 return; | |
1260 } | |
1261 | |
1262 // All the other stuff we can do uses a JSON payload. | |
1263 var obj = fromJson(msgBody); | |
1264 | |
1265 if (type == 'callRemote') { | |
1266 | |
1267 if (obj.method && obj.params && obj.id) { | |
1268 | |
1269 var ret = { | |
1270 success: 0, | |
1271 returnId: obj.id | |
1272 }; | |
1273 | |
1274 try { | |
1275 // Undefined isn't valid JSON, so use null as default value. | |
1276 ret.value = this._localExposed[ obj.method ](evt, obj) || null; | |
1277 ret.success = 1; | |
1278 } catch(e) { | |
1279 ret.error = e.message; | |
1280 } | |
1281 | |
1282 this._postMessage('callReturn:' + toJson(ret)); | |
1283 } | |
1284 } | |
1285 | |
1286 if (type == 'callReturn') { | |
1287 // see if we're waiting on an outstanding RPC call, which | |
1288 // would be identified by returnId. | |
1289 var rid = obj['returnId']; | |
1290 if (!rid) { | |
1291 // throw an error? | |
1292 return; | |
1293 } | |
1294 var callWrap = this._inFlight[rid]; | |
1295 if (!callWrap) { | |
1296 return; | |
1297 } | |
1298 | |
1299 if (obj.success) { | |
1300 callWrap.d.callback(obj['value'] || 1); | |
1301 } else { | |
1302 callWrap.d.errback(new Error(obj['error'] || 'unspecified RPC error')); | |
1303 } | |
1304 delete this._inFlight[rid]; | |
1305 } | |
1306 | |
1307 }; | |
1308 | |
1309 /** | |
1310 * Makes a method visible to be called | |
1311 * @param {string} name The name to expose the method at. | |
1312 * @param {Function|string} method The function (or name of the function) to | |
1313 * expose. If a name is provided, it's looked up from the passed scope. | |
1314 * If no scope is provided, the global scope is queried for a function | |
1315 * with that name. | |
1316 * @param {Object} scope Optional A scope to bind the passed method to. If | |
1317 * the method parameter is specified by a string, the method is both | |
1318 * located on the passed scope and bound to it. | |
1319 * @param {Array} domains Optional A list of domains in | |
1320 * 'http://example.com:8080' format which may call the given method. | |
1321 * Currently un-implemented. | |
1322 * @public | |
1323 */ | |
1324 RPC.prototype.expose = function(name, method, scope, domains) { | |
1325 scope = scope || window; | |
1326 method = isString(method) ? scope[method] : method; | |
1327 | |
1328 // local call proxy that the other side will hit when calling our method | |
1329 this._localExposed[name] = function(evt, obj) { | |
1330 return method.apply(scope, obj.params); | |
1331 }; | |
1332 | |
1333 if (!this._initialized) { | |
1334 this._exposeMsgBacklog.push(arguments); | |
1335 return; | |
1336 } | |
1337 | |
1338 var a = [name, method, scope, domains]; | |
1339 this._sendExpose.apply(this, a); | |
1340 }; | |
1341 | |
1342 RPC.prototype._sendExpose = function(name) { | |
1343 // now tell the other side that we're advertising this method | |
1344 this._postMessage('expose:' + toJson({ name: name })); | |
1345 }; | |
1346 | |
1347 | |
1348 /** | |
1349 * Calls a remote method asynchronously and returns a Deferred object | |
1350 * representing the response. | |
1351 * @param {string} method Name of the method to call. Should be the same name | |
1352 * which the other side has expose()'d. | |
1353 * @param {Array} params Optional A list of arguments to pass to the called | |
1354 * method. All elements in the list must be cleanly serializable to | |
1355 * JSON. | |
1356 * @param {CFInstance.Deferred} deferred Optional A Deferred to use for | |
1357 * reporting the response of the call. If no deferred is passed, a new | |
1358 * Deferred is created and returned. | |
1359 * @return {CFInstance.Deferred} | |
1360 * @public | |
1361 */ | |
1362 RPC.prototype.callRemote = function(method, params, timeout, deferred) { | |
1363 var d = deferred || new Deferred(null, timeout || -1); | |
1364 | |
1365 if (!this._exposed) { | |
1366 var args = toArray(arguments); | |
1367 args.length = 3; | |
1368 args.push(d); | |
1369 this._callRemoteMsgBacklog.push(args); | |
1370 return d; | |
1371 } | |
1372 | |
1373 | |
1374 if (!method) { | |
1375 d.errback('no method provided!'); | |
1376 return d; | |
1377 } | |
1378 | |
1379 var id = msgPrefix + (_counter++); | |
1380 | |
1381 // JSON-ify the whole bundle | |
1382 var callWrapper = { | |
1383 method: String(method), | |
1384 params: params || [], | |
1385 id: id | |
1386 }; | |
1387 var callJson = toJson(callWrapper); | |
1388 callWrapper.d = d; | |
1389 this._inFlight[id] = callWrapper; | |
1390 this._postMessage('callRemote:' + callJson); | |
1391 return d; | |
1392 }; | |
1393 | |
1394 | |
1395 /** | |
1396 * Tells the other side of the connection that we're ready to start receiving | |
1397 * calls. Returns a Deferred that is called back when both sides have | |
1398 * initialized and any backlogged messages have been sent. RPC users should | |
1399 * generally work to make sure that they call expose() on all of the methods | |
1400 * they'd like to make available to the other side *before* calling init() | |
1401 * @return {CFInstance.Deferred} | |
1402 * @public | |
1403 */ | |
1404 RPC.prototype.init = function() { | |
1405 var d = this.initDeferred; | |
1406 this.doWithAck('init').addCallback(this, function() { | |
1407 // once the init floodgates are open, send our backlogs one at a time, | |
1408 // with a little lag in the middle to prevent ordering problems | |
1409 | |
1410 this._initialized = true; | |
1411 while (this._exposeMsgBacklog.length) { | |
1412 this.expose.apply(this, this._exposeMsgBacklog.shift()); | |
1413 } | |
1414 | |
1415 setTimeout(hitch(this, function(){ | |
1416 | |
1417 this._exposed = true; | |
1418 while (this._callRemoteMsgBacklog.length) { | |
1419 this.callRemote.apply(this, this._callRemoteMsgBacklog.shift()); | |
1420 } | |
1421 | |
1422 d.callback(1); | |
1423 | |
1424 }), 30); | |
1425 | |
1426 }); | |
1427 return d; | |
1428 }; | |
1429 | |
1430 // CFInstance design notes: | |
1431 // | |
1432 // The CFInstance constructor is only ever used in host environments. In | |
1433 // content pages (things hosted by a ChromeFrame instance), CFInstance | |
1434 // acts as a singleton which provides services like RPC for communicating | |
1435 // with it's mirror-image object in the hosting environment. We want the | |
1436 // same methods and properties to be available on *instances* of | |
1437 // CFInstance objects in the host env as on the singleton in the hosted | |
1438 // content, despite divergent implementation. | |
1439 // | |
1440 // Further complicating things, CFInstance may specialize behavior | |
1441 // internally based on whether or not it is communicationg with a fallback | |
1442 // iframe or a 'real' ChromeFrame instance. | |
1443 | |
1444 var CFInstance; // forward declaration | |
1445 var h = window['externalHost']; | |
1446 var inIframe = (window.parent != window); | |
1447 | |
1448 if (inIframe) { | |
1449 h = window.parent; | |
1450 } | |
1451 | |
1452 var normalizeTarget = function(targetOrigin) { | |
1453 var l = window.location; | |
1454 if (!targetOrigin) { | |
1455 if (l.protocol != 'file:') { | |
1456 targetOrigin = l.protocol + '//' + l.host + "/"; | |
1457 } else { | |
1458 // TODO(slightlyoff): | |
1459 // is this secure enough? Is there another way to get messages | |
1460 // flowing reliably across file-hosted documents? | |
1461 targetOrigin = '*'; | |
1462 } | |
1463 } | |
1464 return targetOrigin; | |
1465 }; | |
1466 | |
1467 var postMessageToDest = function(dest, msg, targetOrigin) { | |
1468 return dest.postMessage(msg, normalizeTarget(targetOrigin)); | |
1469 }; | |
1470 | |
1471 if (h) { | |
1472 // | |
1473 // We're loaded inside a ChromeFrame widget (or something that should look | |
1474 // like we were). | |
1475 // | |
1476 | |
1477 CFInstance = {}; | |
1478 | |
1479 installEvtSys(CFInstance); | |
1480 | |
1481 // FIXME(slightlyoff): | |
1482 // passing a target origin to externalHost's postMessage seems b0rked | |
1483 // right now, so pass null instead. Will re-enable hitch()'d variant | |
1484 // once that's fixed. | |
1485 | |
1486 // CFInstance.postMessage = hitch(null, postMessageToDest, h); | |
1487 | |
1488 CFInstance.postMessage = function(msg, targetOrigin) { | |
1489 return h.postMessage(msg, | |
1490 (inIframe ? normalizeTarget(targetOrigin) : null) ); | |
1491 }; | |
1492 | |
1493 // Attach to the externalHost's onmessage to proxy it in to CFInstance's | |
1494 // onmessage. | |
1495 var dispatchMsg = function(evt) { | |
1496 try { | |
1497 CFInstance._dispatch('message', evt); | |
1498 } catch(e) { | |
1499 log(e); | |
1500 // squelch | |
1501 } | |
1502 }; | |
1503 if (inIframe) { | |
1504 listen(window, 'message', dispatchMsg); | |
1505 } else { | |
1506 h.onmessage = dispatchMsg; | |
1507 } | |
1508 | |
1509 CFInstance.rpc = new RPC(CFInstance); | |
1510 | |
1511 _loadedListenerList.push(function(evt) { | |
1512 CFInstance._dispatch('load', evt); | |
1513 }); | |
1514 | |
1515 } else { | |
1516 // | |
1517 // We're the host document. | |
1518 // | |
1519 | |
1520 var installProperties = function(instance, args) { | |
1521 var s = instance.supportedEvents = ['load', 'message']; | |
1522 instance._msgPrefix = 'CFMessage:'; | |
1523 | |
1524 installEvtSys(instance); | |
1525 | |
1526 instance.log = log; | |
1527 | |
1528 // set up an RPC instance | |
1529 instance.rpc = new RPC(instance); | |
1530 | |
1531 forEach(s, function(evt) { | |
1532 var l = args['on' + evt]; | |
1533 if (l) { | |
1534 instance.listen(evt, l); | |
1535 } | |
1536 }); | |
1537 | |
1538 var contentWindow = instance.contentWindow; | |
1539 | |
1540 // if it doesn't have a postMessage, route to/from the iframe's built-in | |
1541 if (typeof instance['postMessage'] == undefStr && !!contentWindow) { | |
1542 | |
1543 instance.postMessage = hitch(null, postMessageToDest, contentWindow); | |
1544 | |
1545 listen(window, 'message', function(evt) { | |
1546 if (evt.source == contentWindow) { | |
1547 instance._dispatch('message', evt); | |
1548 } | |
1549 }); | |
1550 } | |
1551 | |
1552 return instance; | |
1553 }; | |
1554 | |
1555 /** | |
1556 * A class whose instances correspond to ChromeFrame instances. Passing an | |
1557 * arguments object to CFInstance helps parameterize the instance. | |
1558 * @constructor | |
1559 * @public | |
1560 */ | |
1561 CFInstance = function(args) { | |
1562 args = args || {}; | |
1563 var instance; | |
1564 var success = false; | |
1565 | |
1566 // If we've been passed a CFInstance object as our source node, just | |
1567 // re-use it. | |
1568 if (args['node']) { | |
1569 var n = byId(args['node']); | |
1570 // Look for CF-specific properties. | |
1571 if (n && n.tagName == 'OBJECT' && n.success && n.rpc) { | |
1572 // Navigate, set styles, etc. | |
1573 setProperties(n, args); | |
1574 return n; | |
1575 } | |
1576 } | |
1577 | |
1578 var force = !!args['forcePlugin']; | |
1579 | |
1580 if (!force && testRequirements(args['requirements'])) { | |
1581 instance = makeCFIframe(args); | |
1582 success = true; | |
1583 } else if (isCfAvailable()) { | |
1584 instance = makeCFPlugin(args); | |
1585 success = true; | |
1586 } else { | |
1587 // else create an iframe but load the failure content and | |
1588 // not the 'normal' content | |
1589 | |
1590 // grab the fallback URL, and if none, use the 'click here | |
1591 // to install ChromeFrame' URL. Note that we only support | |
1592 // polling for install success if we're using the default | |
1593 // URL | |
1594 | |
1595 var fallback = '//www.google.com/chromeframe'; | |
1596 | |
1597 args.src = args['fallback'] || fallback; | |
1598 instance = makeCFIframe(args); | |
1599 | |
1600 if (args.src == fallback) { | |
1601 // begin polling for install success. | |
1602 | |
1603 // TODO(slightlyoff): need to prevent firing of onload hooks! | |
1604 // TODO(slightlyoff): implement polling | |
1605 // TODO(slightlyoff): replacement callback? | |
1606 // TODO(slightlyoff): add flag to disable this behavior | |
1607 } | |
1608 } | |
1609 instance.success = success; | |
1610 | |
1611 installProperties(instance, args); | |
1612 | |
1613 return instance; | |
1614 }; | |
1615 | |
1616 // compatibility shims for development-time. These mirror the methods that | |
1617 // are created on the CFInstance singleton if we detect that we're running | |
1618 // inside of CF. | |
1619 if (!CFInstance['postMessage']) { | |
1620 CFInstance.postMessage = function() { | |
1621 var args = toArray(arguments); | |
1622 args.unshift('CFInstance.postMessage:'); | |
1623 log.apply(null, args); | |
1624 }; | |
1625 CFInstance.listen = function() { | |
1626 // this space intentionally left blank | |
1627 }; | |
1628 } | |
1629 } | |
1630 | |
1631 // expose some properties | |
1632 CFInstance.ua = ua; | |
1633 CFInstance._documentLoaded = documentLoaded; | |
1634 CFInstance.contentTests = contentTests; | |
1635 CFInstance.isAvailable = function(requirements) { | |
1636 var hasCf = isCfAvailable(); | |
1637 return requirements ? (hasCf || testRequirements(requirements)) : hasCf; | |
1638 | |
1639 }; | |
1640 CFInstance.Deferred = Deferred; | |
1641 CFInstance.toJson = toJson; | |
1642 CFInstance.fromJson = fromJson; | |
1643 CFInstance.log = log; | |
1644 | |
1645 // expose CFInstance to the external scope. We've already checked to make | |
1646 // sure we're not going to blow existing objects away. | |
1647 scope.CFInstance = CFInstance; | |
1648 | |
1649 })( this['ChromeFrameScope'] || this ); | |
1650 | |
1651 // vim: shiftwidth=2:et:ai:tabstop=2 | |
OLD | NEW |