| 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 |