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