/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* global Components */ var loop = loop || {}; loop.shared = loop.shared || {}; var inChrome = typeof Components != "undefined" && "utils" in Components; (function() { "use strict"; /** * Root object, by default set to window if we're not in chrome code. * @type {DOMWindow|Object} */ var rootObject = inChrome ? {} : window; /** * Root navigator, by default set to navigator if we're not in chrome code. * @type {Navigator|Object} */ var rootNavigator = inChrome ? {} : navigator; /** * Sets new root objects. This is useful for testing native DOM events, and also * so we can easily stub items like navigator.mediaDevices. * can fake them. In beforeEach(), loop.shared.utils.setRootObjects is used to * substitute fake objects, and in afterEach(), the real window object is * replaced. * * @param {Object} windowObj The fake window object, undefined to use window. * @param {Object} navigatorObj The fake navigator object, undefined to use navigator. */ function setRootObjects(windowObj, navigatorObj) { rootObject = windowObj || window; rootNavigator = navigatorObj || navigator; } var mozL10n; if (inChrome) { this.EXPORTED_SYMBOLS = ["utils"]; mozL10n = { get: function() { throw new Error("mozL10n.get not availabled from chrome!"); }}; } else { mozL10n = document.mozL10n || navigator.mozL10n; } /** * Call types used for determining if a call is audio/video or audio-only. */ var CALL_TYPES = { AUDIO_VIDEO: "audio-video", AUDIO_ONLY: "audio" }; var REST_ERRNOS = { INVALID_TOKEN: 105, EXPIRED: 111, USER_UNAVAILABLE: 122, ROOM_FULL: 202 }; var WEBSOCKET_REASONS = { ANSWERED_ELSEWHERE: "answered-elsewhere", BUSY: "busy", CANCEL: "cancel", CLOSED: "closed", MEDIA_FAIL: "media-fail", REJECT: "reject", TIMEOUT: "timeout" }; var FAILURE_DETAILS = { MEDIA_DENIED: "reason-media-denied", NO_MEDIA: "reason-no-media", UNABLE_TO_PUBLISH_MEDIA: "unable-to-publish-media", USER_UNAVAILABLE: "reason-user-unavailable", COULD_NOT_CONNECT: "reason-could-not-connect", NETWORK_DISCONNECTED: "reason-network-disconnected", EXPIRED_OR_INVALID: "reason-expired-or-invalid", // TOS_FAILURE reflects the sdk error code 1026: // https://tokbox.com/developer/sdks/js/reference/ExceptionEvent.html TOS_FAILURE: "reason-tos-failure", UNKNOWN: "reason-unknown" }; var ROOM_INFO_FAILURES = { // There's no data available from the server. NO_DATA: "no_data", // WebCrypto is unsupported in this browser. WEB_CRYPTO_UNSUPPORTED: "web_crypto_unsupported", // The room is missing the crypto key information. NO_CRYPTO_KEY: "no_crypto_key", // Decryption failed. DECRYPT_FAILED: "decrypt_failed" }; var STREAM_PROPERTIES = { VIDEO_DIMENSIONS: "videoDimensions", HAS_AUDIO: "hasAudio", HAS_VIDEO: "hasVideo" }; var SCREEN_SHARE_STATES = { INACTIVE: "ss-inactive", // Pending is when the user is being prompted, aka gUM in progress. PENDING: "ss-pending", ACTIVE: "ss-active" }; /** * Format a given date into an l10n-friendly string. * * @param {Integer} The timestamp in seconds to format. * @return {String} The formatted string. */ function formatDate(timestamp) { var date = (new Date(timestamp * 1000)); var options = {year: "numeric", month: "long", day: "numeric"}; return date.toLocaleDateString(navigator.language, options); } /** * Used for getting a boolean preference. It will either use the browser preferences * (if navigator.mozLoop is defined) or try to get them from localStorage. * * @param {String} prefName The name of the preference. Note that mozLoop adds * 'loop.' to the start of the string. * * @return The value of the preference, or false if not available. */ function getBoolPreference(prefName) { if (navigator.mozLoop) { return !!navigator.mozLoop.getLoopPref(prefName); } return !!localStorage.getItem(prefName); } function isChrome(platform) { return platform.toLowerCase().indexOf("chrome") > -1 || platform.toLowerCase().indexOf("chromium") > -1; } function isFirefox(platform) { return platform.toLowerCase().indexOf("firefox") !== -1; } function isOpera(platform) { return platform.toLowerCase().indexOf("opera") > -1 || platform.toLowerCase().indexOf("opr") > -1; } /** * Helper to get the platform if it is unsupported. * * @param {String} platform The platform this is running on. * @return null for supported platforms, a string for unsupported platforms. */ function getUnsupportedPlatform(platform) { if (/^(iPad|iPhone|iPod)/.test(platform)) { return "ios"; } if (/Windows Phone/i.test(platform)) { return "windows_phone"; } if (/BlackBerry/i.test(platform)) { return "blackberry"; } return null; } /** * Helper to get the Operating System name. * * @param {String} [platform] The platform this is running on, will fall * back to navigator.oscpu and navigator.userAgent * respectively if not supplied. * @param {Boolean} [withVersion] Optional flag to keep the version number * included in the resulting string. Defaults to * `false`. * @return {String} The platform we're currently running on, in lower-case. */ var getOS = function(platform, withVersion) { if (!platform) { if ("oscpu" in window.navigator) { // See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/oscpu platform = window.navigator.oscpu.split(";")[0].trim(); } else { // Fall back to navigator.userAgent as a last resort. platform = window.navigator.userAgent; } } if (!platform) { return "unknown"; } // Support passing in navigator.userAgent. var platformPart = platform.match(/\((.*)\)/); if (platformPart) { platform = platformPart[1]; } platform = platform.toLowerCase().split(";"); if (/macintosh/.test(platform[0]) || /x11/.test(platform[0])) { platform = platform[1]; } else { if (platform[0].indexOf("win") > -1 && platform.length > 4) { // Skip the security notation. platform = platform[2]; } else { platform = platform[0]; } } if (!withVersion) { platform = platform.replace(/\s[0-9.]+/g, ""); } return platform.trim(); }; /** * Helper to get the Operating System version. * See http://en.wikipedia.org/wiki/Windows_NT for a table of Windows NT * versions. * * @param {String} [platform] The platform this is running on, will fall back * to navigator.oscpu and navigator.userAgent * respectively if not supplied. * @return {String} The current version of the platform we're currently running * on. */ var getOSVersion = function(platform) { var os = getOS(platform, true); var digitsRE = /\s([0-9.]+)/; var version = os.match(digitsRE); if (!version) { if (os.indexOf("win") > -1) { if (os.indexOf("xp")) { return { major: 5, minor: 2 }; } else if (os.indexOf("vista") > -1) { return { major: 6, minor: 0 }; } } } else { version = version[1]; // Windows versions have an interesting scheme. if (os.indexOf("win") > -1) { switch (parseFloat(version)) { case 98: return { major: 4, minor: 1 }; case 2000: return { major: 5, minor: 0 }; case 2003: return { major: 5, minor: 2 }; case 7: case 2008: case 2011: return { major: 6, minor: 1 }; case 8: return { major: 6, minor: 2 }; case 8.1: case 2012: return { major: 6, minor: 3 }; } } version = version.split("."); return { major: parseInt(version[0].trim(), 10), minor: parseInt(version[1] ? version[1].trim() : 0, 10) }; } return { major: Infinity, minor: 0 }; }; /** * Helper to get the current short platform string, based on the return value * of `getOS`. * Possible return values are 'mac', 'win' or 'other'. * * @param {String} [os] Optional string for the OS, used in tests only. * @return {String} 'mac', 'win' or 'other'. */ var getPlatform = function(os) { os = getOS(os); var platform = "other"; if (os.indexOf("mac") > -1) { platform = "mac"; } else if (os.indexOf("win") > -1) { platform = "win"; } return platform; }; /** * Determines if the user has any audio devices installed. * * @param {Function} callback Called with a boolean which is true if there * are audio devices present. */ function hasAudioOrVideoDevices(callback) { // mediaDevices is the official API for the spec. // Older versions of FF had mediaDevices but not enumerateDevices. if ("mediaDevices" in rootNavigator && "enumerateDevices" in rootNavigator.mediaDevices) { rootNavigator.mediaDevices.enumerateDevices().then(function(result) { function checkForInput(device) { return device.kind === "audioinput" || device.kind === "videoinput"; } callback(result.some(checkForInput)); }).catch(function() { callback(false); }); // MediaStreamTrack is the older version of the API, implemented originally // by Google Chrome. } else if ("MediaStreamTrack" in rootObject && "getSources" in rootObject.MediaStreamTrack) { rootObject.MediaStreamTrack.getSources(function(result) { function checkForInput(device) { return device.kind === "audio" || device.kind === "video"; } callback(result.some(checkForInput)); }); } else { // We don't know, so assume true. callback(true); } } /** * Helper to allow getting some of the location data in a way that's compatible * with stubbing for unit tests. */ function locationData() { return { hash: window.location.hash, pathname: window.location.pathname }; } /** * Formats a url for display purposes. This includes converting the * domain to punycode, and then decoding the url. * * @param {String} url The url to format. * @return {Object} An object containing the hostname and full location. */ function formatURL(url) { // We're using new URL to pass this through the browser's ACE/punycode // processing system. If the browser considers a url to need to be // punycode encoded for it to be displayed, then new URL will do that for // us. This saves us needing our own punycode library. var urlObject; try { urlObject = new URL(url); } catch (ex) { console.error("Error occurred whilst parsing URL:", ex); return null; } // Finally, ensure we look good. return { hostname: urlObject.hostname, location: decodeURI(urlObject.href) }; } /** * Generates and opens a mailto: url with call URL information prefilled. * Note: This only works for Desktop. * * @param {String} callUrl The call URL. * @param {String} [recipient] The recipient email address (optional). * @param {String} [contextDescription] The context description (optional). * @param {String} [from] The area from which this function is called. */ function composeCallUrlEmail(callUrl, recipient, contextDescription, from) { var mozLoop = navigator.mozLoop; if (typeof mozLoop === "undefined") { console.warn("composeCallUrlEmail isn't available for Loop standalone."); return; } var subject, body; var footer = mozL10n.get("share_email_footer"); if (contextDescription) { subject = mozL10n.get("share_email_subject6"); body = mozL10n.get("share_email_body_context2", { callUrl: callUrl, title: contextDescription }); } else { subject = mozL10n.get("share_email_subject6"); body = mozL10n.get("share_email_body6", { callUrl: callUrl }); } var bodyFooter = body + footer; bodyFooter = bodyFooter.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n"); mozLoop.composeEmail( subject, bodyFooter, recipient ); var bucket = mozLoop.SHARING_ROOM_URL["EMAIL_FROM_" + (from || "").toUpperCase()]; if (typeof bucket === "undefined") { console.error("No URL sharing type bucket found for '" + from + "'"); return; } mozLoop.telemetryAddValue("LOOP_SHARING_ROOM_URL", bucket); } // We can alias `subarray` to `slice` when the latter is not available, because // they're semantically identical. if (!Uint8Array.prototype.slice) { /* eslint-disable */ // Eslint disabled for no-extend-native; Specific override needed for Firefox 37 // and earlier, also for other browsers. Uint8Array.prototype.slice = Uint8Array.prototype.subarray; /* eslint-enable */ } /** * Binary-compatible Base64 decoding. * * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding * * @param {String} base64str The string to decode. * @return {Uint8Array} The decoded result in array format. */ function atob(base64str) { var strippedEncoding = base64str.replace(/[^A-Za-z0-9\+\/]/g, ""); var inLength = strippedEncoding.length; var outLength = inLength * 3 + 1 >> 2; var result = new Uint8Array(outLength); var mod3; var mod4; var uint24 = 0; var outIndex = 0; for (var inIndex = 0; inIndex < inLength; inIndex++) { mod4 = inIndex & 3; uint24 |= _b64ToUint6(strippedEncoding.charCodeAt(inIndex)) << 6 * (3 - mod4); if (mod4 === 3 || inLength - inIndex === 1) { for (mod3 = 0; mod3 < 3 && outIndex < outLength; mod3++, outIndex++) { result[outIndex] = uint24 >>> (16 >>> mod3 & 24) & 255; } uint24 = 0; } } return result; } /** * Binary-compatible Base64 encoding. * * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding * * @param {Uint8Array} bytes The data to encode. * @return {String} The base64 encoded string. */ function btoa(bytes) { var mod3 = 2; var result = ""; var length = bytes.length; var uint24 = 0; for (var index = 0; index < length; index++) { mod3 = index % 3; if (index > 0 && (index * 4 / 3) % 76 === 0) { result += "\r\n"; } uint24 |= bytes[index] << (16 >>> mod3 & 24); if (mod3 === 2 || length - index === 1) { result += String.fromCharCode(_uint6ToB64(uint24 >>> 18 & 63), _uint6ToB64(uint24 >>> 12 & 63), _uint6ToB64(uint24 >>> 6 & 63), _uint6ToB64(uint24 & 63)); uint24 = 0; } } return result.substr(0, result.length - 2 + mod3) + (mod3 === 2 ? "" : mod3 === 1 ? "=" : "=="); } /** * Utility function to decode a base64 character into an integer. * * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding * * @param {Number} chr The character code to decode. * @return {Number} The decoded value. */ function _b64ToUint6 (chr) { return chr > 64 && chr < 91 ? chr - 65 : chr > 96 && chr < 123 ? chr - 71 : chr > 47 && chr < 58 ? chr + 4 : chr === 43 ? 62 : chr === 47 ? 63 : 0; } /** * Utility function to encode an integer into a base64 character code. * * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding * * @param {Number} uint6 The number to encode. * @return {Number} The encoded value. */ function _uint6ToB64 (uint6) { return uint6 < 26 ? uint6 + 65 : uint6 < 52 ? uint6 + 71 : uint6 < 62 ? uint6 - 4 : uint6 === 62 ? 43 : uint6 === 63 ? 47 : 65; } /** * Utility function to convert a string into a uint8 array. * * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding * * @param {String} inString The string to convert. * @return {Uint8Array} The converted string in array format. */ function strToUint8Array(inString) { var inLength = inString.length; var arrayLength = 0; var chr; // Mapping. for (var mapIndex = 0; mapIndex < inLength; mapIndex++) { chr = inString.charCodeAt(mapIndex); arrayLength += chr < 0x80 ? 1 : chr < 0x800 ? 2 : chr < 0x10000 ? 3 : chr < 0x200000 ? 4 : chr < 0x4000000 ? 5 : 6; } var result = new Uint8Array(arrayLength); var index = 0; // Transcription. for (var chrIndex = 0; index < arrayLength; chrIndex++) { chr = inString.charCodeAt(chrIndex); if (chr < 128) { // One byte. result[index++] = chr; } else if (chr < 0x800) { // Two bytes. result[index++] = 192 + (chr >>> 6); result[index++] = 128 + (chr & 63); } else if (chr < 0x10000) { // Three bytes. result[index++] = 224 + (chr >>> 12); result[index++] = 128 + (chr >>> 6 & 63); result[index++] = 128 + (chr & 63); } else if (chr < 0x200000) { // Four bytes. result[index++] = 240 + (chr >>> 18); result[index++] = 128 + (chr >>> 12 & 63); result[index++] = 128 + (chr >>> 6 & 63); result[index++] = 128 + (chr & 63); } else if (chr < 0x4000000) { // Five bytes. result[index++] = 248 + (chr >>> 24); result[index++] = 128 + (chr >>> 18 & 63); result[index++] = 128 + (chr >>> 12 & 63); result[index++] = 128 + (chr >>> 6 & 63); result[index++] = 128 + (chr & 63); } else { // if (chr <= 0x7fffffff) // Six bytes. result[index++] = 252 + (chr >>> 30); result[index++] = 128 + (chr >>> 24 & 63); result[index++] = 128 + (chr >>> 18 & 63); result[index++] = 128 + (chr >>> 12 & 63); result[index++] = 128 + (chr >>> 6 & 63); result[index++] = 128 + (chr & 63); } } return result; } /** * Utility function to change a uint8 based integer array to a string. * * Taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding * * @param {Uint8Array} arrayBytes Array to convert. * @param {String} The array as a string. */ function Uint8ArrayToStr(arrayBytes) { var result = ""; var length = arrayBytes.length; var part; for (var index = 0; index < length; index++) { part = arrayBytes[index]; result += String.fromCharCode( part > 251 && part < 254 && index + 5 < length ? // Six bytes. // (part - 252 << 30) may be not so safe in ECMAScript! So...: (part - 252) * 1073741824 + (arrayBytes[++index] - 128 << 24) + (arrayBytes[++index] - 128 << 18) + (arrayBytes[++index] - 128 << 12) + (arrayBytes[++index] - 128 << 6) + arrayBytes[++index] - 128 : part > 247 && part < 252 && index + 4 < length ? // Five bytes. (part - 248 << 24) + (arrayBytes[++index] - 128 << 18) + (arrayBytes[++index] - 128 << 12) + (arrayBytes[++index] - 128 << 6) + arrayBytes[++index] - 128 : part > 239 && part < 248 && index + 3 < length ? // Four bytes. (part - 240 << 18) + (arrayBytes[++index] - 128 << 12) + (arrayBytes[++index] - 128 << 6) + arrayBytes[++index] - 128 : part > 223 && part < 240 && index + 2 < length ? // Three bytes. (part - 224 << 12) + (arrayBytes[++index] - 128 << 6) + arrayBytes[++index] - 128 : part > 191 && part < 224 && index + 1 < length ? // Two bytes. (part - 192 << 6) + arrayBytes[++index] - 128 : // One byte. part ); } return result; } /** * Get the difference after comparing two different objects. It compares property * names and their respective values if necessary. * This function does _not_ recurse into object values to keep this functions' * complexity predictable to O(2). * * @param {Object} a Object number 1, the comparator. * @param {Object} b Object number 2, the comparison. * @return {Object} The diff output, which is itself an object structured as: * { * updated: [prop1, prop6], * added: [prop2], * removed: [prop3] * } */ function objectDiff(a, b) { var propsA = a ? Object.getOwnPropertyNames(a) : []; var propsB = b ? Object.getOwnPropertyNames(b) : []; var diff = { updated: [], added: [], removed: [] }; var prop; for (var i = 0, lA = propsA.length; i < lA; ++i) { prop = propsA[i]; if (propsB.indexOf(prop) === -1) { diff.removed.push(prop); } else if (a[prop] !== b[prop]) { diff.updated.push(prop); } } for (var j = 0, lB = propsB.length; j < lB; ++j) { prop = propsB[j]; if (propsA.indexOf(prop) === -1) { diff.added.push(prop); } } return diff; } /** * When comparing two object, you sometimes want to ignore falsy values when * they're not persisted on the server, for example. * This function removes all the empty/ falsy properties from the target object. * * @param {Object} obj Target object to strip the falsy properties from * @return {Object} */ function stripFalsyValues(obj) { var props = Object.getOwnPropertyNames(obj); var prop; for (var i = props.length; i >= 0; --i) { prop = props[i]; // If the value of the object property evaluates to |false|, delete it. if (!obj[prop]) { delete obj[prop]; } } return obj; } /** * Truncate a string if it exceeds the length as defined in `maxLen`, which * is defined as '72' characters by default. If the string needs trimming, * it'll be suffixed with the unicode ellipsis char, \u2026. * * @param {String} str The string to truncate, if needed. * @param {Number} maxLen Maximum number of characters that the string is * allowed to contain. Optional, defaults to 72. * @return {String} Truncated version of `str`. */ function truncate(str, maxLen) { maxLen = maxLen || 72; if (str.length > maxLen) { var substring = str.substr(0, maxLen); // XXX Due to the fact that we have two different l10n libraries. var direction = mozL10n.getDirection ? mozL10n.getDirection() : mozL10n.language.direction; if (direction === "rtl") { return "…" + substring; } return substring + "…"; } return str; } /** * Look up the DOM hierarchy for a node matching `selector`. * If it is not found return the parent node, this is a sane default so * that subsequent queries on the result do no fail. * Better choice than the alternative `document.querySelector(selector)` * because we ensure it works in the UI showcase as well. * * @param {HTMLElement} node Child element of the node we are looking for. * @param {String} selector CSS class value of element we are looking for. * @return {HTMLElement} Parent of node that matches selector query. */ function findParentNode(node, selector) { var parentNode = node.parentNode; while (parentNode) { if (parentNode.classList.contains(selector)) { return parentNode; } parentNode = parentNode.parentNode; } return node; } this.utils = { CALL_TYPES: CALL_TYPES, FAILURE_DETAILS: FAILURE_DETAILS, REST_ERRNOS: REST_ERRNOS, WEBSOCKET_REASONS: WEBSOCKET_REASONS, STREAM_PROPERTIES: STREAM_PROPERTIES, SCREEN_SHARE_STATES: SCREEN_SHARE_STATES, ROOM_INFO_FAILURES: ROOM_INFO_FAILURES, setRootObjects: setRootObjects, composeCallUrlEmail: composeCallUrlEmail, findParentNode: findParentNode, formatDate: formatDate, formatURL: formatURL, getBoolPreference: getBoolPreference, getOS: getOS, getOSVersion: getOSVersion, getPlatform: getPlatform, isChrome: isChrome, isFirefox: isFirefox, isOpera: isOpera, getUnsupportedPlatform: getUnsupportedPlatform, hasAudioOrVideoDevices: hasAudioOrVideoDevices, locationData: locationData, atob: atob, btoa: btoa, strToUint8Array: strToUint8Array, Uint8ArrayToStr: Uint8ArrayToStr, objectDiff: objectDiff, stripFalsyValues: stripFalsyValues, truncate: truncate }; }).call(inChrome ? this : loop.shared);