UNPKG

voluptasmollitia

Version:
1,514 lines (1,341 loc) 50.5 kB
/** * @license * Copyright 2017 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @fileoverview Defines utility and helper functions. */ goog.provide('fireauth.util'); goog.require('goog.Promise'); goog.require('goog.Timer'); goog.require('goog.Uri'); goog.require('goog.dom'); goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('goog.html.SafeUrl'); goog.require('goog.json'); goog.require('goog.object'); goog.require('goog.string'); goog.require('goog.userAgent'); goog.require('goog.window'); /** @suppress {duplicate} Suppress variable 'angular' first declared. */ var angular; /** * Checks whether the user agent is IE11. * @return {boolean} True if it is IE11. */ fireauth.util.isIe11 = function() { return goog.userAgent.IE && !!goog.userAgent.DOCUMENT_MODE && goog.userAgent.DOCUMENT_MODE == 11; }; /** * Checks whether the user agent is IE10. * @return {boolean} True if it is IE10. */ fireauth.util.isIe10 = function() { return goog.userAgent.IE && !!goog.userAgent.DOCUMENT_MODE && goog.userAgent.DOCUMENT_MODE == 10; }; /** * Checks whether the user agent is Edge. * @param {string} userAgent The browser user agent string. * @return {boolean} True if it is Edge. */ fireauth.util.isEdge = function(userAgent) { return /Edge\/\d+/.test(userAgent); }; /** * @param {?string=} opt_userAgent The navigator user agent. * @return {boolean} Whether local storage is not synchronized between an iframe * and a popup of the same domain. */ fireauth.util.isLocalStorageNotSynchronized = function(opt_userAgent) { var ua = opt_userAgent || fireauth.util.getUserAgentString(); return fireauth.util.isIe11() || fireauth.util.isEdge(ua); }; /** @return {string} The current URL. */ fireauth.util.getCurrentUrl = function() { return (goog.global['window'] && goog.global['window']['location']['href']) || // Check for worker environments. (self && self['location'] && self['location']['href']) || ''; }; /** * @param {string} requestUri The request URI to send in verifyAssertion * request. * @return {string} The sanitized URI, in this case it undoes the hashbang * angularJs routing changes to request URI. */ fireauth.util.sanitizeRequestUri = function(requestUri) { // If AngularJS is included. if (typeof angular != 'undefined') { // Remove hashbang modifications from URL. requestUri = requestUri.replace('#/', '#').replace('#!/', '#'); } return requestUri; }; /** * @param {?string} url The target URL. When !url, redirects to a blank page. * @param {!Window=} opt_window The optional window to redirect to target URL. * @param {boolean=} opt_bypassCheck Whether to bypass check. Used for custom * scheme redirects. */ fireauth.util.goTo = function(url, opt_window, opt_bypassCheck) { var win = opt_window || goog.global['window']; // No URL, redirect to blank page. var finalUrl = 'about:blank'; // Popping up a window and then assigning its URL seems to cause some weird // error. Fixed by setting win.location.href for now in IE browsers. // Bug was detected in Edge and IE9. if (url && !opt_bypassCheck) { // We cannot use goog.dom.safe.setLocationHref since it tries to read // popup.location from a different origin, which is an error in IE. // (In Chrome, popup.location is just an empty Location object) finalUrl = goog.html.SafeUrl.unwrap(goog.html.SafeUrl.sanitize(url)); } win.location.href = finalUrl; }; /** * @param {string} url The target URL. * @param {!Window=} opt_window The optional window to replace with target URL. * @param {boolean=} opt_bypassCheck Whether to bypass check. Used for custom * scheme redirects. */ fireauth.util.replaceCurrentUrl = function(url, opt_window, opt_bypassCheck) { var win = opt_window || goog.global['window']; if (!opt_bypassCheck) { win.location.replace( goog.html.SafeUrl.unwrap(goog.html.SafeUrl.sanitize(url))); } else { win.location.replace(url); } }; /** * Deep comparison of two objects. * @param {!Object} a The first object. * @param {!Object} b The second object. * @return {!Array<string>} The list of keys that are different between both * objects provided. */ fireauth.util.getKeyDiff = function(a, b) { var diff = []; for (var k in a) { if (!(k in b)) { diff.push(k); } else if (typeof a[k] != typeof b[k]) { diff.push(k); } else if (typeof a[k] == 'object' && a[k] != null && b[k] != null) { if (fireauth.util.getKeyDiff( a[k], b[k]).length > 0) { diff.push(k); } } else if (a[k] !== b[k]) { diff.push(k); } } for (var k in b) { if (!(k in a)) { diff.push(k); } } return diff; }; /** * @param {?string=} opt_userAgent The navigator user agent. * @return {?number} The Chrome version, null if the user agent is not Chrome. */ fireauth.util.getChromeVersion = function(opt_userAgent) { var ua = opt_userAgent || fireauth.util.getUserAgentString(); var browserName = fireauth.util.getBrowserName(ua); // Confirm current browser is Chrome. if (browserName != fireauth.util.BrowserName.CHROME) { return null; } var matches = ua.match(/\sChrome\/(\d+)/i); if (matches && matches.length == 2) { return parseInt(matches[1], 10); } return null; }; /** * Detects CORS support. * @param {?string=} opt_userAgent The navigator user agent. * @return {boolean} True if the browser supports CORS. */ fireauth.util.supportsCors = function(opt_userAgent) { // Chrome 7 has CORS issues, pick 30 as upper limit. var chromeVersion = fireauth.util.getChromeVersion(opt_userAgent); if (chromeVersion && chromeVersion < 30) { return false; } // Among all other supported browsers, only IE8 and IE9 don't support CORS. return !goog.userAgent.IE || // Not IE. !goog.userAgent.DOCUMENT_MODE || // No document mode == IE Edge. goog.userAgent.DOCUMENT_MODE > 9; }; /** * Detects whether browser is running on a mobile device. * @param {?string=} opt_userAgent The navigator user agent. * @return {boolean} True if the browser is running on a mobile device. */ fireauth.util.isMobileBrowser = function(opt_userAgent) { var ua = opt_userAgent || fireauth.util.getUserAgentString(); var uaLower = ua.toLowerCase(); // TODO: implement getBrowserName equivalent for OS. if (uaLower.match(/android/) || uaLower.match(/webos/) || uaLower.match(/iphone|ipad|ipod/) || uaLower.match(/blackberry/) || uaLower.match(/windows phone/) || uaLower.match(/iemobile/)) { return true; } return false; }; /** * Closes the provided window. * @param {?Window=} opt_window The optional window to close. The current window * is used if none is provided. */ fireauth.util.closeWindow = function(opt_window) { var win = opt_window || goog.global['window']; // In some browsers, in certain cases after the window closes, as seen in // Samsung Galaxy S3 Android 4.4.2 stock browser, the win object here is an // empty object {}. Try to catch the failure and ignore it. try { win.close(); } catch(e) {} }; /** * Opens a popup window. * @param {?string=} opt_url initial URL of the popup window * @param {string=} opt_name title of the popup * @param {?number=} opt_width width of the popup * @param {?number=} opt_height height of the popup * @return {?Window} Returns the window object that was opened. This returns * null if a popup blocker prevented the window from being * opened. */ fireauth.util.popup = function(opt_url, opt_name, opt_width, opt_height) { var width = opt_width || 500; var height = opt_height || 600; var top = (window.screen.availHeight - height) / 2; var left = (window.screen.availWidth - width) / 2; var options = { 'width': width, 'height': height, 'top': top > 0 ? top : 0, 'left': left > 0 ? left : 0, 'location': true, 'resizable': true, 'statusbar': true, 'toolbar': false }; // Chrome iOS 7 and 8 is returning an undefined popup win when target is // specified, even though the popup is not necessarily blocked. var ua = fireauth.util.getUserAgentString().toLowerCase(); if (opt_name) { options['target'] = opt_name; // This will force a new window on each call, achieving the same effect as // passing a random name on each call. if (goog.string.contains(ua, 'crios/')) { options['target'] = '_blank'; } } var browserName = fireauth.util.getBrowserName( fireauth.util.getUserAgentString()); if (browserName == fireauth.util.BrowserName.FIREFOX) { // Firefox complains when invalid URLs are popped out. Hacky way to bypass. opt_url = opt_url || 'http://localhost'; // Firefox disables by default scrolling on popup windows, which can create // issues when the user has many Google accounts, for instance. options['scrollbars'] = true; } // about:blank getting sanitized causing browsers like IE/Edge to display // brief error message before redirecting to handler. var newWin = goog.window.open(opt_url || '', options); if (newWin) { // Flaky on IE edge, encapsulate with a try and catch. try { newWin.focus(); } catch (e) {} } return newWin; }; /** * The default value for the popup wait cycle in ms. * @const {number} * @private */ fireauth.util.POPUP_WAIT_CYCLE_MS_ = 2000; /** * @param {?string=} opt_userAgent The optional user agent. * @return {boolean} Whether the popup requires a delay before closing itself. */ fireauth.util.requiresPopupDelay = function(opt_userAgent) { // TODO: remove this hack when CriOS behavior is fixed in iOS. var ua = opt_userAgent || fireauth.util.getUserAgentString(); // Was observed in iOS 10.2 Chrome version 55.0.2883.79. // Apply to Chrome 55+ iOS 10+ to ensure future Chrome versions or iOS 10 // minor updates do not suddenly resurface this bug. Revisit this check on // next CriOS update. var matches = ua.match(/OS (\d+)_.*CriOS\/(\d+)\./i); if (matches && matches.length > 2) { // iOS 10+ && chrome 55+. return parseInt(matches[1], 10) >= 10 && parseInt(matches[2], 10) >= 55; } return false; }; /** * @param {?Window} win The popup window to check. * @param {number=} opt_stepDuration The duration of each wait cycle before * checking that window is closed. * @return {!goog.Promise<undefined>} The promise to resolve when window is * closed. */ fireauth.util.onPopupClose = function(win, opt_stepDuration) { var stepDuration = opt_stepDuration || fireauth.util.POPUP_WAIT_CYCLE_MS_; return new goog.Promise(function(resolve, reject) { // Function to repeat each stepDuration. var repeat = function() { goog.Timer.promise(stepDuration).then(function() { // After wait, check if window is closed. if (!win || win.closed) { // If so, resolve. resolve(); } else { // Call repeat again. return repeat(); } }); }; return repeat(); }); }; /** * @param {!Array<string>} authorizedDomains List of authorized domains. * @param {string} url The URL to check. * @return {boolean} Whether the passed domain is an authorized one. */ fireauth.util.isAuthorizedDomain = function(authorizedDomains, url) { var uri = goog.Uri.parse(url); var scheme = uri.getScheme(); var domain = uri.getDomain(); for (var i = 0; i < authorizedDomains.length; i++) { // Currently this corresponds to: domain.com = *://*.domain.com:* or // exact domain match. // In the case of Chrome extensions, the authorizedDomain will be formatted // as 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456'. // The URL to check must have a chrome extension scheme and the domain // must be an exact match domain == 'abcdefghijklmnopqrstuvwxyz123456'. if (fireauth.util.matchDomain(authorizedDomains[i], domain, scheme)) { return true; } } return false; }; /** * Represents the dimensions of an entity (width and height). * @typedef {{ * width: number, * height: number * }} */ fireauth.util.Dimensions; /** * @param {?Window=} opt_window The optional window whose dimensions are to be * returned. The current window is used if not found. * @return {?fireauth.util.Dimensions} The requested window dimensions if * available. */ fireauth.util.getWindowDimensions = function(opt_window) { var win = opt_window || goog.global['window']; if (win && win['innerWidth'] && win['innerHeight']) { return { 'width': parseFloat(win['innerWidth']), 'height': parseFloat(win['innerHeight']) }; } return null; }; /** * RegExp to detect if the domain given is an IP address. This is only used * for validating http and https schemes. * * It does not strictly validate if the IP is a real IP address, but as the * matchDomain method tests against a set of valid domains (extracted from the * window's current URL), it is sufficient. * * @const {!RegExp} * @private */ fireauth.util.IP_ADDRESS_REGEXP_ = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; /** * @param {string} domainPattern The domain pattern to match. * @param {string} domain The domain to check. It is assumed that it is a valid * domain, not a user provided one. * @param {string} scheme The scheme of the domain to check. * @return {boolean} Whether the provided domain matches the domain pattern. */ fireauth.util.matchDomain = function(domainPattern, domain, scheme) { // Chrome extension matching. if (domainPattern.indexOf('chrome-extension://') == 0) { var chromeExtUri = goog.Uri.parse(domainPattern); // Domain must match and the current scheme must be a Chrome extension. return chromeExtUri.getDomain() == domain && scheme == 'chrome-extension'; } else if (scheme != 'http' && scheme != 'https') { // Any other scheme that is not http or https cannot be whitelisted. return false; } else { // domainPattern must not contain a scheme and the current scheme must be // either http or https. // Check if authorized domain pattern is an IP address. if (fireauth.util.IP_ADDRESS_REGEXP_.test(domainPattern)) { // The domain has to be exactly equal to the pattern, as an IP domain will // only contain the IP, no extra character. return domain == domainPattern; } // Dots in pattern should be escaped. var escapedDomainPattern = domainPattern.split('.').join('\\.'); // Non ip address domains. // domain.com = *.domain.com OR domain.com var re = new RegExp( '^(.+\\.' + escapedDomainPattern + '|' + escapedDomainPattern + ')$', 'i'); return re.test(domain); } }; /** * RegExp to detect if the email address given is valid. * @const {!RegExp} * @private */ fireauth.util.EMAIL_ADDRESS_REGEXP_ = /^[^@]+@[^@]+$/; /** * Determines if it is a valid email address. * @param {*} email The email address. * @return {boolean} Whether the email address is valid. */ fireauth.util.isValidEmailAddress = function(email) { return typeof email === 'string' && fireauth.util.EMAIL_ADDRESS_REGEXP_.test(email); }; /** * @return {!goog.Promise<void>} A promise that resolves when DOM is ready. */ fireauth.util.onDomReady = function() { var resolver = null; return new goog.Promise(function(resolve, reject) { var doc = goog.global.document; // If document already loaded, resolve immediately. if (doc.readyState == 'complete') { resolve(); } else { // Document not ready, wait for load before resolving. // Save resolver, so we can remove listener in case it was externally // cancelled. resolver = function() { resolve(); }; goog.events.listenOnce(window, goog.events.EventType.LOAD, resolver); } }).thenCatch(function(error) { // In case this promise was cancelled, make sure it unlistens to load. goog.events.unlisten(window, goog.events.EventType.LOAD, resolver); throw error; }); }; /** @return {boolean} Whether environment supports DOM. */ fireauth.util.isDOMSupported = function() { return !!goog.global.document; }; /** * The default ondeviceready Cordova timeout in ms. * @const {number} * @private */ fireauth.util.CORDOVA_ONDEVICEREADY_TIMEOUT_MS_ = 1000; /** * @param {?string=} opt_userAgent The optional user agent. * @param {number=} opt_timeout The optional timeout in ms for deviceready * event to resolve. * @return {!goog.Promise} A promise that resolves if the current environment is * a Cordova environment. */ fireauth.util.checkIfCordova = function(opt_userAgent, opt_timeout) { // Errors generated are internal and should be converted if needed to // developer facing Firebase errors. // Only supported in Android/iOS environment. if (fireauth.util.isAndroidOrIosCordovaScheme(opt_userAgent)) { return fireauth.util.onDomReady().then(function() { return new goog.Promise(function(resolve, reject) { var doc = goog.global.document; var timeoutId = setTimeout(function() { reject(new Error('Cordova framework is not ready.')); }, opt_timeout || fireauth.util.CORDOVA_ONDEVICEREADY_TIMEOUT_MS_); // This should resolve immediately after DOM ready. doc.addEventListener('deviceready', function() { clearTimeout(timeoutId); resolve(); }, false); }); }); } return goog.Promise.reject( new Error('Cordova must run in an Android or iOS file scheme.')); }; /** * @param {?string=} opt_userAgent The optional user agent. * @return {boolean} Whether the app is rendered in a mobile iOS or Android * Cordova environment. */ fireauth.util.isAndroidOrIosCordovaScheme = function(opt_userAgent) { var ua = opt_userAgent || fireauth.util.getUserAgentString(); return !!((fireauth.util.getCurrentScheme() === 'file:' || fireauth.util.getCurrentScheme() === 'ionic:') && ua.toLowerCase().match(/iphone|ipad|ipod|android/)); }; /** * @param {?string=} opt_userAgent The optional user agent. * @return {boolean} Whether the app is rendered in a mobile iOS 7 or 8 browser. */ fireauth.util.isIOS7Or8 = function(opt_userAgent) { var ua = opt_userAgent || fireauth.util.getUserAgentString(); return !!(ua.match(/(iPad|iPhone|iPod).*OS 7_\d/i) || ua.match(/(iPad|iPhone|iPod).*OS 8_\d/i)); }; /** * @return {boolean} Whether browser is Safari or an iOS browser and page is * embedded in an iframe. Local Storage does not synchronize with an iframe * embedded on a page in a different domain but will still trigger storage * event with storage changes. */ fireauth.util.isSafariLocalStorageNotSynced = function() { var ua = fireauth.util.getUserAgentString(); // Safari or iOS browser and embedded in an iframe. if (!fireauth.util.iframeCanSyncWebStorage(ua) && fireauth.util.isIframe()) { return true; } return false; }; /** * @param {?Window=} opt_win Optional window to check whether it is an iframe. * If not provided, the current window is checked. * @return {boolean} Whether web page is running in an iframe. */ fireauth.util.isIframe = function(opt_win) { var win = opt_win || goog.global['window']; try { // Check that the current window is not the top window. // If so, return true. return !!(win && win != win['top']); } catch (e) { return false; } }; /** * @param {?Window=} opt_win Optional window to check whether it has an opener * that is an iframe. * @return {boolean} Whether the web page was opened from an iframe. */ fireauth.util.isOpenerAnIframe = function(opt_win) { var win = opt_win || goog.global['window']; try { // Get the opener if available. var opener = win && win['opener']; // Check if the opener is an iframe. If so, return true. // Confirm opener is available, otherwise the current window is checked // instead. return !!(opener && fireauth.util.isIframe(opener)); } catch (e) { return false; } }; /** * @param {?Object=} global The optional global scope. * @return {boolean} Whether current environment is a worker. */ fireauth.util.isWorker = function(global) { var scope = global || goog.global; // WorkerGlobalScope only defined in worker environment. return typeof scope['WorkerGlobalScope'] !== 'undefined' && typeof scope['importScripts'] === 'function'; }; /** * @param {?Object=} opt_global The optional global scope. * @return {boolean} Whether current environment supports fetch API and other * APIs it depends on. */ fireauth.util.isFetchSupported = function(opt_global) { // Required by fetch API calls. var scope = opt_global || goog.global; return typeof scope['fetch'] !== 'undefined' && typeof scope['Headers'] !== 'undefined' && typeof scope['Request'] !== 'undefined'; }; /** * Enum for the runtime environment. * @enum {string} */ fireauth.util.Env = { BROWSER: 'Browser', NODE: 'Node', REACT_NATIVE: 'ReactNative', WORKER: 'Worker' }; /** * @return {!fireauth.util.Env} The current runtime environment. */ fireauth.util.getEnvironment = function() { if (firebase.INTERNAL.hasOwnProperty('reactNative')) { return fireauth.util.Env.REACT_NATIVE; } else if (firebase.INTERNAL.hasOwnProperty('node')) { // browserify seems to keep the process property in some cases even though // the library is browser only. Use this check instead to reliably detect // a Node.js environment. return fireauth.util.Env.NODE; } else if (fireauth.util.isWorker()) { // Worker environment. return fireauth.util.Env.WORKER; } // The default is a browser environment. return fireauth.util.Env.BROWSER; }; /** * @return {boolean} Whether the environment is a native environment, where * CORS checks do not apply. */ fireauth.util.isNativeEnvironment = function() { var environment = fireauth.util.getEnvironment(); return environment === fireauth.util.Env.REACT_NATIVE || environment === fireauth.util.Env.NODE; }; /** * The separator for storage keys to concatenate App name and API key. * @const {string} * @private */ fireauth.util.STORAGE_KEY_SEPARATOR_ = ':'; /** * @param {string} apiKey The API Key of the app. * @param {string} appName The App name. * @return {string} The key used for identifying the app owner of the user. */ fireauth.util.createStorageKey = function(apiKey, appName) { return apiKey + fireauth.util.STORAGE_KEY_SEPARATOR_ + appName; }; /** @return {string} a long random character string. */ fireauth.util.generateRandomString = function() { return Math.floor(Math.random() * 1000000000).toString(); }; /** * Generates a random alpha numeric string. * @param {number} numOfChars The number of random characters within the string. * @return {string} A string with a specific number of random characters. */ fireauth.util.generateRandomAlphaNumericString = function(numOfChars) { var chars = []; var allowedChars = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; while (numOfChars > 0) { chars.push( allowedChars.charAt( Math.floor(Math.random() * allowedChars.length))); numOfChars--; } return chars.join(''); }; /** * Enums for Browser name. * @enum {string} */ fireauth.util.BrowserName = { ANDROID: 'Android', BLACKBERRY: 'Blackberry', EDGE: 'Edge', FIREFOX: 'Firefox', IE: 'IE', IEMOBILE: 'IEMobile', OPERA: 'Opera', OTHER: 'Other', CHROME: 'Chrome', SAFARI: 'Safari', SILK: 'Silk', WEBOS: 'Webos' }; /** * @param {string} userAgent The navigator user agent string. * @return {string} The browser name, eg Safari, Firefox, etc. */ fireauth.util.getBrowserName = function(userAgent) { var ua = userAgent.toLowerCase(); if (goog.string.contains(ua, 'opera/') || goog.string.contains(ua, 'opr/') || goog.string.contains(ua, 'opios/')) { return fireauth.util.BrowserName.OPERA; } else if (goog.string.contains(ua, 'iemobile')) { // Windows phone IEMobile browser. return fireauth.util.BrowserName.IEMOBILE; } else if (goog.string.contains(ua, 'msie') || goog.string.contains(ua, 'trident/')) { return fireauth.util.BrowserName.IE; } else if (goog.string.contains(ua, 'edge/')) { return fireauth.util.BrowserName.EDGE; } else if (goog.string.contains(ua, 'firefox/')) { return fireauth.util.BrowserName.FIREFOX; } else if (goog.string.contains(ua, 'silk/')) { return fireauth.util.BrowserName.SILK; } else if (goog.string.contains(ua, 'blackberry')) { // Blackberry browser. return fireauth.util.BrowserName.BLACKBERRY; } else if (goog.string.contains(ua, 'webos')) { // WebOS default browser. return fireauth.util.BrowserName.WEBOS; } else if (goog.string.contains(ua, 'safari/') && !goog.string.contains(ua, 'chrome/') && !goog.string.contains(ua, 'crios/') && !goog.string.contains(ua, 'android')) { return fireauth.util.BrowserName.SAFARI; } else if ((goog.string.contains(ua, 'chrome/') || goog.string.contains(ua, 'crios/')) && !goog.string.contains(ua, 'edge/')) { return fireauth.util.BrowserName.CHROME; } else if (goog.string.contains(ua, 'android')) { // Android stock browser. return fireauth.util.BrowserName.ANDROID; } else { // Most modern browsers have name/version at end of user agent string. var re = new RegExp('([a-zA-Z\\d\\.]+)\/[a-zA-Z\\d\\.]*$'); var matches = userAgent.match(re); if (matches && matches.length == 2) { return matches[1]; } } return fireauth.util.BrowserName.OTHER; }; /** * Enums for client implementation name. * @enum {string} */ fireauth.util.ClientImplementation = { JSCORE: 'JsCore', OAUTH_HANDLER: 'Handler', OAUTH_IFRAME: 'Iframe' }; /** * Enums for the framework ID to be logged in RPC header. * Future frameworks to possibly add: angularfire, polymerfire, reactfire, etc. * @enum {string}. */ fireauth.util.Framework = { // No other framework used. DEFAULT: 'FirebaseCore-web', // Firebase Auth used with FirebaseUI-web. FIREBASEUI: 'FirebaseUI-web' }; /** * @param {!Array<string>} providedFrameworks List of framework ID strings. * @return {!Array<!fireauth.util.Framework>} List of supported framework IDs * with no duplicates. */ fireauth.util.getFrameworkIds = function(providedFrameworks) { var frameworkVersion = []; var frameworkSet = {}; for (var key in fireauth.util.Framework) { frameworkSet[fireauth.util.Framework[key]] = true; } for (var i = 0; i < providedFrameworks.length; i++) { if (typeof frameworkSet[providedFrameworks[i]] !== 'undefined') { // Delete it from set to prevent duplications. delete frameworkSet[providedFrameworks[i]]; frameworkVersion.push(providedFrameworks[i]); } } // Sort alphabetically so that "FirebaseCore-web,FirebaseUI-web" and // "FirebaseUI-web,FirebaseCore-web" aren't viewed as different. frameworkVersion.sort(); return frameworkVersion; }; /** * @param {!fireauth.util.ClientImplementation} clientImplementation The client * implementation. * @param {string} clientVersion The client version. * @param {?Array<string>=} opt_frameworkVersion The framework version. * @param {?string=} opt_userAgent The optional user agent. * @return {string} The full client SDK version. */ fireauth.util.getClientVersion = function(clientImplementation, clientVersion, opt_frameworkVersion, opt_userAgent) { var frameworkVersion = fireauth.util.getFrameworkIds( opt_frameworkVersion || []); if (!frameworkVersion.length) { frameworkVersion = [fireauth.util.Framework.DEFAULT]; } var environment = fireauth.util.getEnvironment(); var reportedEnvironment = ''; if (environment === fireauth.util.Env.BROWSER) { // In a browser environment, report the browser name. var userAgent = opt_userAgent || fireauth.util.getUserAgentString(); reportedEnvironment = fireauth.util.getBrowserName(userAgent); } else if (environment === fireauth.util.Env.WORKER) { // Technically a worker runs from a browser but we need to differentiate a // worker from a browser. // For example: Chrome-Worker/JsCore/4.9.1/FirebaseCore-web. var userAgent = opt_userAgent || fireauth.util.getUserAgentString(); reportedEnvironment = fireauth.util.getBrowserName(userAgent) + '-' + environment; } else { // Otherwise, just report the environment name. reportedEnvironment = environment; } // The format to be followed: // ${browserName}/${clientImplementation}/${clientVersion}/${frameworkVersion} // As multiple Firebase frameworks/libraries can be used, join their IDs with // a comma. return reportedEnvironment + '/' + clientImplementation + '/' + clientVersion + '/' + frameworkVersion.join(','); }; /** * @return {string} The user agent string reported by the environment, or the * empty string if not available. */ fireauth.util.getUserAgentString = function() { return (goog.global['navigator'] && goog.global['navigator']['userAgent']) || ''; }; /** * @param {string} varStrName The variable string name. * @param {?Object=} opt_scope The optional scope where to look in. The default * is window. * @return {*} The reference if found. */ fireauth.util.getObjectRef = function(varStrName, opt_scope) { var pieces = varStrName.split('.'); var last = opt_scope || goog.global; for (var i = 0; i < pieces.length && typeof last == 'object' && last != null; i++) { last = last[pieces[i]]; } // Last hasn't reached the end yet, return undefined. if (i != pieces.length) { last = undefined; } return last; }; /** @return {boolean} Whether web storage is supported. */ fireauth.util.isWebStorageSupported = function() { try { var storage = goog.global['localStorage']; var key = fireauth.util.generateEventId(); if (storage) { // setItem will throw an exception if we cannot access WebStorage (e.g., // Safari in private mode). storage['setItem'](key, '1'); storage['removeItem'](key); // For browsers where iframe web storage does not synchronize with a popup // of the same domain, indexedDB is used for persistent storage. These // browsers include IE11 and Edge. // Make sure it is supported (IE11 and Edge private mode does not support // that). if (fireauth.util.isLocalStorageNotSynchronized()) { // In such browsers, if indexedDB is not supported, an iframe cannot be // notified of the popup sign in result. return !!goog.global['indexedDB']; } return true; } } catch (e) { // localStorage is not available from a worker. Test availability of // indexedDB. return fireauth.util.isWorker() && !!goog.global['indexedDB']; } return false; }; /** * This guards against leaking Cordova support before official launch. * This field will be removed or updated to return true when the new feature is * ready for launch. * @return {boolean} Whether Cordova OAuth support is enabled. */ fireauth.util.isCordovaOAuthEnabled = function() { return false; }; /** * @return {boolean} Whether popup and redirect operations are supported in the * current environment. */ fireauth.util.isPopupRedirectSupported = function() { // Popup and redirect are supported in an environment where the container // origin can be securely whitelisted. return (fireauth.util.isHttpOrHttps() || fireauth.util.isChromeExtension() || fireauth.util.isAndroidOrIosCordovaScheme()) && // React Native with remote debugging reports its location.protocol as // http. !fireauth.util.isNativeEnvironment() && // Local storage has to be supported for browser popup and redirect // operations to work. fireauth.util.isWebStorageSupported() && // DOM, popups and redirects are not supported within a worker. !fireauth.util.isWorker(); }; /** * @return {boolean} Whether the current environment is http or https. */ fireauth.util.isHttpOrHttps = function() { return fireauth.util.getCurrentScheme() === 'http:' || fireauth.util.getCurrentScheme() === 'https:'; }; /** @return {?string} The current URL scheme. */ fireauth.util.getCurrentScheme = function() { return (goog.global['location'] && goog.global['location']['protocol']) || null; }; /** * Checks whether the current page is a Chrome extension. * @return {boolean} Whether the current page is a Chrome extension. */ fireauth.util.isChromeExtension = function() { return fireauth.util.getCurrentScheme() === 'chrome-extension:'; }; /** * @param {?string=} opt_userAgent The optional user agent. * @return {boolean} Whether the current browser is running in an iOS * environment. */ fireauth.util.isIOS = function(opt_userAgent) { var ua = opt_userAgent || fireauth.util.getUserAgentString(); return !!ua.toLowerCase().match(/iphone|ipad|ipod/); }; /** * @param {?string=} opt_userAgent The optional user agent. * @return {boolean} Whether the current browser is running in an Android * environment. */ fireauth.util.isAndroid = function(opt_userAgent) { var ua = opt_userAgent || fireauth.util.getUserAgentString(); return !!ua.toLowerCase().match(/android/); }; /** * @param {?string=} opt_userAgent The optional user agent. * @return {boolean} Whether the opener of a popup cannot communicate with the * popup while it is in the foreground. */ fireauth.util.runsInBackground = function(opt_userAgent) { // TODO: split this check into 2, one check that opener can access // popup, another check that storage synchronizes between popup and opener. // Popup events fail in iOS version 7 (lowest version we currently support) // browsers. When the popup is triggered, the opener is unable to redirect // the popup url, close the popup and in some cases will miss the storage // event triggered when localStorage is changed. // Extend this to all mobile devices. This behavior is more likely to work // cross mobile platforms. var ua = opt_userAgent || fireauth.util.getUserAgentString(); if (fireauth.util.isMobileBrowser(ua)) { return false; } else if (fireauth.util.getBrowserName(ua) == fireauth.util.BrowserName.FIREFOX) { // Latest version of Firefox 47.0 does not allow you to access properties on // the popup window from the opener. return false; } return true; }; /** * Stringifies an object, retuning null if the object is not defined. * @param {*} obj The raw object. * @return {?string} The JSON-serialized object. */ fireauth.util.stringifyJSON = function(obj) { if (typeof obj === 'undefined') { return null; } return goog.json.serialize(obj); }; /** * @param {!Object} obj The original object. * @return {!Object} A copy of the original object with all entries that are * null or undefined removed. */ fireauth.util.copyWithoutNullsOrUndefined = function(obj) { // The processed copy to return. var trimmedObj = {}; // Remove all empty fields from data, allow zero and false booleans. for (var key in obj) { if (obj.hasOwnProperty(key) && obj[key] !== null && obj[key] !== undefined) { trimmedObj[key] = obj[key]; } } return trimmedObj; }; /** * Removes all key/pairs with the specified keys from the given object. * @param {!Object} obj The object to process. * @param {!Array<string>} keys The list of keys to remove. * @return {!Object} The object with the keys removed. */ fireauth.util.removeEntriesWithKeys = function(obj, keys) { // Clone object. var copy = goog.object.clone(obj); // Traverse keys. for (var i = 0; i < keys.length; i++) { var key = keys[i]; // If key found in object, remove it. if (key in copy) { delete copy[key]; } } // Returned filtered copy. return copy; }; /** * Parses a JSON string, returning undefined if null is passed. * @param {?string} json The JSON-serialized object. * @return {*} The raw object. */ fireauth.util.parseJSON = function(json) { if (json === null) { return undefined; } // Do not use goog.json.parse since it uses eval underneath to support old // browsers that do not provide JSON.parse. The recommended Content Security // Policy does not allow unsafe-eval in some environments like Chrome // extensions. Usage of eval is not recommend in Chrome in general. // Use native parsing instead via JSON.parse. This is provided in our list // of supported browsers. return JSON.parse(json); }; /** * @param {?string=} opt_prefix An optional prefix string to prepend to ID. * @return {string} The generated event ID used to identify a generic event. */ fireauth.util.generateEventId = function(opt_prefix) { return opt_prefix ? opt_prefix : '' + Math.floor(Math.random() * 1000000000).toString(); }; /** * @param {?string=} opt_userAgent The optional user agent. * @return {boolean} Whether an embedded iframe can sync to web storage changes. * Web storage sync fails in Safari desktop browsers and iOS mobile * browsers. */ fireauth.util.iframeCanSyncWebStorage = function(opt_userAgent) { var ua = opt_userAgent || fireauth.util.getUserAgentString(); if (fireauth.util.getBrowserName(ua) == fireauth.util.BrowserName.SAFARI || ua.toLowerCase().match(/iphone|ipad|ipod/)) { return false; } return true; }; /** * Reset unlaoded GApi modules. If gapi.load fails due to a network error, * it will stop working after a retrial. This is a hack to fix this issue. */ fireauth.util.resetUnloadedGapiModules = function() { // Clear last failed gapi.load state to force next gapi.load to first // load the failed gapi.iframes module. // Get gapix.beacon context. var beacon = goog.global['___jsl']; // Get current hint. if (beacon && beacon['H']) { // Get gapi hint. for (var hint in beacon['H']) { // Requested modules. beacon['H'][hint]['r'] = beacon['H'][hint]['r'] || []; // Loaded modules. beacon['H'][hint]['L'] = beacon['H'][hint]['L'] || []; // Set requested modules to a copy of the loaded modules. beacon['H'][hint]['r'] = beacon['H'][hint]['L'].concat(); // Clear pending callbacks. if (beacon['CP']) { for (var i = 0; i < beacon['CP'].length; i++) { // Remove all failed pending callbacks. beacon['CP'][i] = null; } } } } }; /** * Returns whether the current device is a mobile device. Mobile browsers and * React-Native environments are considered mobile devices. * @param {?string=} opt_userAgent The optional navigator user agent. * @param {?fireauth.util.Env=} opt_env The optional environment. * @return {boolean} Whether the current device is a mobile device or not. */ fireauth.util.isMobileDevice = function(opt_userAgent, opt_env) { // Get user agent. var ua = opt_userAgent || fireauth.util.getUserAgentString(); // Get environment. var environment = opt_env || fireauth.util.getEnvironment(); return fireauth.util.isMobileBrowser(ua) || environment === fireauth.util.Env.REACT_NATIVE; }; /** * @param {?Object=} opt_navigator The optional navigator object typically used * for testing. * @return {boolean} Whether the app is currently online. If offline, false is * returned. If this cannot be determined, true is returned. */ fireauth.util.isOnline = function(opt_navigator) { var navigator = opt_navigator || goog.global['navigator']; if (navigator && typeof navigator['onLine'] === 'boolean' && // Apply only for traditional web apps and Chrome extensions. // This is especially true for Cordova apps which have unreliable // navigator.onLine behavior unless cordova-plugin-network-information is // installed which overwrites the native navigator.onLine value and // defines navigator.connection. (fireauth.util.isHttpOrHttps() || fireauth.util.isChromeExtension() || typeof navigator['connection'] !== 'undefined')) { return navigator['onLine']; } // If we can't determine the state, assume it is online. return true; }; /** * @param {?Object=} opt_navigator The object with navigator data, defaulting * to window.navigator if unspecified. * @return {?string} The user's preferred language. Returns null if */ fireauth.util.getUserLanguage = function(opt_navigator) { var navigator = opt_navigator || goog.global['navigator']; if (!navigator) { return null; } return ( // Most reliable, but only supported in Chrome/Firefox. navigator['languages'] && navigator['languages'][0] || // Supported in most browsers, but returns the language of the browser // UI, not the language set in browser settings. navigator['language'] || // IE <= 10. navigator['userLanguage'] || // Couldn't determine language. null ); }; /** * A structure to help pick between a range of long and short delay durations * depending on the current environment. In general, the long delay is used for * mobile environments whereas short delays are used for desktop environments. * @param {number} shortDelay The short delay duration. * @param {number} longDelay The long delay duration. * @param {?string=} opt_userAgent The optional navigator user agent. * @param {?fireauth.util.Env=} opt_env The optional environment. * @constructor */ fireauth.util.Delay = function(shortDelay, longDelay, opt_userAgent, opt_env) { // Internal error when improperly initialized. if (shortDelay > longDelay) { throw new Error('Short delay should be less than long delay!'); } /** * @private @const {number} The short duration delay used for desktop * environments. */ this.shortDelay_ = shortDelay; /** * @private @const {number} The long duration delay used for mobile * environments. */ this.longDelay_ = longDelay; /** @private @const {boolean} Whether the environment is a mobile one. */ this.isMobile_ = fireauth.util.isMobileDevice(opt_userAgent, opt_env); }; /** * The default value for the offline delay timeout in ms. * @const {number} * @private */ fireauth.util.Delay.OFFLINE_DELAY_MS_ = 5000; /** * @return {number} The delay that matches with the current environment. */ fireauth.util.Delay.prototype.get = function() { // navigator.onLine is unreliable in some cases. // Failing hard in those cases may make it impossible to recover for end user. // Waiting for the regular full duration when there is no network can result // in a bad experience. // Instead return a short timeout duration. If there is no network connection, // the user would wait 5 seconds to detect that. If there is a connection // (false alert case), the user still has the ability to try to send the // request. If it fails (timeout too short), they can still retry. if (!fireauth.util.isOnline()) { // Pick the shorter timeout. return Math.min(fireauth.util.Delay.OFFLINE_DELAY_MS_, this.shortDelay_); } // If running in a mobile environment, return the long delay, otherwise // return the short delay. // This could be improved in the future to dynamically change based on other // variables instead of just reading the current environment. return this.isMobile_ ? this.longDelay_ : this.shortDelay_; }; /** * @return {boolean} Whether the app is visible in the foreground. This uses * document.visibilityState. For browsers that do not support it, this is * always true. */ fireauth.util.isAppVisible = function() { // https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilityState var doc = goog.global.document; // Check if supported. if (doc && typeof doc['visibilityState'] !== 'undefined') { // Check if visible. return doc['visibilityState'] == 'visible'; } // API not supported in current browser, default to true. return true; }; /** * @return {!goog.Promise} A promise that resolves when the app is visible in * the foreground. */ fireauth.util.onAppVisible = function() { var doc = goog.global.document; // Visibility change listener reference. var onVisibilityChange = null; if (fireauth.util.isAppVisible() || !doc) { // Visible or non browser environment. return goog.Promise.resolve(); } else { // Invisible and in browser environment. return new goog.Promise(function(resolve, reject) { // On visibility change listener. onVisibilityChange = function(event) { // App is visible. if (fireauth.util.isAppVisible()) { // Unregister event listener. doc.removeEventListener( 'visibilitychange', onVisibilityChange, false); // Resolve promise. resolve(); } }; // Listen to visibility change. doc.addEventListener('visibilitychange', onVisibilityChange, false); }).thenCatch(function(error) { // In case this promise was cancelled, make sure it unlistens to // visibilitychange event. doc.removeEventListener('visibilitychange', onVisibilityChange, false); // Rethrow the same error. throw error; }); } }; /** * Logs a warning message to the console, if the console is available. * @param {string} message */ fireauth.util.consoleWarn = function(message) { if (typeof console !== 'undefined' && typeof console.warn === 'function') { console.warn(message); } }; /** * Logs an info message to the console, if the console is available. * @param {string} message */ fireauth.util.consoleInfo = function(message) { if (typeof console !== 'undefined' && typeof console.info === 'function') { console.info(message); } }; /** * Parses a UTC time stamp string or number and returns the corresponding UTC * date string if valid. Otherwise, returns null. * @param {?string|number} utcTimestamp The UTC timestamp number or string. * @return {?string} The corresponding UTC date string. Null if invalid. */ fireauth.util.utcTimestampToDateString = function(utcTimestamp) { try { // Convert to date object. var date = new Date(parseInt(utcTimestamp, 10)); // Test date is valid. if (!isNaN(date.getTime()) && // Confirm that utcTimestamp is numeric. goog.string.isNumeric(utcTimestamp)) { // Convert to UTC date string. return date.toUTCString(); } } catch (e) { // Do nothing. null will be returned. } return null; }; /** @return {boolean} Whether indexedDB is available. */ fireauth.util.isIndexedDBAvailable = function() { return !!goog.global['indexedDB']; }; /** @return {boolean} Whether current mode is Auth handler or iframe. */ fireauth.util.isAuthHandlerOrIframe = function() { return !!(fireauth.util.getObjectRef('fireauth.oauthhelper', goog.global) || fireauth.util.getObjectRef('fireauth.iframe', goog.global)); }; /** @return {boolean} Whether indexedDB is used to persist storage. */ fireauth.util.persistsStorageWithIndexedDB = function() { // This will cover: // IE11, Edge when indexedDB is available (this is unavailable in InPrivate // mode). (SDK, OAuth handler and iframe) // Any environment where indexedDB is available (SDK only). // In a browser environment, when an iframe and a popup web storage are not // synchronized, use the indexedDB fireauth.storage.Storage implementation. return (fireauth.util.isLocalStorageNotSynchronized() || !fireauth.util.isAuthHandlerOrIframe()) && fireauth.util.isIndexedDBAvailable(); }; /** Sets the no-referrer meta tag in the document head if applicable. */ fireauth.util.setNoReferrer = function() { var doc = goog.global.document; if (doc) { try { var meta = goog.dom.createDom(goog.dom.TagName.META, { 'name': 'referrer', 'content': 'no-referrer' }); var headCollection = goog.dom.getElementsByTagName(goog.dom.TagName.HEAD); // Append meta tag to head. if (headCollection.length) { headCollection[0].appendChild(meta); } } catch (e) { // Best effort approach. } } }; /** @return {?ServiceWorker} The servicerWorker controller if available. */ fireauth.util.getServiceWorkerController = function() { var navigator = goog.global['navigator']; return (navigator && navigator.serviceWorker && navigator.serviceWorker.controller) || null; }; /** @return {?WorkerGlobalScope} The worker global scope if available. */ fireauth.util.getWorkerGlobalScope = function() { return fireauth.util.isWorker() ? /** @type {!WorkerGlobalScope} */ (self) : null; }; /** * @return {!goog.Promise<?ServiceWorker>} A promise that resolves with the * service worker. This will resolve only when a service worker becomes * available. If no service worker is supported, it will resolve with null. */ fireauth.util.getActiveServiceWorker = function() { var navigator = goog.global['navig