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