voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
659 lines (604 loc) • 23.8 kB
JavaScript
/**
* @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 utilities for session management.
*/
goog.provide('fireauth.authStorage');
goog.provide('fireauth.authStorage.Key');
goog.provide('fireauth.authStorage.Manager');
goog.provide('fireauth.authStorage.Persistence');
goog.require('fireauth.AuthError');
goog.require('fireauth.authenum.Error');
goog.require('fireauth.storage.Factory');
goog.require('fireauth.storage.IndexedDB');
goog.require('fireauth.storage.Storage');
goog.require('fireauth.util');
goog.require('goog.Promise');
goog.require('goog.array');
goog.require('goog.events');
goog.require('goog.object');
/**
* The namespace for Firebase Auth storage.
* @private @const {string}
*/
fireauth.authStorage.NAMESPACE_ = 'firebase';
/**
* The separator for Firebase Auth storage with App ID key.
* @private @const {string}
*/
fireauth.authStorage.SEPARATOR_ = ':';
/**
* @const {number} The IE 10 localStorage cross tab synchronization delay in
* milliseconds.
*/
fireauth.authStorage.IE10_LOCAL_STORAGE_SYNC_DELAY = 10;
/**
* Enums for Auth state persistence.
* @enum {string}
*/
fireauth.authStorage.Persistence = {
// State will persist even when the browser window is closed or the activity
// is destroyed in react-native.
LOCAL: 'local',
// State is only stored in memory and will be cleared when the window or
// activity is refreshed.
NONE: 'none',
// State will only persist in current session/tab, relevant to web only, and
// will be cleared when the tab is closed.
SESSION: 'session'
};
/**
* Validates that an argument is a valid persistence value. If an invalid type
* is specified, an error is thrown synchronously.
* @param {*} arg The argument to validate.
*/
fireauth.authStorage.validatePersistenceArgument =
function(arg) {
// Invalid type error.
var invalidTypeError = new fireauth.AuthError(
fireauth.authenum.Error.INVALID_PERSISTENCE);
// Unsupported type error.
var unsupportedTypeError = new fireauth.AuthError(
fireauth.authenum.Error.UNSUPPORTED_PERSISTENCE);
// Check if the persistence type is a valid one.
// Throw invalid type error if not valid.
if (!goog.object.containsValue(fireauth.authStorage.Persistence, arg) ||
// goog.object.containsValue(fireauth.authStorage.Persistence, ['none'])
// returns true.
typeof arg !== 'string') {
throw invalidTypeError;
}
// Validate if the specified type is supported in the current environment.
switch (fireauth.util.getEnvironment()) {
case fireauth.util.Env.REACT_NATIVE:
// This is only supported in a browser.
if (arg === fireauth.authStorage.Persistence.SESSION) {
throw unsupportedTypeError;
}
break;
case fireauth.util.Env.NODE:
// Only none is supported in Node.js.
if (arg !== fireauth.authStorage.Persistence.NONE) {
throw unsupportedTypeError;
}
break;
case fireauth.util.Env.WORKER:
// In a worker environment, either LOCAL or NONE are supported.
// If indexedDB not supported and LOCAL provided, throw an error.
if (arg === fireauth.authStorage.Persistence.SESSION ||
(!fireauth.storage.IndexedDB.isAvailable() &&
arg !== fireauth.authStorage.Persistence.NONE)) {
throw unsupportedTypeError;
}
break;
case fireauth.util.Env.BROWSER:
default:
// This is restricted by what the browser supports.
if (!fireauth.util.isWebStorageSupported() &&
arg !== fireauth.authStorage.Persistence.NONE) {
throw unsupportedTypeError;
}
break;
}
};
/**
* Storage key metadata.
* @typedef {{name: string, persistent: !fireauth.authStorage.Persistence}}
*/
fireauth.authStorage.Key;
/**
* Storage manager.
* @param {string} namespace The optional namespace.
* @param {string} separator The optional separator.
* @param {boolean} safariLocalStorageNotSynced Whether browser has Safari
* iframe restriction with storage event triggering but storage not updated.
* @param {boolean} runsInBackground Whether browser can detect storage event
* when it had already been pushed to the background. This may happen in
* some mobile browsers. A localStorage change in the foreground window
* will not be detected in the background window via the storage event.
* This was detected in iOS 7.x mobile browsers.
* @param {boolean} webStorageSupported Whether browser web storage is
* supported.
* @constructor @struct @final
*/
fireauth.authStorage.Manager = function(
namespace,
separator,
safariLocalStorageNotSynced,
runsInBackground,
webStorageSupported) {
/** @const @private {string} Storage namespace. */
this.namespace_ = namespace;
/** @const @private {string} Storage namespace key separator. */
this.separator_ = separator;
/**
* @const @private {boolean} Whether browser has Safari iframe restriction
* with storage event triggering but storage not updated.
*/
this.safariLocalStorageNotSynced_ = safariLocalStorageNotSynced;
/**
* @private {boolean} Whether browser can detect storage event when it
* had already been pushed to the background. This may happen in some
* mobile browsers.
*/
this.runsInBackground_ = runsInBackground;
/** @const @private {boolean} Whether browser web storage is supported. */
this.webStorageSupported_ = webStorageSupported;
/**
* @const @private {!Object.<string, !Array<function()>>} The storage event
* key to listeners map.
*/
this.listeners_ = {};
var storageFactory = fireauth.storage.Factory.getInstance();
try {
/**
* @private {!fireauth.storage.Storage} Persistence storage.
*/
this.persistentStorage_ = storageFactory.makePersistentStorage();
} catch (e) {
// Default to in memory storage if the preferred persistent storage is not
// supported.
this.persistentStorage_ = storageFactory.makeInMemoryStorage();
// Do not use indexedDB fallback.
this.localStorageNotSynchronized_ = false;
// Do not set polling functions on window.localStorage.
this.runsInBackground_ = true;
}
try {
/**
* @private {!fireauth.storage.Storage} Temporary session storage.
*/
this.temporaryStorage_ = storageFactory.makeTemporaryStorage();
} catch (e) {
// Default to in memory storage if the preferred temporary storage is not
// supported. This should be a different in memory instance as the
// persistent storage, since the same key could be available for both types
// of storage.
this.temporaryStorage_ = storageFactory.makeInMemoryStorage();
}
/**
* @private {!fireauth.storage.Storage} In memory storage.
*/
this.inMemoryStorage_ = storageFactory.makeInMemoryStorage();
/**
* @const @private {function(!goog.events.BrowserEvent)|
* function(!Array<string>)} Storage change handler.
*/
this.storageChangeEventHandler_ = goog.bind(this.storageChangeEvent_, this);
/** @private {!Object.<string, *>} Local map for localStorage. */
this.localMap_ = {};
};
/**
* @return {!fireauth.authStorage.Manager} The default Auth storage manager
* instance.
*/
fireauth.authStorage.Manager.getInstance = function() {
// Creates the default instance for Auth storage maanger.
if (!fireauth.authStorage.Manager.instance_) {
/**
* @private {?fireauth.authStorage.Manager} The default storage manager
* instance.
*/
fireauth.authStorage.Manager.instance_ = new fireauth.authStorage.Manager(
fireauth.authStorage.NAMESPACE_,
fireauth.authStorage.SEPARATOR_,
fireauth.util.isSafariLocalStorageNotSynced(),
fireauth.util.runsInBackground(),
fireauth.util.isWebStorageSupported());
}
return fireauth.authStorage.Manager.instance_;
};
/** Clears storage manager instances. This is used for testing. */
fireauth.authStorage.Manager.clear = function() {
fireauth.authStorage.Manager.instance_ = null;
};
/**
* Returns the storage corresponding to the specified persistence.
* @param {!fireauth.authStorage.Persistence} persistent The type of storage
* persistence.
* @return {!fireauth.storage.Storage} The corresponding storage instance.
* @private
*/
fireauth.authStorage.Manager.prototype.getStorage_ = function(persistent) {
switch (persistent) {
case fireauth.authStorage.Persistence.SESSION:
return this.temporaryStorage_;
case fireauth.authStorage.Persistence.NONE:
return this.inMemoryStorage_;
case fireauth.authStorage.Persistence.LOCAL:
default:
return this.persistentStorage_;
}
};
/**
* Constructs the corresponding storage key name.
* @param {fireauth.authStorage.Key} dataKey The key under which the value is
* stored.
* @param {?string=} opt_id This ID associates storage values with specific
* apps.
* @return {string} The corresponding key name with namespace prefixed.
* @private
*/
fireauth.authStorage.Manager.prototype.getKeyName_ = function(dataKey, opt_id) {
return this.namespace_ + this.separator_ + dataKey.name +
(opt_id ? this.separator_ + opt_id : '');
};
/**
* Migrates window.localStorage to the provided persistent storage.
* @param {fireauth.authStorage.Key} dataKey The key under which the persistent
* value is supposed to be stored.
* @param {?string=} opt_id When operating in multiple app mode, this ID
* associates storage values with specific apps.
* @return {!goog.Promise<void>} A promise that resolves when the data stored
* in window.localStorage is migrated to the provided persistent storage
* identified by the provided data key.
*/
fireauth.authStorage.Manager.prototype.migrateFromLocalStorage =
function(dataKey, opt_id) {
var self = this;
var key = this.getKeyName_(dataKey, opt_id);
var storage = this.getStorage_(dataKey.persistent);
// Get data stored in the default persistent storage identified by dataKey.
return this.get(dataKey, opt_id).then(function(response) {
// Get the stored value in window.localStorage if available.
var oldStorageValue = null;
try {
oldStorageValue = fireauth.util.parseJSON(
goog.global['localStorage']['getItem'](key));
} catch (e) {
// Set value as null. This will resolve the promise immediately.
}
// If data is stored in window.localStorage but no data is available in
// default persistent storage, migrate data from window.localStorage to
// default persistent storage.
if (oldStorageValue && !response) {
// This condition may fail in situations where a user opens a tab with
// an old version while using a tab with a new version, or when a
// developer switches back and forth between and old and new version of
// the library.
goog.global['localStorage']['removeItem'](key);
// Migrate the value to new default persistent storage.
return self.set(dataKey, oldStorageValue, opt_id);
} else if (oldStorageValue &&
response &&
storage.type != fireauth.storage.Storage.Type.LOCAL_STORAGE) {
// Data stored in both localStorage and new persistent storage (eg.
// indexedDB) for some reason.
// This could happen if the developer is migrating back and forth.
// The new default persistent storage (eg. indexedDB) takes precedence.
goog.global['localStorage']['removeItem'](key);
}
});
};
/**
* Gets the stored value from the corresponding storage.
* @param {fireauth.authStorage.Key} dataKey The key under which the value is
* stored.
* @param {?string=} opt_id When operating in multiple app mode, this ID
* associates storage values with specific apps.
* @return {!goog.Promise} A Promise that resolves with the stored value.
*/
fireauth.authStorage.Manager.prototype.get = function(dataKey, opt_id) {
var keyName = this.getKeyName_(dataKey, opt_id);
return this.getStorage_(dataKey.persistent).get(keyName);
};
/**
* Removes the stored value from the corresponding storage.
* @param {fireauth.authStorage.Key} dataKey The key under which the value is
* stored.
* @param {?string=} opt_id When operating in multiple app mode, this ID
* associates storage values with specific apps.
* @return {!goog.Promise<void>} A Promise that resolves when the operation is
* completed.
*/
fireauth.authStorage.Manager.prototype.remove = function(dataKey, opt_id) {
var keyName = this.getKeyName_(dataKey, opt_id);
// Keep local map up to date for requested key if persistent storage is used.
if (dataKey.persistent == fireauth.authStorage.Persistence.LOCAL) {
this.localMap_[keyName] = null;
}
return this.getStorage_(dataKey.persistent).remove(keyName);
};
/**
* Stores the value in the corresponding storage.
* @param {fireauth.authStorage.Key} dataKey The key under which the value is
* stored.
* @param {*} value The value to be stored.
* @param {?string=} opt_id When operating in multiple app mode, this ID
* associates storage values with specific apps.
* @return {!goog.Promise<void>} A Promise that resolves when the operation is
* completed.
*/
fireauth.authStorage.Manager.prototype.set = function(dataKey, value, opt_id) {
var keyName = this.getKeyName_(dataKey, opt_id);
var self = this;
var storage = this.getStorage_(dataKey.persistent);
return storage.set(keyName, value)
.then(function() {
return storage.get(keyName);
})
.then(function(serializedValue) {
// Keep local map up to date for requested key if persistent storage is
// used.
if (dataKey.persistent == fireauth.authStorage.Persistence.LOCAL) {
self.localMap_[keyName] = serializedValue;
}
});
};
/**
* @param {fireauth.authStorage.Key} dataKey The key under which the value is
* stored.
* @param {?string} id When operating in multiple app mode, this ID associates
* storage values with specific apps.
* @param {function()} listener The callback listener to run on storage event
* related to key.
*/
fireauth.authStorage.Manager.prototype.addListener =
function(dataKey, id, listener) {
var key = this.getKeyName_(dataKey, id);
// Initialize local map for current key if web storage is supported.
if (this.webStorageSupported_) {
this.localMap_[key] = goog.global['localStorage']['getItem'](key);
}
if (goog.object.isEmpty(this.listeners_)) {
// Start listeners.
this.startListeners_();
}
if (!this.listeners_[key]) {
this.listeners_[key] = [];
}
this.listeners_[key].push(listener);
};
/**
* @param {fireauth.authStorage.Key} dataKey The key under which the value is
* stored.
* @param {?string} id When operating in multiple app mode, this ID associates
* storage values with specific apps.
* @param {function()} listener The listener to remove.
*/
fireauth.authStorage.Manager.prototype.removeListener =
function(dataKey, id, listener) {
var key = this.getKeyName_(dataKey, id);
if (this.listeners_[key]) {
goog.array.removeAllIf(
this.listeners_[key],
function(ele) {
return ele == listener;
});
if (this.listeners_[key].length == 0) {
delete this.listeners_[key];
}
}
if (goog.object.isEmpty(this.listeners_)) {
// Stop listeners.
this.stopListeners_();
}
};
/**
* The delay to wait between continuous checks of localStorage on browsers where
* tabs do not run in the background. After each interval wait, we check for
* external changes in localStorage that were not detected in the current tab.
* @const {number}
* @private
*/
fireauth.authStorage.Manager.LOCAL_STORAGE_POLLING_TIMER_ = 1000;
/**
* Starts all storage event listeners.
* @private
*/
fireauth.authStorage.Manager.prototype.startListeners_ = function() {
this.getStorage_(fireauth.authStorage.Persistence.LOCAL)
.addStorageListener(this.storageChangeEventHandler_);
// TODO: refactor this implementation to be handled by the underlying
// storage mechanism.
if (!this.runsInBackground_ &&
// Add an exception for browsers that persist storage with indexedDB, we
// should stick with indexedDB listener implementation in that case.
!fireauth.util.persistsStorageWithIndexedDB() &&
// Confirm browser web storage is supported as polling relies on it.
this.webStorageSupported_) {
this.startManualListeners_();
}
};
/**
* Starts manual polling function to detect storage event changes.
* @private
*/
fireauth.authStorage.Manager.prototype.startManualListeners_ = function() {
var self = this;
this.stopManualListeners_();
/** @private {?number} The interval timer for manual storage checking. */
this.manualListenerTimer_ = setInterval(function() {
// Check all keys with listeners on them.
for (var key in self.listeners_) {
// Get value from localStorage.
var currentValue = goog.global['localStorage']['getItem'](key);
var oldValue = self.localMap_[key];
// If local map value does not match, trigger listener with storage event.
if (currentValue != oldValue) {
self.localMap_[key] = currentValue;
var event = new goog.events.BrowserEvent(/** @type {!Event} */ ({
type: 'storage',
key: key,
target: window,
oldValue: oldValue,
newValue: currentValue,
// Differentiate this simulated event from the real storage event.
poll: true
}));
self.storageChangeEvent_(event);
}
}
}, fireauth.authStorage.Manager.LOCAL_STORAGE_POLLING_TIMER_);
};
/**
* Stops manual polling function to detect storage event changes.
* @private
*/
fireauth.authStorage.Manager.prototype.stopManualListeners_ = function() {
if (this.manualListenerTimer_) {
clearInterval(this.manualListenerTimer_);
this.manualListenerTimer_ = null;
}
};
/**
* Stops all storage event listeners.
* @private
*/
fireauth.authStorage.Manager.prototype.stopListeners_ = function() {
this.getStorage_(fireauth.authStorage.Persistence.LOCAL)
.removeStorageListener(this.storageChangeEventHandler_);
this.stopManualListeners_();
};
/**
* @param {!goog.events.BrowserEvent|!Array<string>} data The storage event
* triggered or the array of keys modified.
* @private
*/
fireauth.authStorage.Manager.prototype.storageChangeEvent_ = function(data) {
if (data && data.getBrowserEvent) {
var event = /** @type {!goog.events.BrowserEvent} */ (data);
var key = event.getBrowserEvent().key;
// Key would be null in some situations, like when localStorage is cleared
// from the browser developer tools.
if (key == null) {
// For all keys of interest.
for (var keyName in this.listeners_) {
// Check if something changed in this key's real value.
var storedValue = this.localMap_[keyName];
// localStorage returns null when a field is not found.
if (typeof storedValue === 'undefined') {
storedValue = null;
}
var realValue = goog.global['localStorage']['getItem'](keyName);
if (realValue !== storedValue) {
// Update local map with real value.
this.localMap_[keyName] = realValue;
// Trigger that key's listener.
this.callListeners_(keyName);
}
}
return;
}
// Check if the key is Firebase Auth related, otherwise ignore.
if (key.indexOf(this.namespace_ + this.separator_) != 0 ||
// Ignore keys that have no listeners.
!this.listeners_[key]) {
return;
}
// Check the mechanism how this event was detected.
// The first event will dictate the mechanism to be used.
// Do not use hasOwnProperty('poll') as poll gets obfuscated.
if (typeof event.getBrowserEvent().poll !== 'undefined') {
// Environment detects storage changes via polling.
// Remove storage event listener to prevent possible event duplication.
this.getStorage_(fireauth.authStorage.Persistence.LOCAL)
.removeStorageListener(this.storageChangeEventHandler_);
} else {
// Environment detects storage changes via storage event listener.
// Remove polling listener to prevent possible event duplication.
this.stopManualListeners_();
}
// Safari embedded iframe. Storage event will trigger with the delta changes
// but no changes will be applied to the iframe localStorage.
if (this.safariLocalStorageNotSynced_) {
// Get current iframe page value, old value and new value.
var currentValue = goog.global['localStorage']['getItem'](key);
var newValue = event.getBrowserEvent().newValue;
// Value not synchronized, synchronize manually.
if (newValue !== currentValue) {
if (newValue !== null) {
// Value changed from current value.
goog.global['localStorage']['setItem'](key, newValue);
} else {
// Current value deleted.
goog.global['localStorage']['removeItem'](key);
}
} else {
// Already detected and processed, do not trigger listeners again.
if (this.localMap_[key] === newValue &&
// Real storage event.
typeof event.getBrowserEvent().poll === 'undefined') {
return;
}
}
}
var self = this;
var triggerListeners = function() {
// Keep local map up to date in case storage event is triggered before
// poll.
if (typeof event.getBrowserEvent().poll === 'undefined' &&
self.localMap_[key] === goog.global['localStorage']['getItem'](key)) {
// Real storage event which has already been detected, do nothing.
// This seems to trigger in some IE browsers for some reason.
return;
}
self.localMap_[key] = goog.global['localStorage']['getItem'](key);
self.callListeners_(key);
};
if (fireauth.util.isIe10() &&
goog.global['localStorage']['getItem'](key) !==
event.getBrowserEvent().newValue &&
event.getBrowserEvent().newValue !== event.getBrowserEvent().oldValue) {
// IE 10 has this weird bug where a storage event would trigger with the
// correct key, oldValue and newValue but localStorage.getItem(key) does
// not yield the updated value until a few milliseconds. This ensures this
// recovers from that situation.
setTimeout(
triggerListeners, fireauth.authStorage.IE10_LOCAL_STORAGE_SYNC_DELAY);
} else {
triggerListeners();
}
} else {
var keys = /** @type {!Array<string>} */ (data);
goog.array.forEach(keys, goog.bind(this.callListeners_, this));
}
};
/**
* Calls all listeners for specified storage event key.
* @param {string} key The storage event key whose listeners are to be run.
* @private
*/
fireauth.authStorage.Manager.prototype.callListeners_ = function(key) {
if (this.listeners_[key]) {
goog.array.forEach(
this.listeners_[key],
function(listener) {
listener();
});
}
};