// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview * A module that contains basic utility components and methods for the * chromoting project * */ 'use strict'; /** @suppress {duplicate} */ var base = base || {}; /** * @interface */ base.Disposable = function() {}; base.Disposable.prototype.dispose = function() {}; /** * @constructor * @param {...base.Disposable} var_args * @implements {base.Disposable} * @suppress {reportUnknownTypes} */ base.Disposables = function(var_args) { /** * @type {Array} * @private */ this.disposables_ = Array.prototype.slice.call(arguments, 0); }; /** * @param {...base.Disposable} var_args * @suppress {reportUnknownTypes} */ base.Disposables.prototype.add = function(var_args) { var disposables = Array.prototype.slice.call(arguments, 0); for (var i = 0; i < disposables.length; i++) { var current = /** @type {base.Disposable} */ (disposables[i]); if (this.disposables_.indexOf(current) === -1) { this.disposables_.push(current); } } }; /** * @param {...base.Disposable} var_args Dispose |var_args| and remove * them from the current object. * @suppress {reportUnknownTypes} */ base.Disposables.prototype.remove = function(var_args) { var disposables = Array.prototype.slice.call(arguments, 0); for (var i = 0; i < disposables.length; i++) { var disposable = /** @type {base.Disposable} */ (disposables[i]); var index = this.disposables_.indexOf(disposable); if(index !== -1) { this.disposables_.splice(index, 1); disposable.dispose(); } } }; base.Disposables.prototype.dispose = function() { for (var i = 0; i < this.disposables_.length; i++) { this.disposables_[i].dispose(); } this.disposables_ = null; }; /** * A utility function to invoke |obj|.dispose without a null check on |obj|. * @param {base.Disposable} obj */ base.dispose = function(obj) { if (obj) { console.assert(typeof obj.dispose == 'function', 'dispose() should have type function, not ' + typeof obj.dispose + '.'); obj.dispose(); } }; /** * Copy all properties from src to dest. * @param {Object} dest * @param {Object} src */ base.mix = function(dest, src) { for (var prop in src) { if (src.hasOwnProperty(prop) && !(prop in dest)) { dest[prop] = src[prop]; } } }; /** * Adds a mixin to a class. * @param {Object} dest * @param {Object} src * @suppress {checkTypes|reportUnknownTypes} */ base.extend = function(dest, src) { base.mix(dest.prototype, src.prototype || src); }; /** * Inherits properties and methods from |parentCtor| at object construction time * using prototypical inheritance. e.g. * * var ParentClass = function(parentArg) { * this.parentProperty = parentArg; * } * * var ChildClass = function() { * base.inherits(this, ParentClass, 'parentArg'); // must be the first line. * } * * var child = new ChildClass(); * child instanceof ParentClass // true * * See base_inherits_unittest.js for the guaranteed behavior of base.inherits(). * This lazy approach is chosen so that it is not necessary to maintain proper * script loading order between the parent class and the child class. * * @param {*} childObject * @param {*} parentCtor * @param {...} parentCtorArgs * @suppress {checkTypes|reportUnknownTypes} */ base.inherits = function(childObject, parentCtor, parentCtorArgs) { console.assert(parentCtor && parentCtor.prototype, 'Invalid parent constructor.'); var parentArgs = Array.prototype.slice.call(arguments, 2); // Mix in the parent's prototypes so that they're available during the parent // ctor. base.mix(childObject, parentCtor.prototype); parentCtor.apply(childObject, parentArgs); // Note that __proto__ is deprecated. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ // Global_Objects/Object/proto. // It is used so that childObject instanceof parentCtor will // return true. childObject.__proto__.__proto__ = parentCtor.prototype; console.assert(childObject instanceof parentCtor, 'child is not an instance of parent.'); }; base.doNothing = function() {}; /** * Returns an array containing the values of |dict|. * @param {!Object} dict * @return {Array} */ base.values = function(dict) { return Object.keys(dict).map( /** @param {string} key */ function(key) { return dict[key]; }); }; /** * @param {*} value * @return {*} a recursive copy of |value| or null if |value| is not copyable * (e.g. undefined, NaN). */ base.deepCopy = function(value) { try { return JSON.parse(JSON.stringify(value)); } catch (e) {} return null; }; /** * Returns a copy of the input object with all null/undefined fields * removed. Returns an empty object for a null/undefined input. * * @param {Object|undefined} input * @return {!Object} * @template T */ base.copyWithoutNullFields = function(input) { var result = {}; base.mergeWithoutNullFields(result, input); return result; }; /** * Merge non-null fields of |src| into |dest|. * * @param {!Object} dest * @param {Object|undefined} src * @template T */ base.mergeWithoutNullFields = function(dest, src) { if (src) { for (var field in src) { var value = /** @type {*} */ (src[field]); if (value != null) { dest[field] = base.deepCopy(value); } } } }; /** * @param {!Object} object * @return {boolean} True if the object is empty (equal to {}); false otherwise. */ base.isEmptyObject = function(object) { return Object.keys(object).length === 0; }; /** * @type {boolean|undefined} * @private */ base.isAppsV2_ = undefined; /** * @return {boolean} True if this is a v2 app; false if it is a legacy app. */ base.isAppsV2 = function() { if (base.isAppsV2_ === undefined) { var manifest = chrome.runtime.getManifest(); base.isAppsV2_ = Boolean(manifest && manifest.app && manifest.app.background); } return base.isAppsV2_; }; /** * Joins the |url| with optional query parameters defined in |opt_params| * See unit test for usage. * @param {string} url * @param {Object=} opt_params * @return {string} */ base.urlJoin = function(url, opt_params) { if (!opt_params) { return url; } var queryParameters = []; for (var key in opt_params) { queryParameters.push(encodeURIComponent(key) + "=" + encodeURIComponent(opt_params[key])); } return url + '?' + queryParameters.join('&'); }; /** * @return {Object} The URL parameters. */ base.getUrlParameters = function() { var result = {}; var parts = window.location.search.substring(1).split('&'); for (var i = 0; i < parts.length; i++) { var pair = parts[i].split('='); result[pair[0]] = decodeURIComponent(pair[1]); } return result; }; /** * Convert special characters (e.g. &, < and >) to HTML entities. * * @param {string} str * @return {string} */ base.escapeHTML = function(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; }; /** * Promise is a great tool for writing asynchronous code. However, the construct * var p = new promise(function init(resolve, reject) { * ... // code that fulfills the Promise. * }); * forces the Promise-resolving logic to reside in the |init| function * of the constructor. This is problematic when you need to resolve the * Promise in a member function(which is quite common for event callbacks). * * base.Deferred comes to the rescue. It encapsulates a Promise * object and exposes member methods (resolve/reject) to fulfill it. * * Here are the recommended steps to follow when implementing an asynchronous * function that returns a Promise: * 1. Create a deferred object by calling * var deferred = new base.Deferred(); * 2. Call deferred.resolve() when the asynchronous operation finishes. * 3. Call deferred.reject() when the asynchronous operation fails. * 4. Return deferred.promise() to the caller so that it can subscribe * to status changes using the |then| handler. * * Sample Usage: * function myAsyncAPI() { * var deferred = new base.Deferred(); * window.setTimeout(function() { * deferred.resolve(); * }, 100); * return deferred.promise(); * }; * * @constructor * @template T */ base.Deferred = function() { /** * @private {?function(?):void} */ this.resolve_ = null; /** * @private {?function(?):void} */ this.reject_ = null; /** * @this {base.Deferred} * @param {function(?):void} resolve * @param {function(*):void} reject */ var initPromise = function(resolve, reject) { this.resolve_ = resolve; this.reject_ = reject; }; /** * @private {!Promise} */ this.promise_ = new Promise(initPromise.bind(this)); }; /** @param {*} reason */ base.Deferred.prototype.reject = function(reason) { this.reject_(reason); }; /** @param {*=} opt_value */ base.Deferred.prototype.resolve = function(opt_value) { this.resolve_(opt_value); }; /** @return {!Promise} */ base.Deferred.prototype.promise = function() { return this.promise_; }; base.Promise = function() {}; /** * @param {number} delay * @param {*=} opt_value * @return {!Promise} a Promise that will be fulfilled with |opt_value| * after |delay| ms. */ base.Promise.sleep = function(delay, opt_value) { return new Promise( function(resolve) { window.setTimeout(function() { resolve(opt_value); }, delay); }); }; /** * @param {Promise} promise * @return {Promise} a Promise that will be fulfilled iff the specified Promise * is rejected. */ base.Promise.negate = function(promise) { return promise.then( /** @return {Promise} */ function() { return Promise.reject(); }, /** @return {Promise} */ function() { return Promise.resolve(); }); }; /** * Creates a promise that will be fulfilled within a certain timeframe. * * This function creates a result promise |R| that will be resolved to * either |promise| or |opt_defaultValue|. If |promise| is fulfulled * (i.e. resolved or rejected) within |delay| milliseconds, then |R| * is resolved with |promise|. Otherwise, |R| is resolved with * |opt_defaultValue|. * * Avoid passing a promise as |opt_defaultValue|, as this could result * in |R| remaining unfulfilled after |delay| milliseconds. * * @param {!Promise} promise The promise to wrap. * @param {number} delay The number of milliseconds to wait. * @param {*=} opt_defaultValue The default value used to resolve the * result. * @return {!Promise} A new promise. * @template T */ base.Promise.withTimeout = function(promise, delay, opt_defaultValue) { return Promise.race([promise, base.Promise.sleep(delay, opt_defaultValue)]); }; /** * Creates a promise that will be rejected if it is not fulfilled within a * certain timeframe. * * This function creates a result promise |R|. If |promise| is fulfilled * (i.e. resolved or rejected) within |delay| milliseconds, then |R| * is resolved or rejected, respectively. Otherwise, |R| is rejected with * |opt_defaultError|. * * @param {!Promise} promise The promise to wrap. * @param {number} delay The number of milliseconds to wait. * @param {*=} opt_defaultError The default error used to reject the promise. * @return {!Promise} A new promise. * @template T */ base.Promise.rejectAfterTimeout = function(promise, delay, opt_defaultError) { return Promise.race([ promise, base.Promise.sleep(delay).then(function() { return Promise.reject(opt_defaultError); }) ]); }; /** * Converts a |method| with callbacks into a Promise. * * @param {Function} method * @param {Array} params * @param {*=} opt_context * @param {boolean=} opt_hasErrorHandler whether the method has an error handler * @return {Promise} */ base.Promise.as = function(method, params, opt_context, opt_hasErrorHandler) { return new Promise(function(resolve, reject) { params.push(resolve); if (opt_hasErrorHandler) { params.push(reject); } try { method.apply(opt_context, params); } catch (/** @type {*} */ e) { reject(e); } }); }; /** * A mixin for classes with events. * * For example, to create an alarm event for SmokeDetector: * functionSmokeDetector() { * base.inherits(this, base.EventSourceImpl); * this.defineEvents(['alarm']); * }; * * To fire an event: * SmokeDetector.prototype.onCarbonMonoxideDetected = function() { * var param = {} // optional parameters * this.raiseEvent('alarm', param); * } * * To listen to an event: * var smokeDetector = new SmokeDetector(); * smokeDetector.addEventListener('alarm', listenerObj.someCallback) * */ /** * Helper interface for the EventSource. * @constructor */ base.EventEntry = function() { /** @type {Array} */ this.listeners = []; }; /** @interface */ base.EventSource = function() {}; /** * Add a listener |fn| to listen to |type| event. * @param {string} type * @param {Function} fn */ base.EventSource.prototype.addEventListener = function(type, fn) {}; /** * Remove a listener |fn| to listen to |type| event. * @param {string} type * @param {Function} fn */ base.EventSource.prototype.removeEventListener = function(type, fn) {}; /** * @constructor * Since this class is implemented as a mixin, the constructor may not be * called. All initializations should be done in defineEvents. * @implements {base.EventSource} */ base.EventSourceImpl = function() { /** @type {Object} */ this.eventMap_; }; /** * @param {base.EventSourceImpl} obj * @param {string} type * @private */ base.EventSourceImpl.assertHasEvent_ = function(obj, type) { console.assert(Boolean(obj.eventMap_), "The object doesn't support events."); console.assert(Boolean(obj.eventMap_[type]), 'Event <' + type +'> is undefined for the current object.'); }; base.EventSourceImpl.prototype = { /** * Define |events| for this event source. * @param {Array} events */ defineEvents: function(events) { console.assert(!Boolean(this.eventMap_), 'defineEvents() can only be called once.'); this.eventMap_ = {}; events.forEach( /** * @this {base.EventSourceImpl} * @param {string} type */ function(type) { console.assert(typeof type == 'string', 'Event name must be a string; found ' + type + '.'); this.eventMap_[type] = new base.EventEntry(); }, this); }, /** * @param {string} type * @param {Function} fn */ addEventListener: function(type, fn) { console.assert(typeof fn == 'function', 'addEventListener(): event listener for ' + type + ' must be function, not ' + typeof fn + '.'); base.EventSourceImpl.assertHasEvent_(this, type); var listeners = this.eventMap_[type].listeners; listeners.push(fn); }, /** * @param {string} type * @param {Function} fn */ removeEventListener: function(type, fn) { console.assert(typeof fn == 'function', 'removeEventListener(): event listener for ' + type + ' must be function, not ' + typeof fn + '.'); base.EventSourceImpl.assertHasEvent_(this, type); var listeners = this.eventMap_[type].listeners; // find the listener to remove. for (var i = 0; i < listeners.length; i++) { var listener = listeners[i]; if (listener == fn) { listeners.splice(i, 1); break; } } }, /** * Fire an event of a particular type on this object. * @param {string} type * @param {*=} opt_details The type of |opt_details| should be ?= to * match what is defined in add(remove)EventListener. However, JSCompile * cannot handle invoking an unknown type as an argument to |listener| * As a hack, we set the type to *=. */ raiseEvent: function(type, opt_details) { base.EventSourceImpl.assertHasEvent_(this, type); var entry = this.eventMap_[type]; var listeners = entry.listeners.slice(0); // Make a copy of the listeners. listeners.forEach( /** @param {Function} listener */ function(listener){ if (listener) { listener(opt_details); } }); } }; /** * A lightweight object that helps manage the lifetime of an event listener. * * For example, do the following if you want to automatically unhook events * when your object is disposed: * * var MyConstructor = function(domElement) { * this.eventHooks_ = new base.Disposables( * new base.EventHook(domElement, 'click', this.onClick_.bind(this)), * new base.EventHook(domElement, 'keydown', this.onClick_.bind(this)), * new base.ChromeEventHook(chrome.runtime.onMessage, * this.onMessage_.bind(this)) * ); * } * * MyConstructor.prototype.dispose = function() { * this.eventHooks_.dispose(); * this.eventHooks_ = null; * } * * @param {base.EventSource} src * @param {string} eventName * @param {Function} listener * * @constructor * @implements {base.Disposable} */ base.EventHook = function(src, eventName, listener) { this.src_ = src; this.eventName_ = eventName; this.listener_ = listener; src.addEventListener(eventName, listener); }; base.EventHook.prototype.dispose = function() { this.src_.removeEventListener(this.eventName_, this.listener_); }; /** * An event hook implementation for DOM Events. * * @param {HTMLElement|Element|Window|HTMLDocument} src * @param {string} eventName * @param {Function} listener * @param {boolean} capture * * @constructor * @implements {base.Disposable} */ base.DomEventHook = function(src, eventName, listener, capture) { this.src_ = src; this.eventName_ = eventName; this.listener_ = listener; this.capture_ = capture; src.addEventListener(eventName, listener, capture); }; base.DomEventHook.prototype.dispose = function() { this.src_.removeEventListener(this.eventName_, this.listener_, this.capture_); }; /** * An event hook implementation for Chrome Events. * * @param {ChromeEvent|chrome.contextMenus.ClickedEvent|ChromeObjectEvent} src * @param {!Function} listener * * @constructor * @implements {base.Disposable} */ base.ChromeEventHook = function(src, listener) { this.src_ = src; this.listener_ = listener; src.addListener(listener); }; base.ChromeEventHook.prototype.dispose = function() { this.src_.removeListener(this.listener_); }; /** * A disposable repeating timer. * * @param {Function} callback * @param {number} interval * @param {boolean=} opt_invokeNow Whether to invoke the callback now, default * to false. * * @constructor * @implements {base.Disposable} */ base.RepeatingTimer = function(callback, interval, opt_invokeNow) { /** @private */ this.intervalId_ = window.setInterval(callback, interval); if (opt_invokeNow) { callback(); } }; base.RepeatingTimer.prototype.dispose = function() { window.clearInterval(this.intervalId_); this.intervalId_ = null; }; /** * A disposable one shot timer. * * @param {Function} callback * @param {number} timeout * * @constructor * @implements {base.Disposable} */ base.OneShotTimer = function(callback, timeout) { var that = this; /** @private */ this.timerId_ = window.setTimeout(function() { that.timerId_ = null; callback(); }, timeout); }; base.OneShotTimer.prototype.dispose = function() { if (this.timerId_ !== null) { window.clearTimeout(this.timerId_); this.timerId_ = null; } }; /** * Converts UTF-8 string to ArrayBuffer. * * @param {string} string * @return {!ArrayBuffer} */ base.encodeUtf8 = function(string) { var utf8String = unescape(encodeURIComponent(string)); var result = new Uint8Array(utf8String.length); for (var i = 0; i < utf8String.length; i++) result[i] = utf8String.charCodeAt(i); return result.buffer; }; /** * Decodes UTF-8 string from ArrayBuffer. * * @param {ArrayBuffer} buffer * @return {string} */ base.decodeUtf8 = function(buffer) { return decodeURIComponent( escape(String.fromCharCode.apply(null, new Uint8Array(buffer)))); }; /** * Generate a nonce, to be used as an xsrf protection token. * * @return {string} A URL-Safe Base64-encoded 128-bit random value. */ base.generateXsrfToken = function() { var random = new Uint8Array(16); window.crypto.getRandomValues(random); var base64Token = window.btoa(String.fromCharCode.apply(null, random)); return base64Token.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); }; /** * @return {string} A random UUID. */ base.generateUuid = function() { var random = new Uint16Array(8); window.crypto.getRandomValues(random); /** @type {Array} */ var e = new Array(); for (var i = 0; i < 8; i++) { e[i] = (/** @type {number} */ (random[i]) + 0x10000). toString(16).substring(1); } return e[0] + e[1] + '-' + e[2] + '-' + e[3] + '-' + e[4] + '-' + e[5] + e[6] + e[7]; }; /** * @param {string} jsonString A JSON-encoded string. * @return {Object|undefined} The decoded object, or undefined if the string * cannot be parsed. */ base.jsonParseSafe = function(jsonString) { try { return /** @type {Object} */ (JSON.parse(jsonString)); } catch (err) { return undefined; } }; /** * Return the current time as a formatted string suitable for logging. * * @return {string} The current time, formatted as the standard ISO string. * [yyyy-mm-ddDhh:mm:ss.xyz] */ base.timestamp = function() { return '[' + new Date().toISOString() + ']'; }; /** * A online function that can be stubbed by unit tests. * @return {boolean} */ base.isOnline = function() { return navigator.onLine; }; /** * Size the current window to fit its content. * @param {boolean=} opt_centerWindow If true, position the window in the * center of the screen after resizing it. */ base.resizeWindowToContent = function(opt_centerWindow) { var appWindow = chrome.app.window.current(); var borderX = appWindow.outerBounds.width - appWindow.innerBounds.width; var borderY = appWindow.outerBounds.height - appWindow.innerBounds.height; var width = Math.ceil(document.documentElement.scrollWidth + borderX); var height = Math.ceil(document.documentElement.scrollHeight + borderY); appWindow.outerBounds.width = width; appWindow.outerBounds.height = height; if (opt_centerWindow) { var screenWidth = screen.availWidth; var screenHeight = screen.availHeight; appWindow.outerBounds.left = Math.round((screenWidth - width) / 2); appWindow.outerBounds.top = Math.round((screenHeight - height) / 2); } }; /** * @return {boolean} Whether NaCL is enabled in chrome://plugins. */ base.isNaclEnabled = function() { for (var i = 0; i < navigator.mimeTypes.length; i++) { if (navigator.mimeTypes.item(i).type == 'application/x-pnacl') { return true; } } return false; }; /** * Alias for document.getElementById that returns an HTMLElement * @param {string} id The ID of the element to find. * @return {?HTMLElement} The found element or null if not found. */ base.getHtmlElement = function(id) { var el = document.getElementById(id); if (el) console.assert(el instanceof HTMLElement); return /** @type {HTMLElement} */(el); };