1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * This class is a collection of functions for defining packages and 26 * loading them dynamically. 27 * 28 * @author Andy Clark 29 * 30 * @private 31 */ 32 AjxPackage = function() {} 33 34 // 35 // Constants 36 // 37 38 /** 39 * Defines the "XHR SYNC" method. 40 */ 41 AjxPackage.METHOD_XHR_SYNC = "xhr-sync"; 42 /** 43 * Defines the "XHR ASYNC" method. 44 */ 45 AjxPackage.METHOD_XHR_ASYNC = "xhr-async"; 46 /** 47 * Defines the "SCRIPT TAG" method. 48 */ 49 AjxPackage.METHOD_SCRIPT_TAG = "script-tag"; 50 51 AjxPackage.DEFAULT_SYNC = AjxPackage.METHOD_XHR_SYNC; 52 AjxPackage.DEFAULT_ASYNC = AjxEnv.isIE ? AjxPackage.METHOD_XHR_ASYNC : AjxPackage.METHOD_SCRIPT_TAG; 53 54 // 55 // Data 56 // 57 58 AjxPackage._packages = {}; 59 AjxPackage._extension = ".js"; 60 61 AjxPackage.__depth = 0; 62 AjxPackage.__scripts = []; 63 AjxPackage.__data = {}; 64 65 // 66 // Static functions 67 // 68 69 /** 70 * Sets the base path. 71 * 72 * @param {string} basePath the base path 73 */ 74 AjxPackage.setBasePath = function(basePath) { 75 AjxPackage._basePath = basePath; 76 }; 77 /** 78 * Sets the extension. 79 * 80 * @param {string} extension the extension 81 */ 82 AjxPackage.setExtension = function(extension) { 83 AjxPackage._extension = extension; 84 }; 85 /** 86 * Sets the query string. 87 * 88 * @param {string} queryString the query string 89 */ 90 AjxPackage.setQueryString = function(queryString) { 91 AjxPackage._queryString = queryString; 92 }; 93 94 /** 95 * Checks if the specified package has been defined. 96 * 97 * @param {string} name the package name 98 * @return {boolean} <code>true</code> if the package is defined 99 */ 100 AjxPackage.isDefined = function(name) { 101 return Boolean(AjxPackage._packages[name]); 102 }; 103 104 /** 105 * Defines a package and returns true if this is the first definition. 106 * 107 * @param {string} name the package name 108 * @return {boolean} <code>true</code> if this is the first package definition 109 */ 110 AjxPackage.define = function(name) { 111 AjxPackage.__log("DEFINE "+name, "font-weight:bold;font-style:italic"); 112 name = AjxPackage.__package2path(name); 113 if (!AjxPackage._packages[name]) { 114 AjxPackage._packages[name] = true; 115 return true; 116 } 117 return false; 118 }; 119 120 /** 121 * Undefines a package. 122 * 123 * @param {string} name the package name 124 */ 125 AjxPackage.undefine = function(name) { 126 AjxPackage.__log("UNDEFINE "+name, "font-weight:bold;font-style:italic"); 127 name = AjxPackage.__package2path(name); 128 if (AjxPackage._packages[name]) { 129 delete AjxPackage._packages[name]; 130 } 131 }; 132 133 /** 134 * This function ensures that the specified module is loaded and available 135 * for use. If already loaded, this function returns immediately. If not, 136 * then this function will load the necessary code, either synchronously 137 * or asynchronously depending on whether the <tt>callback</tt> or 138 * <tt>forceSync</tt> parameters are specified. 139 * <p> 140 * It can be called with either a package name string or a parameters object. 141 * 142 * @param {hash} nameOrParams a hash of parameters 143 * @param {string} name the package name 144 * @param {string} [basePath] the base path of URL to load. If 145 * not specified, uses the global base path. 146 * @param {string} [extension] the filename extension of URL to 147 * load. If not specified, uses the global 148 * filename extension. 149 * @param {string} [queryString] the query string appended to URL. 150 * If not specified, uses the global query 151 * string. 152 * @param {string} [userName] The username of the request 153 * @param {string} [password] The password of the request 154 * @param {AjxCallback} [callback] the callback to run 155 * @param {constant} [method] the loading method for the package (see <code>METHOD_*</code> constants) 156 * @param {boolean} [forceSync] overrides the load mode (if 157 * this method is called during an async 158 * load) and forces the requested package to 159 * be loaded synchronously. 160 * @param {boolean} [forceReload=false] specifies whether the package is reloaded even if already defined 161 */ 162 AjxPackage.require = function(nameOrParams) { 163 var params = nameOrParams; 164 if (typeof nameOrParams == "string") { 165 params = { name: nameOrParams }; 166 } 167 168 // is an array of names specified? 169 var array = params.name; 170 if (array instanceof Array) { 171 // NOTE: This is to avoid a silent problem: when the caller expects 172 // the array of names to be left unchanged upon return. Because 173 // we call <code>shift</code> on the array, it modifies the 174 // original list so the caller would see an empty array after 175 // calling this function. 176 if (!array.internal) { 177 array = [].concat(array); 178 array.internal = true; 179 params.name = array; 180 } 181 182 var name = array.shift(); 183 184 // if more names, use callback to trigger next 185 if (array.length > 0) { 186 var ctor = new Function(); 187 ctor.prototype = params; 188 ctor.prototype.constructor = ctor; 189 190 var nparams = new ctor(); 191 nparams.name = name; 192 nparams.callback = new AjxCallback(null, AjxPackage.__requireNext, params); 193 194 AjxPackage.require(nparams); 195 return; 196 } 197 198 // continue 199 params.name = name; 200 } 201 202 // see if it's already loaded 203 var oname = params.name; 204 var name = AjxPackage.__package2path(oname); 205 206 var callback = params.callback; 207 if (typeof callback == "function") { 208 callback = new AjxCallback(callback); 209 } 210 var cb = callback ? " (callback)" : ""; 211 var loaded = AjxPackage._packages[name] ? " LOADED" : ""; 212 var mode = AjxPackage.__scripts.length ? " (async, queueing...)" : ""; 213 AjxPackage.__log(["REQUIRE \"",oname,"\"",cb,loaded,mode].join("")); 214 215 var reload = params.forceReload != null ? params.forceReload : false; 216 if (AjxPackage._packages[name] && !reload) { 217 if (callback) { 218 callback.run(); 219 } 220 return; 221 } 222 223 // assemble load url 224 var basePath = params.basePath || AjxPackage._basePath || window.contextPath; 225 var extension = params.extension || AjxPackage._extension; 226 var queryString = params.queryString || AjxPackage._queryString; 227 228 var pathParts = [basePath, "/", name, extension]; 229 if (queryString) { 230 pathParts.push("?",queryString); 231 } 232 var path = pathParts.join(""); 233 234 // load 235 var method = params.method || (params.callback ? AjxPackage.DEFAULT_ASYNC : AjxPackage.DEFAULT_SYNC); 236 237 var isSync = method == AjxPackage.METHOD_XHR_SYNC || params.forceSync; 238 var isAsync = !isSync; 239 240 var data = { 241 name: name, 242 path: path, 243 method: method, 244 async: isAsync, 245 callback: callback || AjxCallback.NOP, 246 scripts: isAsync ? [] : null 247 }; 248 249 if (isSync || AjxPackage.__scripts.length == 0) { 250 AjxPackage.__doLoad(data); 251 } 252 else { 253 var current = AjxPackage.__scripts[AjxPackage.__scripts.length - 1]; 254 data.method = current.method; 255 data.async = current.async; 256 data.scripts = []; 257 if (callback) { 258 // NOTE: This code is here to protect against interleaved async 259 // requests. If a second async request is made before the 260 // the first one is completely processed, the second request 261 // is added to the first request's stack and is processed 262 // as normal. This prevents the second request's callback 263 // from being called. Therefore, we chain the new callback 264 // to the original callback to ensure that they both get 265 // called. 266 var top = AjxPackage.__scripts[0]; 267 top.callback = new AjxCallback(AjxPackage.__chainCallbacks, [top.callback, callback]); 268 data.callback = AjxCallback.NOP; 269 } 270 current.scripts.push(data); 271 } 272 }; 273 274 AjxPackage.eval = function(text) { 275 // eval in global scope (IE) 276 if (window.execScript) { 277 // NOTE: for IE 278 window.execScript(text); 279 } 280 // eval in global scope (FF, Opera, WebKit) 281 else if (AjxEnv.indirectEvalIsGlobal) { 282 var evl=window.eval; 283 evl(text); 284 } 285 // insert script tag into head 286 // Note: if any scripts are still loading, this will not run immediately! 287 else { 288 var e = document.createElement("SCRIPT"); 289 var t = document.createTextNode(text); 290 e.appendChild(t); 291 292 var heads = document.getElementsByTagName("HEAD"); 293 if (heads.length == 0) { 294 // NOTE: Safari doesn't automatically insert <head> 295 heads = [ document.createElement("HEAD") ]; 296 document.documentElement.appendChild(heads[0]); 297 } 298 heads[0].appendChild(e); 299 } 300 }; 301 302 // 303 // Private functions 304 // 305 306 AjxPackage.__package2path = function(name) { 307 return name.replace(/\./g, "/").replace(/\*$/, "__all__"); 308 }; 309 310 AjxPackage.__requireNext = function(params) { 311 // NOTE: Both FF and IE won't eval the next loaded code unless we 312 // first return to the UI loop. So we use a timeout to kick 313 // off the next load. 314 var func = AjxCallback.simpleClosure(AjxPackage.require, null, params); 315 setTimeout(func, AjxEnv.isIE ? 10 : 0); 316 }; 317 318 AjxPackage.__doLoad = function(data) { 319 if (data.async) { 320 AjxPackage.__doAsyncLoad(data); 321 } 322 else { 323 AjxPackage.__doXHR(data); 324 } 325 }; 326 327 AjxPackage.__doAsyncLoad = function(data, force) { 328 AjxPackage.__data[name] = data; 329 if (force || AjxPackage.__scripts.length == 0) { 330 AjxPackage.__scripts.push(data); 331 if (data.method == AjxPackage.METHOD_SCRIPT_TAG) { 332 AjxPackage.__doScriptTag(data); 333 } 334 else { 335 AjxPackage.__doXHR(data); 336 } 337 } 338 else { 339 var current = AjxPackage.__scripts[AjxPackage.__scripts.length - 1]; 340 current.scripts.push(data); 341 } 342 }; 343 344 AjxPackage.__doScriptTag = function(data) { 345 // create script element 346 var script = document.createElement("SCRIPT"); 347 script.type = "text/javascript"; 348 script.src = data.path; 349 350 // attach handler 351 if (script.attachEvent) { 352 var handler = AjxCallback.simpleClosure(AjxPackage.__onAsyncLoadIE, null, script); 353 script.attachEvent("onreadystatechange", handler); 354 } 355 else if (script.addEventListener) { 356 var handler = AjxCallback.simpleClosure(AjxPackage.__onAsyncLoad, null, data.name); 357 script.addEventListener("load", handler, true); 358 } 359 360 // insert element 361 var heads = document.getElementsByTagName("HEAD"); 362 if (!heads || heads.length == 0) { 363 // NOTE: Safari doesn't automatically insert <head> 364 heads = [ document.createElement("HEAD") ]; 365 document.documentElement.appendChild(heads[0]); 366 } 367 heads[0].appendChild(script); 368 }; 369 370 AjxPackage.__doXHR = function(data) { 371 var callback = data.async ? new AjxCallback(null, AjxPackage.__onXHR, [data]) : null; 372 var loadParams = { 373 url: data.path, 374 userName: data.userName, 375 password: data.password, 376 async: data.async, 377 callback: callback 378 }; 379 var req = AjxLoader.load(loadParams); 380 if (!data.async) { 381 AjxPackage.__onXHR(data, req); 382 } 383 }; 384 385 AjxPackage.__onXHR = function(data, req) { 386 // evaluate source 387 if (req.status == 200 || req.status == 0) { 388 AjxPackage.__requireEval(req.responseText || ""); 389 } 390 else { 391 AjxPackage.__log("error: "+req.status, "background-color:red"); 392 } 393 394 // continue 395 if (data.async) { 396 AjxPackage.__onAsyncLoad(); 397 } 398 else { 399 AjxPackage.__onLoad(data); 400 } 401 }; 402 403 AjxPackage.__onAsyncLoadIE = function(script) { 404 if (script.readyState == 'loaded') { 405 AjxPackage.__onAsyncLoad(); 406 } 407 }; 408 409 AjxPackage.__onAsyncLoad = function() { 410 var current; 411 while (current = AjxPackage.__scripts.pop()) { 412 // push next scope 413 if (current.scripts.length) { 414 // NOTE: putting the current back on the stack before adding new scope 415 AjxPackage.__scripts.push(current); 416 current = current.scripts.shift() 417 AjxPackage.__scripts.push(current); 418 AjxPackage.__doAsyncLoad(current, true); 419 return; 420 } 421 AjxPackage.__onLoad(current); 422 } 423 }; 424 425 AjxPackage.__onLoad = function(data) { 426 AjxPackage.define(data.name); 427 if (data.callback) { 428 try { 429 data.callback.run(); 430 } 431 catch (e) { 432 AjxPackage.__log("error on callback: "+e,"color:red"); 433 } 434 } 435 }; 436 437 AjxPackage.__requireEval = function(text) { 438 AjxPackage.__depth++; 439 try { 440 AjxPackage.eval(text); 441 } 442 catch (e) { 443 AjxPackage.__log("error on eval: "+e,"color:red"); 444 if (window.console && window.console.log) { 445 console.log("Error on eval : " +e); 446 } 447 } 448 AjxPackage.__depth--; 449 }; 450 451 /*** 452 AjxPackage.__win = open("about:blank", "AjxPackageLog"+(new Date().getTime())); 453 AjxPackage.__win.document.write("<h3>AjxPackage Log</h3>"); 454 455 AjxPackage.__log = function(s, style) { 456 // AjxDebug 457 // if (!window.AjxDebug) { 458 // var msgs = AjxPackage.__msgs || (AjxPackage.__msgs = []); 459 // msgs.push(s); 460 // return; 461 // } 462 // 463 // if (AjxPackage.__msgs) { 464 // AjxPackage.__DBG = new AjxDebug(AjxDebug.DBG1, "AjxPackage"); 465 // for (var i = 0; i < AjxPackage.__msgs.length; i++) { 466 // AjxPackage.__DBG.println(AjxDebug.DBG1, AjxPackage.__msgs[i]); 467 // } 468 // delete AjxPackage.__msgs; 469 // } 470 // AjxPackage.__DBG.println(AjxDebug.DBG1, s); 471 472 // new window 473 var doc = AjxPackage.__win.document; 474 var div = doc.createElement("DIV"); 475 style = ["padding-left:",AjxPackage.__depth,"em;",style||""].join(""); 476 div.setAttribute("style", style); 477 div.innerHTML = s.replace(/&/g,"&").replace(/</g,"<"); 478 doc.body.appendChild(div); 479 }; 480 /*** 481 AjxPackage.__log = function(s, style) { 482 console.log(s); 483 }; 484 /***/ 485 AjxPackage.__log = function(s, style) { 486 // NOTE: This assumes a debug window has been created and assigned 487 // to the global variable "DBG". 488 // if (window.DBG) { DBG.println(AjxDebug.DBG1, "PACKAGE: " + s); } 489 // if (window.console) { console.log(s); } 490 } 491 /***/ 492 493 AjxPackage.__alertStack = function(title) { 494 var a = []; 495 if (title) a.push(title, "\n\n"); 496 for (var i = AjxPackage.__scripts.length - 1; i >= 0; i--) { 497 var script = AjxPackage.__scripts[i]; 498 a.push(script.name," (",Boolean(script.callback),")","\n"); 499 if (script.scripts) { 500 for (var j = 0; j < script.scripts.length; j++) { 501 var subscript = script.scripts[j]; 502 a.push(" ",subscript.name," (",Boolean(subscript.callback),")","\n"); 503 } 504 } 505 } 506 alert(a.join("")); 507 }; 508 509 AjxPackage.__chainCallbacks = function(callback1, callback2) { 510 if (callback1) callback1.run(); 511 if (callback2) callback2.run(); 512 }; 513