'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 }; });