Supervised user whitelists: Cleanup
[chromium-blink-merge.git] / remoting / webapp / base / js / base.js
blob3be8c8fb6db7fcb958d6dc78c78637408e23a32b
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * @fileoverview
7  * A module that contains basic utility components and methods for the
8  * chromoting project
9  *
10  */
12 'use strict';
14 /** @suppress {duplicate} */
15 var base = base || {};
17 base.debug = function() {};
19 /**
20  * Whether to break in debugger and alert when an assertion fails.
21  * Set it to true for debugging.
22  * @type {boolean}
23  */
24 base.debug.throwOnAssert = false;
26 /**
27  * Assert that |expr| is true else print the |opt_msg|.
28  * @param {boolean} expr
29  * @param {string=} opt_msg
30  */
31 base.debug.assert = function(expr, opt_msg) {
32   if (!expr) {
33     if (!opt_msg) {
34       opt_msg = 'Assertion Failed.';
35     }
36     console.error(opt_msg);
37     if (base.debug.throwOnAssert) {
38       throw new Error(opt_msg);
39     }
40   }
43 /**
44  * @return {string} The callstack of the current method.
45  */
46 base.debug.callstack = function() {
47   try {
48     throw new Error();
49   } catch (/** @type {Error} */ error) {
50     var callstack = error.stack
51       .replace(/^\s+(at eval )?at\s+/gm, '') // Remove 'at' and indentation.
52       .split('\n');
53     callstack.splice(0,2); // Remove the stack of the current function.
54   }
55   return callstack.join('\n');
58 /**
59   * @interface
60   */
61 base.Disposable = function() {};
62 base.Disposable.prototype.dispose = function() {};
64 /**
65  * @constructor
66  * @param {...base.Disposable} var_args
67  * @implements {base.Disposable}
68  */
69 base.Disposables = function(var_args) {
70   /**
71    * @type {Array<base.Disposable>}
72    * @private
73    */
74   this.disposables_ = Array.prototype.slice.call(arguments, 0);
77 /**
78  * @param {...base.Disposable} var_args
79  */
80 base.Disposables.prototype.add = function(var_args) {
81   var disposables = Array.prototype.slice.call(arguments, 0);
82   for (var i = 0; i < disposables.length; i++) {
83     var current = /** @type {base.Disposable} */ (disposables[i]);
84     if (this.disposables_.indexOf(current) === -1) {
85       this.disposables_.push(current);
86     }
87   }
90 /**
91  * @param {...base.Disposable} var_args  Dispose |var_args| and remove
92  *    them from the current object.
93  */
94 base.Disposables.prototype.remove = function(var_args) {
95   var disposables = Array.prototype.slice.call(arguments, 0);
96   for (var i = 0; i < disposables.length; i++) {
97     var disposable = /** @type {base.Disposable} */ (disposables[i]);
98     var index = this.disposables_.indexOf(disposable);
99     if(index !== -1) {
100       this.disposables_.splice(index, 1);
101       disposable.dispose();
102     }
103   }
106 base.Disposables.prototype.dispose = function() {
107   for (var i = 0; i < this.disposables_.length; i++) {
108     this.disposables_[i].dispose();
109   }
110   this.disposables_ = null;
114  * A utility function to invoke |obj|.dispose without a null check on |obj|.
115  * @param {base.Disposable} obj
116  */
117 base.dispose = function(obj) {
118   if (obj) {
119     base.debug.assert(typeof obj.dispose == 'function');
120     obj.dispose();
121   }
125  * Copy all properties from src to dest.
126  * @param {Object} dest
127  * @param {Object} src
128  */
129 base.mix = function(dest, src) {
130   for (var prop in src) {
131     if (src.hasOwnProperty(prop) && !(prop in dest)) {
132       dest[prop] = src[prop];
133     }
134   }
138  * Adds a mixin to a class.
139  * @param {Object} dest
140  * @param {Object} src
141  * @suppress {checkTypes|reportUnknownTypes}
142  */
143 base.extend = function(dest, src) {
144   base.mix(dest.prototype, src.prototype || src);
148  * Inherits properties and methods from |parentCtor| at object construction time
149  * using prototypical inheritance. e.g.
151  * var ParentClass = function(parentArg) {
152  *   this.parentProperty = parentArg;
153  * }
155  * var ChildClass = function() {
156  *   base.inherits(this, ParentClass, 'parentArg'); // must be the first line.
157  * }
159  * var child = new ChildClass();
160  * child instanceof ParentClass // true
162  * See base_inherits_unittest.js for the guaranteed behavior of base.inherits().
163  * This lazy approach is chosen so that it is not necessary to maintain proper
164  * script loading order between the parent class and the child class.
166  * @param {*} childObject
167  * @param {*} parentCtor
168  * @param {...} parentCtorArgs
169  * @suppress {checkTypes|reportUnknownTypes}
170  */
171 base.inherits = function(childObject, parentCtor, parentCtorArgs) {
172   base.debug.assert(parentCtor && parentCtor.prototype,
173                     'Invalid parent constructor.');
174   var parentArgs = Array.prototype.slice.call(arguments, 2);
176   // Mix in the parent's prototypes so that they're available during the parent
177   // ctor.
178   base.mix(childObject, parentCtor.prototype);
179   parentCtor.apply(childObject, parentArgs);
181   // Note that __proto__ is deprecated.
182   //   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
183   //   Global_Objects/Object/proto.
184   // It is used so that childObject instanceof parentCtor will
185   // return true.
186   childObject.__proto__.__proto__ = parentCtor.prototype;
187   base.debug.assert(childObject instanceof parentCtor);
190 base.doNothing = function() {};
193  * Returns an array containing the values of |dict|.
194  * @param {!Object} dict
195  * @return {Array}
196  */
197 base.values = function(dict) {
198   return Object.keys(dict).map(
199     /** @param {string} key */
200     function(key) {
201       return dict[key];
202     });
206  * @param {*} value
207  * @return {*} a recursive copy of |value| or null if |value| is not copyable
208  *   (e.g. undefined, NaN).
209  */
210 base.deepCopy = function(value) {
211   try {
212     return JSON.parse(JSON.stringify(value));
213   } catch (e) {}
214   return null;
218  * Returns a copy of the input object with all null/undefined fields
219  * removed.  Returns an empty object for a null/undefined input.
221  * @param {Object<string,?T>|undefined} input
222  * @return {!Object<string,T>}
223  * @template T
224  */
225 base.copyWithoutNullFields = function(input) {
226   /** @const {!Object} */
227   var result = {};
228   if (input) {
229     for (var field in input) {
230       var value = /** @type {*} */ (input[field]);
231       if (value != null) {
232         result[field] = value;
233       }
234     }
235   }
236   return result;
240  * @type {boolean|undefined}
241  * @private
242  */
243 base.isAppsV2_ = undefined;
246  * @return {boolean} True if this is a v2 app; false if it is a legacy app.
247  */
248 base.isAppsV2 = function() {
249   if (base.isAppsV2_ === undefined) {
250     var manifest = chrome.runtime.getManifest();
251     base.isAppsV2_ =
252         Boolean(manifest && manifest.app && manifest.app.background);
253   }
254   return base.isAppsV2_;
258  * Joins the |url| with optional query parameters defined in |opt_params|
259  * See unit test for usage.
260  * @param {string} url
261  * @param {Object<string>=} opt_params
262  * @return {string}
263  */
264 base.urlJoin = function(url, opt_params) {
265   if (!opt_params) {
266     return url;
267   }
268   var queryParameters = [];
269   for (var key in opt_params) {
270     queryParameters.push(encodeURIComponent(key) + "=" +
271                          encodeURIComponent(opt_params[key]));
272   }
273   return url + '?' + queryParameters.join('&');
278  * @return {Object<string, string>} The URL parameters.
279  */
280 base.getUrlParameters = function() {
281   var result = {};
282   var parts = window.location.search.substring(1).split('&');
283   for (var i = 0; i < parts.length; i++) {
284     var pair = parts[i].split('=');
285     result[pair[0]] = decodeURIComponent(pair[1]);
286   }
287   return result;
291  * Convert special characters (e.g. &, < and >) to HTML entities.
293  * @param {string} str
294  * @return {string}
295  */
296 base.escapeHTML = function(str) {
297   var div = document.createElement('div');
298   div.appendChild(document.createTextNode(str));
299   return div.innerHTML;
303  * Promise is a great tool for writing asynchronous code. However, the construct
304  *   var p = new promise(function init(resolve, reject) {
305  *     ... // code that fulfills the Promise.
306  *   });
307  * forces the Promise-resolving logic to reside in the |init| function
308  * of the constructor.  This is problematic when you need to resolve the
309  * Promise in a member function(which is quite common for event callbacks).
311  * base.Deferred comes to the rescue.  It encapsulates a Promise
312  * object and exposes member methods (resolve/reject) to fulfill it.
314  * Here are the recommended steps to follow when implementing an asynchronous
315  * function that returns a Promise:
316  * 1. Create a deferred object by calling
317  *      var deferred = new base.Deferred();
318  * 2. Call deferred.resolve() when the asynchronous operation finishes.
319  * 3. Call deferred.reject() when the asynchronous operation fails.
320  * 4. Return deferred.promise() to the caller so that it can subscribe
321  *    to status changes using the |then| handler.
323  * Sample Usage:
324  *  function myAsyncAPI() {
325  *    var deferred = new base.Deferred();
326  *    window.setTimeout(function() {
327  *      deferred.resolve();
328  *    }, 100);
329  *    return deferred.promise();
330  *  };
332  * @constructor
333  * @template T
334  */
335 base.Deferred = function() {
336   /**
337    * @private {?function(?):void}
338    */
339   this.resolve_ = null;
341   /**
342    * @private {?function(?):void}
343    */
344   this.reject_ = null;
346   /**
347    * @this {base.Deferred}
348    * @param {function(?):void} resolve
349    * @param {function(*):void} reject
350    */
351   var initPromise = function(resolve, reject) {
352     this.resolve_ = resolve;
353     this.reject_ = reject;
354   };
356   /**
357    * @private {!Promise<T>}
358    */
359   this.promise_ = new Promise(initPromise.bind(this));
362 /** @param {*} reason */
363 base.Deferred.prototype.reject = function(reason) {
364   this.reject_(reason);
367 /** @param {*=} opt_value */
368 base.Deferred.prototype.resolve = function(opt_value) {
369   this.resolve_(opt_value);
372 /** @return {!Promise<T>} */
373 base.Deferred.prototype.promise = function() {
374   return this.promise_;
377 base.Promise = function() {};
380  * @param {number} delay
381  * @return {Promise} a Promise that will be fulfilled after |delay| ms.
382  */
383 base.Promise.sleep = function(delay) {
384   return new Promise(
385     /** @param {function(*):void} fulfill */
386     function(fulfill) {
387       window.setTimeout(fulfill, delay);
388     });
393  * @param {Promise} promise
394  * @return {Promise} a Promise that will be fulfilled iff the specified Promise
395  *     is rejected.
396  */
397 base.Promise.negate = function(promise) {
398   return promise.then(
399       /** @return {Promise} */
400       function() {
401         return Promise.reject();
402       },
403       /** @return {Promise} */
404       function() {
405         return Promise.resolve();
406       });
410  * Converts a |method| with callbacks into a Promise.
412  * @param {Function} method
413  * @param {Array} params
414  * @param {*=} opt_context
415  * @param {boolean=} opt_hasErrorHandler whether the method has an error handler
416  * @return {Promise}
417  */
418 base.Promise.as = function(method, params, opt_context, opt_hasErrorHandler) {
419   return new Promise(function(resolve, reject) {
420     params.push(resolve);
421     if (opt_hasErrorHandler) {
422       params.push(reject);
423     }
424     try {
425       method.apply(opt_context, params);
426     } catch (/** @type {*} */ e) {
427       reject(e);
428     }
429   });
433  * A mixin for classes with events.
435  * For example, to create an alarm event for SmokeDetector:
436  * functionSmokeDetector() {
437  *    base.inherits(this, base.EventSourceImpl);
438  *    this.defineEvents(['alarm']);
439  * };
441  * To fire an event:
442  * SmokeDetector.prototype.onCarbonMonoxideDetected = function() {
443  *   var param = {} // optional parameters
444  *   this.raiseEvent('alarm', param);
445  * }
447  * To listen to an event:
448  * var smokeDetector = new SmokeDetector();
449  * smokeDetector.addEventListener('alarm', listenerObj.someCallback)
451  */
454   * Helper interface for the EventSource.
455   * @constructor
456   */
457 base.EventEntry = function() {
458   /** @type {Array<function():void>} */
459   this.listeners = [];
463 /** @interface */
464 base.EventSource = function() {};
466  /**
467   * Add a listener |fn| to listen to |type| event.
468   * @param {string} type
469   * @param {Function} fn
470   */
471 base.EventSource.prototype.addEventListener = function(type, fn) {};
473  /**
474   * Remove a listener |fn| to listen to |type| event.
475   * @param {string} type
476   * @param {Function} fn
477   */
478 base.EventSource.prototype.removeEventListener = function(type, fn) {};
482   * @constructor
483   * Since this class is implemented as a mixin, the constructor may not be
484   * called.  All initializations should be done in defineEvents.
485   * @implements {base.EventSource}
486   */
487 base.EventSourceImpl = function() {
488   /** @type {Object<string, base.EventEntry>} */
489   this.eventMap_;
493   * @param {base.EventSourceImpl} obj
494   * @param {string} type
495   */
496 base.EventSourceImpl.isDefined = function(obj, type) {
497   base.debug.assert(Boolean(obj.eventMap_),
498                    "The object doesn't support events");
499   base.debug.assert(Boolean(obj.eventMap_[type]), 'Event <' + type +
500     '> is undefined for the current object');
503 base.EventSourceImpl.prototype = {
504   /**
505     * Define |events| for this event source.
506     * @param {Array<string>} events
507     */
508   defineEvents: function(events) {
509     base.debug.assert(!Boolean(this.eventMap_),
510                      'defineEvents can only be called once.');
511     this.eventMap_ = {};
512     events.forEach(
513       /**
514         * @this {base.EventSourceImpl}
515         * @param {string} type
516         */
517       function(type) {
518         base.debug.assert(typeof type == 'string');
519         this.eventMap_[type] = new base.EventEntry();
520     }, this);
521   },
523   /**
524     * @param {string} type
525     * @param {Function} fn
526     */
527   addEventListener: function(type, fn) {
528     base.debug.assert(typeof fn == 'function');
529     base.EventSourceImpl.isDefined(this, type);
531     var listeners = this.eventMap_[type].listeners;
532     listeners.push(fn);
533   },
535   /**
536     * @param {string} type
537     * @param {Function} fn
538     */
539   removeEventListener: function(type, fn) {
540     base.debug.assert(typeof fn == 'function');
541     base.EventSourceImpl.isDefined(this, type);
543     var listeners = this.eventMap_[type].listeners;
544     // find the listener to remove.
545     for (var i = 0; i < listeners.length; i++) {
546       var listener = listeners[i];
547       if (listener == fn) {
548         listeners.splice(i, 1);
549         break;
550       }
551     }
552   },
554   /**
555     * Fire an event of a particular type on this object.
556     * @param {string} type
557     * @param {*=} opt_details The type of |opt_details| should be ?= to
558     *     match what is defined in add(remove)EventListener.  However, JSCompile
559     *     cannot handle invoking an unknown type as an argument to |listener|
560     *     As a hack, we set the type to *=.
561     */
562   raiseEvent: function(type, opt_details) {
563     base.EventSourceImpl.isDefined(this, type);
565     var entry = this.eventMap_[type];
566     var listeners = entry.listeners.slice(0); // Make a copy of the listeners.
568     listeners.forEach(
569       /** @param {function(*=):void} listener */
570       function(listener){
571         if (listener) {
572           listener(opt_details);
573         }
574     });
575   }
580   * A lightweight object that helps manage the lifetime of an event listener.
581   *
582   * For example, do the following if you want to automatically unhook events
583   * when your object is disposed:
584   *
585   * var MyConstructor = function(domElement) {
586   *   this.eventHooks_ = new base.Disposables(
587   *     new base.EventHook(domElement, 'click', this.onClick_.bind(this)),
588   *     new base.EventHook(domElement, 'keydown', this.onClick_.bind(this)),
589   *     new base.ChromeEventHook(chrome.runtime.onMessage,
590   *                              this.onMessage_.bind(this))
591   *   );
592   * }
593   *
594   * MyConstructor.prototype.dispose = function() {
595   *   this.eventHooks_.dispose();
596   *   this.eventHooks_ = null;
597   * }
598   *
599   * @param {base.EventSource} src
600   * @param {string} eventName
601   * @param {Function} listener
602   *
603   * @constructor
604   * @implements {base.Disposable}
605   */
606 base.EventHook = function(src, eventName, listener) {
607   this.src_ = src;
608   this.eventName_ = eventName;
609   this.listener_ = listener;
610   src.addEventListener(eventName, listener);
613 base.EventHook.prototype.dispose = function() {
614   this.src_.removeEventListener(this.eventName_, this.listener_);
618   * An event hook implementation for DOM Events.
619   *
620   * @param {HTMLElement|Element|Window|HTMLDocument} src
621   * @param {string} eventName
622   * @param {Function} listener
623   * @param {boolean} capture
624   *
625   * @constructor
626   * @implements {base.Disposable}
627   */
628 base.DomEventHook = function(src, eventName, listener, capture) {
629   this.src_ = src;
630   this.eventName_ = eventName;
631   this.listener_ = listener;
632   this.capture_ = capture;
633   src.addEventListener(eventName, listener, capture);
636 base.DomEventHook.prototype.dispose = function() {
637   this.src_.removeEventListener(this.eventName_, this.listener_, this.capture_);
642   * An event hook implementation for Chrome Events.
643   *
644   * @param {chrome.Event} src
645   * @param {Function} listener
646   *
647   * @constructor
648   * @implements {base.Disposable}
649   */
650 base.ChromeEventHook = function(src, listener) {
651   this.src_ = src;
652   this.listener_ = listener;
653   src.addListener(listener);
656 base.ChromeEventHook.prototype.dispose = function() {
657   this.src_.removeListener(this.listener_);
661  * A disposable repeating timer.
663  * @param {Function} callback
664  * @param {number} interval
665  * @param {boolean=} opt_invokeNow Whether to invoke the callback now, default
666  *    to false.
668  * @constructor
669  * @implements {base.Disposable}
670  */
671 base.RepeatingTimer = function(callback, interval, opt_invokeNow) {
672   /** @private */
673   this.intervalId_ = window.setInterval(callback, interval);
674   if (opt_invokeNow) {
675     callback();
676   }
679 base.RepeatingTimer.prototype.dispose = function() {
680   window.clearInterval(this.intervalId_);
681   this.intervalId_ = null;
685  * A disposable one shot timer.
687  * @param {Function} callback
688  * @param {number} timeout
690  * @constructor
691  * @implements {base.Disposable}
692  */
693 base.OneShotTimer = function(callback, timeout) {
694   var that = this;
696   /** @private */
697   this.timerId_ = window.setTimeout(function() {
698     that.timerId_ = null;
699     callback();
700   }, timeout);
703 base.OneShotTimer.prototype.dispose = function() {
704   if (this.timerId_ !== null) {
705     window.clearTimeout(this.timerId_);
706     this.timerId_ = null;
707   }
711   * Converts UTF-8 string to ArrayBuffer.
712   *
713   * @param {string} string
714   * @return {ArrayBuffer}
715   */
716 base.encodeUtf8 = function(string) {
717   var utf8String = unescape(encodeURIComponent(string));
718   var result = new Uint8Array(utf8String.length);
719   for (var i = 0; i < utf8String.length; i++)
720     result[i] = utf8String.charCodeAt(i);
721   return result.buffer;
725   * Decodes UTF-8 string from ArrayBuffer.
726   *
727   * @param {ArrayBuffer} buffer
728   * @return {string}
729   */
730 base.decodeUtf8 = function(buffer) {
731   return decodeURIComponent(
732       escape(String.fromCharCode.apply(null, new Uint8Array(buffer))));
736  * Generate a nonce, to be used as an xsrf protection token.
738  * @return {string} A URL-Safe Base64-encoded 128-bit random value. */
739 base.generateXsrfToken = function() {
740   var random = new Uint8Array(16);
741   window.crypto.getRandomValues(random);
742   var base64Token = window.btoa(String.fromCharCode.apply(null, random));
743   return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
747  * @return {string} A random UUID.
748  */
749 base.generateUuid = function() {
750   var random = new Uint16Array(8);
751   window.crypto.getRandomValues(random);
752   /** @type {Array<string>} */
753   var e = new Array();
754   for (var i = 0; i < 8; i++) {
755     e[i] = (/** @type {number} */ (random[i]) + 0x10000).
756         toString(16).substring(1);
757   }
758   return e[0] + e[1] + '-' + e[2] + '-' + e[3] + '-' +
759       e[4] + '-' + e[5] + e[6] + e[7];
763  * @param {string} jsonString A JSON-encoded string.
764  * @return {Object|undefined} The decoded object, or undefined if the string
765  *     cannot be parsed.
766  */
767 base.jsonParseSafe = function(jsonString) {
768   try {
769     return /** @type {Object} */ (JSON.parse(jsonString));
770   } catch (err) {
771     return undefined;
772   }
776  * Return the current time as a formatted string suitable for logging.
778  * @return {string} The current time, formatted as the standard ISO string.
779  *     [yyyy-mm-ddDhh:mm:ss.xyz]
780  */
781 base.timestamp = function() {
782   return '[' + new Date().toISOString() + ']';
786  * Size the current window to fit its content vertically.
787  */
788 base.resizeWindowToContent = function() {
789   var appWindow = chrome.app.window.current();
790   var outerBounds = appWindow.outerBounds;
791   var borderY = outerBounds.height - appWindow.innerBounds.height;
792   appWindow.resizeTo(outerBounds.width, document.body.clientHeight + borderY);
793   // Sometimes, resizing the window causes its position to be reset to (0, 0),
794   // so restore it explicitly.
795   appWindow.moveTo(outerBounds.left, outerBounds.top);