'use strict';
define([], function () {
/**
* settingsStore.js
*
* A reader/writer framework for cookies or localStorage with full unicode support based on the Mozilla cookies framework.
* The Mozilla code has been adapted to test for the availability of the localStorage API, and to use it in preference to cookies.
*
* Mozilla version information:
*
* Revision #1 - September 4, 2014
*
* https://developer.mozilla.org/en-US/docs/Web/API/document.cookie
* https://developer.mozilla.org/User:fusionchess
*
* This framework is released under the GNU Public License, version 3 or later.
* http://www.gnu.org/licenses/gpl-3.0-standalone.html
*
* Syntaxes:
*
* * settingsStore.setItem(name, value[, end[, path[, domain[, secure]]]])
* * settingsStore.getItem(name)
* * settingsStore.removeItem(name[, path[, domain]])
* * settingsStore.hasItem(name)
*
*/
/**
* A RegExp of the settings keys used in the cookie that should be migrated to localStorage if the API is available
* DEV: It should not be necessary to keep this list up-to-date because any keys added after this list was created
* (April 2020) will already be stored in localStorage if it is available to the client's browser or platform and
* will not need to be migrated
* @type {RegExp}
*/
var regexpCookieKeysToMigrate = new RegExp([
'contentInjectionMode', 'lastSelectedArchive', 'lastSelectedArchivePath',
'imageDisplay', 'useMathJax', 'appVersion',
'listOfArchives', 'lastPageVisit', 'cssCache', 'cssUITheme',
'cssTheme', 'cssSource', 'removePageMaxWidth', 'hideActiveContentWarning',
'hideToolbars', 'allowHTMLExtraction', 'openAllSections', 'PWAInstalled',
'lastPageLoad', 'showFileSelectors'
].join('|'));
/**
* A list of deprecated keys that should be removed. Add any further keys to the list of strings separated by a comma.
* @type {Array}
*/
var deprecatedKeys = [
'lastContentInjectionMode', 'lastPageHTML', 'lastPageVisit', 'version', 'useCache'
];
/**
* The prefix that will be added to keys when stored in localStorage: this is used to prevent
* potential collision of key names with localStorage keys used by code inside ZIM archives
* It is set in init.js because it is needed early in app loading
* @type {String}
*/
var keyPrefix = params.keyPrefix;
// Tests for available Storage APIs (document.cookie or localStorage) and returns the best available of these
function getBestAvailableStorageAPI() {
// DEV: In FF extensions, cookies are blocked since at least FF 68.6 but possibly since FF 55 [kiwix-js #612]
var type = 'none';
// First test for localStorage API support
var localStorageTest;
try {
localStorageTest = 'localStorage' in window && window['localStorage'] !== null;
// DEV: Above test returns true in IE11 running from file:// protocol, but attempting to write a key to
// localStorage causes an exception; so to test fully, we must now attempt to write and remove a test key
if (localStorageTest) {
localStorage.setItem('tempKiwixStorageTest', '');
localStorage.removeItem('tempKiwixStorageTest');
}
} catch (e) {
localStorageTest = false;
}
// Now test for document.cookie API support
document.cookie = 'tempKiwixCookieTest=working; expires=Fri, 31 Dec 9999 23:59:59 GMT; SameSite=Strict';
var kiwixCookieTest = /tempKiwixCookieTest=working/.test(document.cookie);
// Remove test value by expiring the key
document.cookie = 'tempKiwixCookieTest=; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Strict';
if (kiwixCookieTest) type = 'cookie';
// Prefer localStorage if supported due to some platforms removing cookies once the session ends in some contexts
if (localStorageTest) type = 'local_storage';
// If both cookies and localStorage are supported, and document.cookie contains keys to migrate,
// migrate settings to use localStorage
if (kiwixCookieTest && localStorageTest && regexpCookieKeysToMigrate.test(document.cookie)) _migrateStorageSettings();
// Remove any deprecated keys
deprecatedKeys.forEach(function (key) {
if (localStorageTest) localStorage.removeItem(keyPrefix + key);
settingsStore.removeItem(key); // Because this runs before we have returned a store type, this will remove from cookie too
});
// Note that if this function returns 'none', the cookie implementations below will run anyway. This is because storing a cookie
// does not cause an exception even if cookies are blocked in some contexts, whereas accessing localStorage may cause an exception
return type;
}
/**
* Performs a full app reset, deleting all caches and settings
* Or, if a parameter is supplied, deletes or disables the object
* @param {String} object Optional name of the object to disable or delete ('cookie', 'localStorage', 'cacheAPI')
*/
function reset(object) {
var performReset = function () {
// 1. Clear any cookie entries
if (!object || object === 'cookie') {
var regexpCookieKeys = /(?:^|;)\s*([^=]+)=([^;]*)/ig;
var currentCookie = document.cookie;
var foundCrumb = false;
var cookieCrumb = regexpCookieKeys.exec(currentCookie);
while (cookieCrumb !== null) {
// DEV: Note that we don't use the keyPrefix in legacy cookie support
foundCrumb = true;
// This expiry date will cause the browser to delete the cookie crumb on next page refresh
document.cookie = cookieCrumb[1] + '=;expires=Thu, 21 Sep 1979 00:00:01 UTC;';
cookieCrumb = regexpCookieKeys.exec(currentCookie);
}
if (foundCrumb) console.debug('All cookie keys were expired...');
}
// 2. Clear any localStorage settings
if (!object || object === 'localStorage') {
if (/localStorage/.test(assetsCache.capability)) {
localStorage.clear();
console.debug('All Local Storage settings were deleted...');
}
}
// 3. Clear any IndexedDB entries
if (!object || object === 'indexedDB') {
if (/indexedDB/.test(assetsCache.capability)) {
var cache = require('cache');
cache.clear('reset');
}
}
// 4. Clear any (remaining) Cache API caches
if (!object || object === 'cacheAPI') {
getCacheNames(function (cacheNames) {
if (cacheNames && !cacheNames.error) {
var cnt = 0;
for (var cacheName in cacheNames) {
cnt++;
caches.delete(cacheNames[cacheName]).then(function () {
cnt--;
if (!cnt) {
// All caches deleted
console.debug('All Cache API caches were deleted...');
// Reload if user performed full reset or if appCache is needed
if (!object || params.appCache) _reloadApp();
}
});
}
} else {
console.debug('No Cache API caches were in use (or we do not have access to the names).');
// All operations complete, reload if user performed full reset or if appCache is needed
if (!object || params.appCache) _reloadApp();
}
});
}
};
var uiUtil = require('uiUtil');
// If no specific object was specified, we are doing a general reset, so ask user for confirmation
if (object) performReset();
else {
uiUtil.systemAlert('WARNING: This will reset the app to a freshly installed state, deleting all app caches and settings! Make sure you have an Internet connection if this is an offline PWA, because it will be erased and reloaded.',
'Warning!', true).then(function (confirm) {
if (confirm) performReset();
else console.debug('User cancelled');
});
}
}
// Gets cache names from Service Worker, as we cannot rely on having them in params.cacheNames
function getCacheNames(callback) {
if (navigator.serviceWorker && navigator.serviceWorker.controller) {
var channel = new MessageChannel();
channel.port1.onmessage = function (event) {
var names = event.data;
callback(names);
};
navigator.serviceWorker.controller.postMessage({
action: 'getCacheNames'
}, [channel.port2]);
} else {
callback(null);
}
}
// Deregisters all Service Workers and reboots the app
function _reloadApp() {
var reboot = function () {
console.debug('Performing app reload...');
setTimeout(function () {
window.location.href = location.origin + location.pathname + uriParams
}, 300);
};
// Blank the querystring, so that parameters are not set on reload
var uriParams = '';
if (~window.location.href.indexOf(params.PWAServer) && params.referrerExtensionURL) {
// However, if we're in a PWA that was called from local code, then by definition we must remain in SW mode and we need to
// ensure the user still has access to the referrerExtensionURL (so they can get back to local code from the UI)
uriParams = '?allowInternetAccess=truee&contentInjectionMode=serviceworker';
uriParams += '&referrerExtensionURL=' + encodeURIComponent(params.referrerExtensionURL);
}
if (navigator && navigator.serviceWorker) {
console.debug('Deregistering Service Workers...');
var cnt = 0;
navigator.serviceWorker.getRegistrations().then(function (registrations) {
if (!registrations.length) {
reboot();
return;
}
cnt++;
registrations.forEach(function (registration) {
registration.unregister().then(function () {
cnt--;
if (!cnt) {
console.debug('All Service Workers unregistered...');
reboot();
}
});
});
}).catch(function (err) {
console.error(err);
reboot();
});
} else {
console.debug('Performing app reload...');
reboot();
}
}
var settingsStore = {
getItem: function (sKey) {
if (!sKey) {
return null;
}
if (params.storeType !== 'local_storage') {
return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[-.+*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
} else {
return localStorage.getItem(keyPrefix + sKey);
}
},
setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
if (params.storeType !== 'local_storage') {
if (!sKey || /^(?:expires|max-age|path|domain|secure)$/i.test(sKey)) {
return false;
}
var sExpires = "";
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
break;
case String:
sExpires = "; expires=" + vEnd;
break;
case Date:
sExpires = "; expires=" + vEnd.toUTCString();
break;
}
}
document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
} else {
localStorage.setItem(keyPrefix + sKey, sValue);
}
return true;
},
removeItem: function (sKey, sPath, sDomain) {
if (!this.hasItem(sKey)) {
return false;
}
if (params.storeType !== 'local_storage') {
document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "");
} else {
localStorage.removeItem(keyPrefix + sKey);
}
return true;
},
hasItem: function (sKey) {
if (!sKey) {
return false;
}
if (params.storeType !== 'local_storage') {
return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[-.+*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
} else {
return localStorage.getItem(keyPrefix + sKey) === null ? false : true;
}
},
_cookieKeys: function () {
var aKeys = document.cookie.replace(/((?:^|\s*;)[^=]+)(?=;|$)|^\s*|\s*(?:=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:=[^;]*)?;\s*/);
for (var nLen = aKeys.length, nIdx = 0; nIdx < nLen; nIdx++) {
aKeys[nIdx] = decodeURIComponent(aKeys[nIdx]);
}
return aKeys;
}
};
// One-off migration of storage settings from cookies to localStorage
function _migrateStorageSettings() {
console.log('Migrating Settings Store from cookies to localStorage...');
var cookieKeys = settingsStore._cookieKeys();
// Note that because migration occurs before setting params.storeType, settingsStore.getItem() will get the item from
// document.cookie instead of localStorage, which is the intended behaviour
for (var i = 0; i < cookieKeys.length; i++) {
if (regexpCookieKeysToMigrate.test(cookieKeys[i])) {
var migratedKey = keyPrefix + cookieKeys[i];
localStorage.setItem(migratedKey, settingsStore.getItem(cookieKeys[i]));
settingsStore.removeItem(cookieKeys[i]);
console.log('- ' + migratedKey);
}
}
console.log('Migration done.');
}
return {
getItem: settingsStore.getItem,
setItem: settingsStore.setItem,
removeItem: settingsStore.removeItem,
hasItem: settingsStore.hasItem,
getCacheNames: getCacheNames,
reset: reset,
getBestAvailableStorageAPI: getBestAvailableStorageAPI
};
});