1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2008-2018 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.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
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 
 20 var console = console || {warn : function(){}};
 21 
 22 /**
 23  * @namespace Utils static class to provide miscellaneous utility functions.
 24  */
 25 LABKEY.Utils = new function()
 26 {
 27     // Private array of chars to use for UUID generation
 28     var CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'.split('');
 29     var idSeed = 100;
 30 
 31     //When using Ext dateFields you can use DATEALTFORMATS for the altFormat: config option.
 32     var DATEALTFORMATS_Either =
 33             'j-M-y g:i a|j-M-Y g:i a|j-M-y G:i|j-M-Y G:i|' +
 34             'j-M-y|j-M-Y|' +
 35             'Y-n-d H:i:s|Y-n-d|' +
 36             'Y/n/d H:i:s|Y/n/d|' +
 37             'j M Y G:i:s O|' +  // 10 Sep 2009 11:24:12 -0700
 38             'j M Y H:i:s|c';
 39     var DATEALTFORMATS_MonthDay =
 40             'n/j/y g:i:s a|n/j/Y g:i:s a|n/j/y G:i:s|n/j/Y G:i:s|' +
 41             'n-j-y g:i:s a|n-j-Y g:i:s a|n-j-y G:i:s|n-j-Y G:i:s|' +
 42             'n/j/y g:i a|n/j/Y g:i a|n/j/y G:i|n/j/Y G:i|' +
 43             'n-j-y g:i a|n-j-Y g:i a|n-j-y G:i|n-j-Y G:i|' +
 44             'n/j/y|n/j/Y|' +
 45             'n-j-y|n-j-Y|' + DATEALTFORMATS_Either;
 46     var DATEALTFORMATS_DayMonth =
 47             'j/n/y g:i:s a|j/n/Y g:i:s a|j/n/y G:i:s|j/n/Y G:i:s|' +
 48             'j-n-y g:i:s a|j-n-Y g:i:s a|j-n-y G:i:s|j-n-Y G:i:s|' +
 49             'j/n/y g:i a|j/n/Y g:i a|j/n/y G:i|j/n/Y G:i|' +
 50             'j-n-y g:i a|j-n-Y g:i a|j-n-y G:i|j-n-Y G:i|' +
 51             'j/n/y|j/n/Y|' +
 52             'j-n-y|j-n-Y|' +
 53             'j-M-y|j-M-Y|' + DATEALTFORMATS_Either;
 54 
 55     var DATETIMEFORMAT_WithMS = 'Y-m-d H:i:s.u'; //24 hr format with milliseconds
 56 
 57     function isObject(v)
 58     {
 59         return typeof v == "object" && Object.prototype.toString.call(v) === '[object Object]';
 60     }
 61 
 62 
 63     function _copy(o, depth)
 64     {
 65         if (depth==0 || !isObject(o))
 66             return o;
 67         var copy = {};
 68         for (var key in o)
 69             copy[key] = _copy(o[key], depth-1);
 70         return copy;
 71     }
 72 
 73 
 74     // like a general version of Ext.apply() or mootools.merge()
 75     function _merge(to, from, overwrite, depth)
 76     {
 77         for (var key in from)
 78         {
 79             if (from.hasOwnProperty(key))
 80             {
 81                 if (isObject(to[key]) && isObject(from[key]))
 82                 {
 83                     _merge(to[key], from[key], overwrite, depth-1);
 84                 }
 85                 else if (undefined === to[key] || overwrite)
 86                 {
 87                     to[key] = _copy(from[key], depth-1);
 88                 }
 89             }
 90         }
 91     }
 92 
 93     var getNextRow = function(rowElem, targetTagName)
 94     {
 95         if (null == rowElem)
 96             return null;
 97 
 98 
 99         var nextRow = rowElem.nextSibling;
100         while (nextRow != null && !nextRow.tagName)
101             nextRow = nextRow.nextSibling;
102 
103         if (nextRow == null)
104             return null;
105 
106         if (targetTagName)
107         {
108             if (nextRow.tagName != targetTagName)
109                 return null;
110         }
111         else
112         {
113             if (nextRow.tagName != "TR")
114                 return null;
115         }
116 
117         return nextRow;
118     };
119 
120     var collapseExpand = function(elem, notify, targetTagName)
121     {
122         var collapse = false;
123         var url = elem.href;
124         if (targetTagName)
125         {
126             while (elem.tagName != targetTagName)
127                 elem = elem.parentNode;
128         }
129         else
130         {
131             while (elem.tagName != 'TR')
132                 elem = elem.parentNode;
133         }
134 
135         var nextRow = getNextRow(elem, targetTagName);
136         if (null != nextRow && nextRow.style.display != "none")
137             collapse = true;
138 
139         while (nextRow != null)
140         {
141             if (nextRow.className.indexOf("labkey-header") != -1)
142                 break;
143             if (nextRow.style.display != "none")
144                 nextRow.style.display = "none";
145             else
146                 nextRow.style.display = "";
147             nextRow = getNextRow(nextRow, targetTagName);
148         }
149 
150         if (null != url && notify)
151             notifyExpandCollapse(url, collapse);
152         return false;
153     };
154 
155     var notifyExpandCollapse = function(url, collapse)
156     {
157         if (url) {
158             if (collapse)
159                 url += "&collapse=true";
160             LABKEY.Ajax.request({url: url});
161         }
162     };
163 
164     var toggleLink = function(link, notify, targetTagName)
165     {
166         collapseExpand(link, notify, targetTagName);
167         var i = 0;
168         while (typeof(link.childNodes[i].src) == "undefined" )
169             i++;
170 
171         if (link.childNodes[i].src.search("plus.gif") >= 0)
172             link.childNodes[i].src = link.childNodes[i].src.replace("plus.gif", "minus.gif");
173         else
174             link.childNodes[i].src = link.childNodes[i].src.replace("minus.gif", "plus.gif");
175         return false;
176     };
177 
178     /** @scope LABKEY.Utils */
179     return {
180         /**
181         * Encodes the html passed in so that it will not be interpreted as HTML by the browser.
182         * For example, if your input string was "<p>Hello</p>" the output would be
183         * "&lt;p&gt;Hello&lt;/p&gt;". If you set an element's innerHTML property
184         * to this string, the HTML markup will be displayed as literal text rather than being
185         * interpreted as HTML.
186         *
187         * @param {String} html The HTML to encode
188 		* @return {String} The encoded HTML
189 		*/
190         encodeHtml : function(html)
191         {
192             // https://stackoverflow.com/a/7124052
193             return String(html)
194                     .replace(/&/g, '&')
195                     .replace(/"/g, '"')
196                     .replace(/'/g, ''')
197                     .replace(/</g, '<')
198                     .replace(/>/g, '>')
199                     .replace(/\//g, '/');
200         },
201 
202         isArray: function(value) {
203             // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
204             return Object.prototype.toString.call(value) === "[object Array]";
205         },
206 
207         isDate: function(value) {
208             return Object.prototype.toString.call(value) === '[object Date]';
209         },
210 
211         isNumber: function(value) {
212             return typeof value === 'number' && isFinite(value);
213         },
214 
215         isDefined: function(value) {
216             return typeof value !== 'undefined';
217         },
218 
219         isFunction: function(value) {
220             // http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type
221             var getType = {};
222             return value !== null && value !== undefined && getType.toString.call(value) === '[object Function]';
223         },
224 
225         isObject: isObject,
226 
227         /**
228          * Returns date formats for use in an Ext.form.DateField. Useful when using a DateField in an Ext object,
229          * it contains a very large set of date formats, which helps make a DateField more robust. For example, a
230          * user would be allowed to enter dates like 6/1/2011, 06/01/2011, 6/1/11, etc.
231          */
232         getDateAltFormats : function()
233         {
234             return LABKEY.useMDYDateParsing ? DATEALTFORMATS_MonthDay : DATEALTFORMATS_DayMonth;
235         },
236 
237         /**
238          * Returns date format with timestamp including milliseconds. Useful for parsing the date in "yyyy-MM-dd HH:mm:ss.SSS" format
239          * as returned by DateUtil.getJsonDateTimeFormatString().
240          * ex. Ext4.Date.parse("2019-02-15 17:15:10.123", 'Y-m-d H:i:s.u')
241          */
242         getDateTimeFormatWithMS : function()
243         {
244             return DATETIMEFORMAT_WithMS;
245         },
246 
247         displayAjaxErrorResponse: function() {
248             console.warn('displayAjaxErrorResponse: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
249         },
250 
251         /**
252          * Generates a display string from the response to an error from an AJAX request
253          * @private
254          * @ignore
255          * @param {XMLHttpRequest} responseObj The XMLHttpRequest object containing the response data.
256          * @param {Error} [exceptionObj] A JavaScript Error object caught by the calling code.
257          * @param {Object} [config] An object with additional configuration properties.  It supports the following:
258          * <li>msgPrefix: A string that will be used as a prefix in the error message.  Default to: 'An error occurred trying to load'</li>
259          * <li>showExceptionClass: A boolean flag to display the java class of the exception.</li>
260          */
261         getMsgFromError: function(responseObj, exceptionObj, config){
262             config = config || {};
263             var error;
264             var prefix = config.msgPrefix || 'An error occurred trying to load:\n';
265 
266             if (responseObj && responseObj.responseText && responseObj.getResponseHeader('Content-Type'))
267             {
268                 var contentType = responseObj.getResponseHeader('Content-Type');
269                 if (contentType.indexOf('application/json') >= 0)
270                 {
271                     var jsonResponse = LABKEY.Utils.decode(responseObj.responseText);
272                     if (jsonResponse && jsonResponse.exception) {
273                         error = prefix + jsonResponse.exception;
274                         if (config.showExceptionClass)
275                             error += "\n(" + (jsonResponse.exceptionClass ? jsonResponse.exceptionClass : "Exception class unknown") + ")";
276                     }
277                 }
278                 else if (contentType.indexOf('text/html') >= 0 && jQuery)
279                 {
280                     var html = jQuery(responseObj.responseText);
281                     var el = html.find('.exception-message');
282                     if (el && el.length === 1)
283                         error = prefix + el.text().trim();
284                 }
285             }
286             if (!error)
287                 error = prefix + "Status: " + responseObj.statusText + " (" + responseObj.status + ")";
288             if (exceptionObj && exceptionObj.message)
289                 error += "\n" + exceptionObj.name + ": " + exceptionObj.message;
290 
291             return error;
292         },
293 
294         /**
295          * This method has been migrated to specific instances for both Ext 3.4.1 and Ext 4.2.1.
296          * For Ext 3.4.1 see LABKEY.ext.Utils.resizeToViewport
297          * For Ext 4.2.1 see LABKEY.ext4.Util.resizeToViewport
298          */
299         resizeToViewport : function()
300         {
301             console.warn('LABKEY.Utils.resizeToViewport has been migrated. See JavaScript API documentation for details.');
302         },
303 
304         /**
305          * Returns a URL to the appropriate file icon image based on the specified file name.
306          * Note that file name can be a full path or just the file name and extension.
307          * If the file name does not include an extension, the URL for a generic image will be returned
308          * @param {String} fileName The file name.
309          * @return {String} The URL suitable for use in the src attribute of an img element.
310          */
311         getFileIconUrl : function(fileName) {
312             var idx = fileName.lastIndexOf(".");
313             var extension = (idx >= 0) ? fileName.substring(idx + 1) : "_generic";
314             return LABKEY.ActionURL.buildURL("core", "getAttachmentIcon", "", {extension: extension});
315         },
316 
317         /**
318          * This is used internally by other class methods to automatically parse returned JSON
319          * and call another success function passing that parsed JSON.
320          * @param fn The callback function to wrap
321          * @param scope The scope for the callback function
322          * @param {boolean} [isErrorCallback=false] Set to true if the function is an error callback.  If true, and you do not provide a separate callback, alert will popup showing the error message
323          */
324         getCallbackWrapper : function(fn, scope, isErrorCallback) {
325             return function(response, options)
326             {
327                 var json = response.responseJSON;
328                 if (!json)
329                 {
330                     //ensure response is JSON before trying to decode
331                     if (response && response.getResponseHeader && response.getResponseHeader('Content-Type')
332                             && response.getResponseHeader('Content-Type').indexOf('application/json') >= 0){
333                         try {
334                             json = LABKEY.Utils.decode(response.responseText);
335                         }
336                         catch (error){
337                             //we still want to proceed even if we cannot decode the JSON
338                         }
339 
340                     }
341 
342                     response.responseJSON = json;
343                 }
344 
345                 if (!json && isErrorCallback)
346                 {
347                     json = {};
348                 }
349 
350                 if (json && !json.exception && isErrorCallback)
351                 {
352                     // Try to make sure we don't show an empty error message
353                     json.exception = (response && response.statusText ? response.statusText : "Communication failure.");
354                 }
355 
356                 if (fn)
357                     fn.call(scope || this, json, response, options);
358                 else if (isErrorCallback && response.status != 0)
359                 {
360                     // Don't show an error dialog if the user cancelled the request in the browser, like navigating
361                     // to another page
362                     LABKEY.Utils.alert("Error", json.exception);
363                 }
364             };
365         },
366 
367         /**
368          * Applies properties from the source object to the target object, translating
369          * the property names based on the translation map. The translation map should
370          * have an entry per property that you wish to rename when it is applied on
371          * the target object. The key should be the name of the property on the source object
372          * and the value should be the desired name on the target object. The value may
373          * also be set to null or false to prohibit that property from being applied.
374          * By default, this function will also apply all other properties on the source
375          * object that are not listed in the translation map, but you can override this
376          * by supplying false for the applyOthers parameter.
377          * @param target The target object
378          * @param source The source object
379          * @param translationMap A map listing property name translations
380          * @param applyOthers Set to false to prohibit application of properties
381          * not explicitly mentioned in the translation map.
382          * @param applyFunctions Set to false to prohibit application of properties
383          * that are functions
384          */
385         applyTranslated : function(target, source, translationMap, applyOthers, applyFunctions)
386         {
387             if (undefined === target)
388                 target = {};
389             if (undefined === applyOthers)
390                 applyOthers = true;
391             if (undefined == applyFunctions && applyOthers)
392                 applyFunctions = true;
393             var targetPropName;
394             for (var prop in source)
395             {
396                 //special case: Ext adds a "constructor" property to every object, which we don't want to apply
397                 if (prop == "constructor" || LABKEY.Utils.isFunction(prop))
398                     continue;
399                 
400                 targetPropName = translationMap[prop];
401                 if (targetPropName)
402                     target[translationMap[prop]] = source[prop];
403                 else if (undefined === targetPropName && applyOthers && (applyFunctions || !LABKEY.Utils.isFunction(source[prop])))
404                     target[prop] = source[prop];
405             }
406         },
407 
408         /**
409          * Sets a client-side cookie.  Useful for saving non-essential state to provide a better
410          * user experience.  Note that some browser settings may prevent cookies from being saved,
411          * and users can clear browser cookies at any time, so cookies are not a substitute for
412          * database persistence.
413          * @param {String} name The name of the cookie to be saved.
414          * @param {String} value The value of the cookie to be saved.
415          * @param {Boolean} pageonly Whether this cookie should be scoped to the entire site, or just this page.
416          * Page scoping considers the entire URL without parameters; all URL contents after the '?' are ignored.
417          * @param {int} days The number of days the cookie should be saved on the client.
418          */
419         setCookie : function(name, value, pageonly, days) {
420             var expires;
421             if (days)
422             {
423                 var date = new Date();
424                 date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
425                 expires = "; expires=" + date.toGMTString();
426             }
427             else
428                 expires = "";
429             var path = "/";
430             if (pageonly)
431                 path = location.pathname.substring(0, location.pathname.lastIndexOf('/'));
432             document.cookie = name + "=" + value + expires + "; path=" + path;
433         },
434 
435         /**
436          * Retrieves a cookie.  Useful for retrieving non-essential state to provide a better
437          * user experience.  Note that some browser settings may prevent cookies from being saved,
438          * and users can clear browser cookies at any time, so previously saved cookies should not be assumed
439          * to be available.
440          * @param {String} name The name of the cookie to be retrieved.
441          * @param {String} defaultvalue The value to be returned if no cookie with the specified name is found on the client.
442          */
443         getCookie : function(name, defaultvalue) {
444             var nameEQ = name + "=";
445             var ca = document.cookie.split(';');
446             for (var i=0; i < ca.length; i++)
447             {
448                 var c = ca[i];
449                 while (c.charAt(0) == ' ')
450                     c = c.substring(1, c.length);
451                 if (c.indexOf(nameEQ) == 0)
452                     return c.substring(nameEQ.length, c.length);
453             }
454             return defaultvalue;
455         },
456 
457         /**
458          * Retrieves the current LabKey Server session ID. Note that this may only be made available when the
459          * session ID cookie is marked as httpOnly = false.
460          * @returns {String} sessionid The current session id. Defaults to ''.
461          * @see {@link https://www.owasp.org/index.php/HttpOnly|OWASP HttpOnly}
462          * @see {@link https://tomcat.apache.org/tomcat-7.0-doc/config/context.html#Common_Attributes|Tomcat Attributes}
463          */
464         getSessionID : function()
465         {
466             return LABKEY.Utils.getCookie('JSESSIONID', '');
467         },
468 
469         /**
470          * Deletes a cookie.  Note that 'name' and 'pageonly' should be exactly the same as when the cookie
471          * was set.
472          * @param {String} name The name of the cookie to be deleted.
473          * @param {Boolean} pageonly Whether the cookie is scoped to the entire site, or just this page.
474          * Deleting a site-level cookie has no impact on page-level cookies, and deleting page-level cookies
475          * has no impact on site-level cookies, even if the cookies have the same name.
476          */
477         deleteCookie : function (name, pageonly)
478         {
479             LABKEY.Utils.setCookie(name, "", pageonly, -1);
480         },
481 
482         /**
483          * Loads JavaScript file(s) from the server.
484          * @function
485          * @param {(string|string[])} file - A file or Array of files to load.
486          * @param {Function} [callback] - Callback for when all dependencies are loaded.
487          * @param {Object} [scope] - Scope of callback.
488          * @param {boolean} [inOrder=false] - True to load the scripts in the order they are passed in. Default is false.
489          * @example
490          <script type="text/javascript">
491          LABKEY.requiresScript("myModule/myScript.js", true, function() {
492                     // your script is loaded
493                 });
494          </script>
495          */
496         requiresScript : function(file, callback, scope, inOrder)
497         {
498             LABKEY.requiresScript.apply(this, arguments);
499         },
500 
501         /**
502          * Includes a Cascading Style Sheet (CSS) file into the page. If the file was already included by some other code, this
503          * function will simply ignore the call. This may be used to include CSS files defined in your module's web/ directory.
504          * @param {String} filePath The path to the script file to include. This path should be relative to the web application
505          * root. So for example, if you wanted to include a file in your module's web/mymodule/styles/ directory,
506          * the path would be "mymodule/styles/mystyles.css"
507          */
508         requiresCSS : function(filePath)
509         {
510             LABKEY.requiresCss(filePath);
511         },
512 
513         /**
514          * Returns true if value ends with ending
515          * @param value the value to examine
516          * @param ending the ending to look for
517          */
518         endsWith : function(value, ending)
519         {
520             if (!value || !ending)
521                 return false;
522             if (value.length < ending.length)
523                 return false;
524             return value.substring(value.length - ending.length) == ending;
525         },
526 
527         pluralBasic : function(count, singular)
528         {
529             return LABKEY.Utils.pluralize(count, singular, singular + 's');
530         },
531 
532         pluralize : function(count, singular, plural)
533         {
534             return count.toLocaleString() + " " + (1 == count ? singular : plural);
535         },
536 
537         /**
538          * Iteratively calls a tester function you provide, calling another callback function once the
539          * tester function returns true. This function is useful for advanced JavaScript scenarios, such
540          * as cases where you are including common script files dynamically using the requiresScript()
541          * method, and need to wait until classes defined in those files are parsed and ready for use.
542          *  
543          * @param {Object} config a configuration object with the following properties:
544          * @param {Function} config.testCallback A function that returns true or false. This will be called every
545          * ten milliseconds until it returns true or the maximum number of tests have been made.
546          * @param {Array} [config.testArguments] A array of arguments to pass to the testCallback function
547          * @param {Function} config.success The function to call when the testCallback returns true.
548          * @param {Array} [config.successArguments] A array of arguments to pass to the successCallback function
549          * @param {Object} [config.failure] A function to call when the testCallback throws an exception, or when
550          * the maximum number of tests have been made.
551          * @param {Array} [config.errorArguments] A array of arguments to pass to the errorCallback function
552          * @param {Object} [config.scope] A scope to use when calling any of the callback methods (defaults to this)
553          * @param {int} [config.maxTests] Maximum number of tests before the errorCallback is called (defaults to 1000).
554          *
555          * @example
556 <script>
557     LABKEY.Utils.requiresScript("FileUploadField.js");
558     LABKEY.Utils.requiresCSS("FileUploadField.css");
559 </script>
560 
561 <script>
562     function tester()
563     {
564         return undefined != Ext.form.FileUploadField;
565     }
566 
567     function onTrue(msg)
568     {
569         //this alert is merely to demonstrate the successArguments config property
570         alert(msg);
571 
572         //use the file upload field...
573     }
574 
575     function onFailure(msg)
576     {
577         alert("ERROR: " + msg);
578     }
579 
580     LABKEY.Utils.onTrue({
581         testCallback: tester,
582         success: onTrue,
583         successArguments: ['FileUploadField is ready to use!'],
584         failure: onFailure,
585         maxTests: 100
586     });
587 </script>
588          */
589         onTrue : function(config) {
590             config.maxTests = config.maxTests || 1000;
591             try
592             {
593                 if (config.testCallback.apply(config.scope || this, config.testArguments || []))
594                     LABKEY.Utils.getOnSuccess(config).apply(config.scope || this, config.successArguments || []);
595                 else
596                 {
597                     if (config.maxTests <= 0)
598                     {
599                         throw "Maximum number of tests reached!";
600                     }
601                     else
602                     {
603                         --config.maxTests;
604                         LABKEY.Utils.onTrue.defer(10, this, [config]);
605                     }
606                 }
607             }
608             catch(e)
609             {
610                 if (LABKEY.Utils.getOnFailure(config))
611                 {
612                     LABKEY.Utils.getOnFailure(config).apply(config.scope || this, [e,config.errorArguments]);
613                 }
614             }
615         },
616 
617         ensureBoxVisible : function() {
618             console.warn('LABKEY.Utils.ensureBoxVisible has been migrated to the appropriate Ext scope. Consider LABKEY.ext.Utils.ensureBoxVisible or LABKEY.ext4.Util.ensureBoxVisible');
619         },
620 
621         /**
622          * Will generate a unique id. If you provide a prefix, consider making it DOM safe so it can be used as
623          * an element id.
624          * @param {string} [prefix=lk-gen] - Optional prefix to start the identifier.
625          * @returns {*}
626          */
627         id : function(prefix) {
628             return (prefix || "lk-gen") + (++idSeed);
629         },
630 
631         /**
632           * Returns a universally unique identifier, of the general form: "92329D39-6F5C-4520-ABFC-AAB64544E172"
633           * NOTE: Do not use this for DOM id's as it does not meet the requirements for DOM id specification.
634           * Based on original Math.uuid.js (v1.4)
635           * http://www.broofa.com
636           * mailto:robert@broofa.com
637           * Copyright (c) 2010 Robert Kieffer
638           * Dual licensed under the MIT and GPL licenses.
639         */
640         generateUUID : function() {
641             // First see if there are any server-generated UUIDs available to return
642             if (LABKEY && LABKEY.uuids && LABKEY.uuids.length > 0)
643             {
644                 return LABKEY.uuids.pop();
645             }
646             // From the original Math.uuidFast implementation
647             var chars = CHARS, uuid = new Array(36), rnd = 0, r;
648             for (var i = 0; i < 36; i++)
649             {
650                 if (i == 8 || i == 13 || i == 18 || i == 23)
651                 {
652                     uuid[i] = '-';
653                 }
654                 else if (i == 14)
655                 {
656                     uuid[i] = '4';
657                 }
658                 else
659                 {
660                     if (rnd <= 0x02) rnd = 0x2000000 + (Math.random() * 0x1000000) | 0;
661                     r = rnd & 0xf;
662                     rnd = rnd >> 4;
663                     uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
664                 }
665             }
666             return uuid.join('');
667         },
668 
669         /**
670          * Returns a string containing a well-formed html anchor that will apply theme specific styling. The configuration
671          * takes any property value pair and places them on the anchor.
672          * @param {Object} config a configuration object that models html anchor properties:
673          * @param {String} config.href (required if config.onClick not specified) the reference the anchor will use.
674          * @param {String} config.onClick (required if config.href not specified) script called when the onClick event is fired by
675          * the anchor.
676          * @param {String} config.text text that is rendered inside the anchor element.
677          */
678         textLink : function(config)
679         {
680             if (config.href === undefined && !config.onClick === undefined)
681             {
682                 throw "href AND/OR onClick required in call to LABKEY.Utils.textLink()";
683             }
684             var attrs = " ";
685             if (config)
686             {
687                 for (var i in config)
688                 {
689                     if (config.hasOwnProperty(i))
690                     {
691                         if (i.toString() != "text" && i.toString() != "class")
692                         {
693                             attrs += i.toString() + '=\"' + config[i] + '\" ';
694                         }
695                     }
696                 }
697 
698                 return '<a class="labkey-text-link"' + attrs + '>' + (config.text != null ? config.text : "") + '</a>';
699             }
700             throw "Config object not found for textLink.";
701         },
702 
703         /**
704          *
705          * Standard documented name for error callback arguments is "failure" but various other names have been employed in past.
706          * This function provides reverse compatibility by picking the failure callback argument out of a config object
707          * be it named failure, failureCallback or errorCallback.
708          *
709          * @param config
710          */
711         getOnFailure : function(config)
712         {
713             return config.failure || config.errorCallback || config.failureCallback;
714             // maybe it be desirable for this fall all the way back to returning LABKEY.Utils.displayAjaxErrorResponse?
715         },
716 
717         /**
718          *
719          * Standard documented name for success callback arguments is "success" but various names have been employed in past.
720          * This function provides reverse compatibility by picking the success callback argument out of a config object,
721          * be it named success or successCallback.
722          *
723          * @param config
724          */
725         getOnSuccess : function(config)
726         {
727             return config.success || config.successCallback
728         },
729 
730 
731         /**
732          * Apply properties from b, c, ...  to a.  Properties of each subsequent
733          * object overwrites the previous.
734          *
735          * The first object is modified.
736          *
737          * Use merge({}, o) to create a deep copy of o.
738          */
739         merge : function(a, b, c)
740         {
741             var o = a;
742             for (var i=1 ; i<arguments.length ; i++)
743                 _merge(o, arguments[i], true, 50);
744             return o;
745         },
746 
747 
748         /**
749          * Apply properties from b, c, ... to a.  Properties are not overwritten.
750          *
751          * The first object is modified.
752          */
753         mergeIf : function(a, b, c)
754         {
755             var o = arguments[0];
756             for (var i=1 ; i<arguments.length ; i++)
757                 _merge(o, arguments[i], false, 50);
758             return o;
759         },
760 
761         onError : function(error){
762             console.warn('onError: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
763         },
764 
765         /**
766          * Returns true if the passed object is empty (ie. {}) and false if not.
767          *
768          * @param {Object} ob The object to test
769          * @return {Boolean} the result of the test
770         */
771         isEmptyObj : function(ob){
772            for (var i in ob){ return false;}
773            return true;
774         },
775 
776         /**
777          * Rounds the passed number to the specified number of decimals
778          *
779          * @param {Number} input The number to round
780          * @param {Number} dec The number of decimal places to use
781          * @return {Number} The rounded number
782         */
783         roundNumber : function(input, dec){
784             return Math.round(input*Math.pow(10,dec))/Math.pow(10,dec);
785         },
786 
787         /**
788          * Will pad the input string with zeros to the desired length.
789          *
790          * @param {Number/String} input The input string / number
791          * @param {Integer} length The desired length
792          * @param {String} padChar The character to use for padding.
793          * @return {String} The padded string
794         **/
795         padString : function(input, length, padChar){
796             if (typeof input != 'string')
797                 input = input.toString();
798 
799             var pd = '';
800             if (length > input.length){
801                 for (var i=0; i < (length-input.length); i++){
802                     pd += padChar;
803                 }
804             }
805             return pd + input;
806         },
807 
808         /**
809          * Returns true if the arguments are case-insensitive equal.  Note: the method converts arguments to strings for the purposes of comparing numbers, which means that it will return odd behaviors with objects (ie. LABKEY.Utils.caseInsensitiveEquals({t: 3}, '[object Object]') returns true)
810          *
811          * @param {String/Number} a The first item to test
812          * @param {String/Number} b The second item to test
813          * @return {boolean} True if arguments are case-insensitive equal, false if not
814         */
815         caseInsensitiveEquals: function(a, b) {
816             return String(a).toLowerCase() == String(b).toLowerCase();
817         },
818 
819         /**
820          * Tests whether the passed value can be used as boolean, using a loose definition.  Acceptable values for true are: 'true', 'yes', 1, 'on' or 't'.  Acceptable values for false are: 'false', 'no', 0, 'off' or 'f'.  Values are case-insensitive.
821          * @param value The value to test
822          */
823         isBoolean: function(value){
824             var upperVal = value.toString().toUpperCase();
825             if (upperVal == "TRUE" || value == "1" || upperVal == "Y" || upperVal == "YES" || upperVal == "ON" || upperVal == "T"
826                     || upperVal == "FALSE" || value == "0" || upperVal == "N" || upperVal == "NO" || upperVal == "OFF" || upperVal == "F"){
827                 return true;
828             }
829         },
830 
831         /**
832          * Returns true if the passed value is a string.
833          * @param {Object} value The value to test
834          * @return {Boolean}
835          */
836         isString: function(value) {
837             return typeof value === 'string';
838         },
839 
840         /**
841          * Returns the string value with the first char capitalized.
842          * @param {String} value The string value to capitalize
843          * @return {String}
844          */
845         capitalize: function(value) {
846             if (value && LABKEY.Utils.isString(value) && value.length > 0) {
847                 return value.charAt(0).toUpperCase() + value.substr(1);
848             }
849             return value;
850         },
851 
852         onReady: function(config) {
853             console.warn('onReady: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
854             if (typeof config === "function") {
855                 config();
856             }
857         },
858 
859         /**
860          * Decodes (parses) a JSON string to an object.
861          *
862          * @param {String} data The JSON string
863          * @return {Object} The resulting object
864          */
865         decode : function(data) {
866             return JSON.parse(data + "");
867         },
868 
869         /**
870          * Encodes an Object to a string.
871          *
872          * @param {Object} data the variable to encode.
873          * @return {String} The JSON string.
874          */
875         encode : function(data) {
876             return JSON.stringify(data);
877         },
878 
879         /**
880          * Applies config properties to the specified object.
881          * @param object
882          * @param config
883          * @returns {*}
884          */
885         apply : function(object, config) {
886             var enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable',
887                 'toLocaleString', 'toString', 'constructor'];
888 
889             if (object && config && typeof config === 'object') {
890                 var i, j, k;
891 
892                 for (i in config) {
893                     object[i] = config[i];
894                 }
895 
896                 if (enumerables) {
897                     for (j = enumerables.length; j--;) {
898                         k = enumerables[j];
899                         if (config.hasOwnProperty(k)) {
900                             object[k] = config[k];
901                         }
902                     }
903                 }
904             }
905             return object;
906         },
907 
908         /**
909          * Display an error dialog
910          * @param title
911          * @param msg
912          */
913         alert : function(title, msg) {
914             console.warn('alert: This is just a stub implementation, request the dom version of the client API : clientapi_dom.lib.xml to get the concrete implementation');
915             console.warn(title + ":", msg);
916         },
917 
918 
919         escapeRe : function(s) {
920             return s.replace(/([-.*+?\^${}()|\[\]\/\\])/g, "\\$1");
921         },
922 
923         getMeasureAlias : function(measure, override) {
924             if (measure.alias && !override) {
925                 return measure.alias;
926             }
927             else {
928                 var alias = measure.schemaName + '_' + measure.queryName + '_' + measure.name;
929                 return alias.replace(/\//g, '_');
930             }
931         },
932 
933         /**
934          * POSTs to the given href, including CSRF token. Taken from PageFlowUtil.postOnClickJavascript .
935          * @param href containing action and parameters to be POSTed
936          */
937 
938         postToAction : function (href) {
939             var form = document.createElement('form');
940             form.setAttribute('method', 'post');
941             form.setAttribute('action', href);
942             var input = document.createElement('input');
943             input.type = 'hidden';
944             input.name = 'X-LABKEY-CSRF';
945             input.value = LABKEY.CSRF;
946             form.appendChild(input);
947             form.style.display = 'hidden';
948             document.body.appendChild(form);
949             form.submit();
950         },
951 
952         /**
953          * Displays a confirmation dialog with the specified message and then, if confirmed, POSTs to the href, using the method above
954          */
955         confirmAndPost : function (message, href) {
956             if (confirm(message))
957                 this.postToAction(href);
958 
959             return false;
960         },
961 
962         // private
963         collapseExpand: collapseExpand,
964         notifyExpandCollapse: notifyExpandCollapse,
965         toggleLink: toggleLink
966     };
967 };
968 
969 /** docs for methods defined in dom/Utils.js - primarily here to ensure API docs get generated with combined core/dom versions */
970 
971 /**
972  * Sends a JSON object to the server which turns it into an Excel file and returns it to the browser to be saved or opened.
973  * @memberOf LABKEY.Utils
974  * @function
975  * @static
976  * @name convertToExcel
977  * @param {Object} spreadsheet the JavaScript representation of the data
978  * @param {String} spreadsheet.fileName name to suggest to the browser for saving the file. If the fileName is
979  * specified and ends with ".xlsx", it will be returned in Excel 2007 format.
980  * @param {String} spreadsheet.sheets array of sheets, which are objects with properties:
981  * <ul>
982  * <li><b>name:</b> name of the Excel sheet</li>
983  * <li><b>data:</b> two dimensional array of values</li>
984  * </ul>
985  * The value array may be either primitives (booleans, numbers, Strings, and dates), or may be a map with
986  * the following structure:
987  * <ul>
988  * <li><b>value:</b> the boolean, number, String, or date value of the cell</li>
989  * <li><b>formatString:</b> for dates and numbers, the Java format string used with SimpleDateFormat
990  * or DecimalFormat to control how the value is formatted</li>
991  * <li><b>timeOnly:</b> for dates, whether the date part should be ignored and only the time value is important</li>
992  * <li><b>forceString:</b> force the value to be treated as a string (i.e. prevent attempt to convert it to a date)</li>
993  * </ul>
994  * @example <script type="text/javascript">
995  LABKEY.Utils.convertToExcel(
996  {
997      fileName: 'output.xls',
998      sheets:
999      [
1000          {
1001              name: 'FirstSheet',
1002              data:
1003              [
1004                  ['Row1Col1', 'Row1Col2'],
1005                  ['Row2Col1', 'Row2Col2']
1006              ]
1007          },
1008          {
1009              name: 'SecondSheet',
1010              data:
1011              [
1012                  ['Col1Header', 'Col2Header'],
1013                  [{value: 1000.5, formatString: '0,000.00'}, {value: '5 Mar 2009 05:14:17', formatString: 'yyyy MMM dd'}],
1014                  [{value: 2000.6, formatString: '0,000.00'}, {value: '6 Mar 2009 07:17:10', formatString: 'yyyy MMM dd'}]
1015 
1016              ]
1017          }
1018      ]
1019  });
1020  </script>
1021  */
1022 
1023 /**
1024  * Sends a JSON object to the server which turns it into an TSV or CSV file and returns it to the browser to be saved or opened.
1025  * @memberOf LABKEY.Utils
1026  * @function
1027  * @static
1028  * @name convertToTable
1029  * @param {Object} config.  The config object
1030  * @param {String} config.fileNamePrefix name to suggest to the browser for saving the file. The appropriate extension (either ".txt" or ".csv", will be appended based on the delim character used (see below).  Defaults to 'Export'
1031  * @param {String} config.delim The separator between fields.  Allowable values are 'COMMA' or 'TAB'.
1032  * @param {String} config.quoteChar The character that will be used to quote each field.  Allowable values are 'DOUBLE' (ie. double-quote character), 'SINLGE' (ie. single-quote character) or 'NONE' (ie. no character used).  Defaults to none.
1033  * @param {String} config.newlineChar The character that will be used to separate each line.  Defaults to '\n'
1034  * @param {String} config.rows array of rows, which are arrays with values for each cell.
1035  * @example <script type="text/javascript">
1036  LABKEY.Utils.convertToTable(
1037  {
1038      fileName: 'output.csv',
1039      rows:
1040      [
1041          ['Row1Col1', 'Row1Col2'],
1042          ['Row2Col1', 'Row2Col2']
1043      ],
1044      delim: 'COMMA'
1045  });
1046  </script>
1047  */
1048 
1049 /**
1050  * Display an error dialog.
1051  * @memberOf LABKEY.Utils
1052  * @function
1053  * @static
1054  * @name alert
1055  * @param title
1056  * @param msg
1057  */
1058 
1059 /**
1060  * Display a modal dialog.
1061  * @memberOf LABKEY.Utils
1062  * @function
1063  * @static
1064  * @name modal
1065  * @param title Title of the modal dialog.
1066  * @param msg Message to be included in the dialog body. Can be null if the function generates its own content.
1067  * @param fn {function} This will be called with the provided argument list {args} after the modal is shown. You can generate content in
1068  * the modal via the following empty div: <div id="modal-fn-body"></div>
1069  * @param args {array} Array of arguments to be applied to the function when it is called.
1070  *
1071  * @example <script type="text/javascript">
1072  *
1073  * var myFN = function(arg1) {
1074  *     document.getElementById('modal-fn-body').innerHTML = "Hello " + LABKEY.Security.currentUser[arg1] + "!";
1075  * }
1076  * LABKEY.Utils.modal("Hello", null, myFN, ["displayName"]);
1077  * </script>
1078  */
1079 
1080 /**
1081  * Sets the title of the webpart on the page.  This change is not sticky, so it will be reverted on refresh.
1082  * @memberOf LABKEY.Utils
1083  * @function
1084  * @static
1085  * @name setWebpartTitle
1086  * @param {string} title The title string
1087  * @param {integer} webPartId The ID of the webpart
1088  */
1089 
1090 /**
1091  * Provides a generic error callback.  This helper show a modal dialog, log the error to the console
1092  * and will log the error to the audit log table. The user must have insert permissions on the selected container for
1093  * this to work.  By default, it will insert the error into the Shared project.  A containerPath param can be passed to
1094  * use a different container.  The intent of this helper is to provide site admins with a mechanism to identify errors associated
1095  * with client-side code.  If noAuditLog=true is used, the helper will not log the error.
1096  *
1097  * @memberOf LABKEY.Utils
1098  * @function
1099  * @static
1100  * @name onError
1101  * @param {Object} error The error object passed to the callback function
1102  * @param {String} [error.containerPath] Container where errors will be logged. Defaults to /shared
1103  * @param {Boolean} [error.noAuditLog] If false, the errors will not be logged in the audit table.  Defaults to true
1104  *
1105  * @example <script type="text/javascript">
1106  //basic usage
1107  LABKEY.Query.selectRows({
1108                 schemaName: 'core',
1109                 queryName: 'users',
1110                 success: function(){},
1111                 failure: LABKEY.Utils.onError
1112             });
1113 
1114  //custom container and turning off logging
1115  LABKEY.Query.selectRows({
1116                 schemaName: 'core',
1117                 queryName: 'users',
1118                 success: function(){},
1119                 failure: function(error){
1120                      error.containerPath = 'myContainer';
1121                      error.noAuditLog = true;
1122                      LABKEY.Utils.onError(error);
1123                 }
1124             });
1125  </script>
1126  */
1127 
1128 /**
1129  * Shows an error dialog box to the user in response to an error from an AJAX request, including
1130  * any error messages from the server.
1131  *
1132  * @memberOf LABKEY.Utils
1133  * @function
1134  * @static
1135  * @name displayAjaxErrorResponse
1136  * @param {XMLHttpRequest} responseObj The XMLHttpRequest object containing the response data.
1137  * @param {Error} [exceptionObj] A JavaScript Error object caught by the calling code.
1138  * @param {boolean} [showExceptionClass] Flag to display the java class of the exception.
1139  * @param {String} [msgPrefix] Prefix to the error message (defaults to: 'An error occurred trying to load:')
1140  * The error dialog will display the Error's name and message, if available.
1141  */
1142 
1143 /**
1144  * Adds new listener to be executed when all required scripts are fully loaded.
1145  * @memberOf LABKEY.Utils
1146  * @function
1147  * @static
1148  * @name onReady
1149  * @param {Mixed} config Either a callback function, or an object with the following properties:
1150  *
1151  * <li>callback (required) A function that will be called when required scripts are loaded.</li>
1152  * <li>scope (optional) The scope to be used for the callback function.  Defaults to the current scope.</li>
1153  * <li>scripts (optional) A string with a single script or an array of script names to load.  This will be passed to LABKEY.requiresScript().</li>
1154  * @example <script type="text/javascript">
1155  //simple usage
1156  LABKEY.onReady(function(){
1157                     //your code here.  will be executed once scripts have loaded
1158                 });
1159 
1160  //
1161  LABKEY.Utils.onReady({
1162                     scope: this,
1163                     scripts: ['/myModule/myScript.js', 'AnotherScript.js],
1164                     callback: function(){
1165                         //your code here.  will be executed once scripts have loaded
1166                     });
1167                 });
1168  </script>
1169  */
1170