'use strict'; /* global params, assetsCache */ /* eslint-disable indent */ import uiUtil from './uiUtil.js'; 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)) { window.indexedDB.deleteDatabase(params.indexedDB); console.debug('All IndexedDB entries were deleted...'); } } // 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(); } }); } // 5. Clear any Origin Private File System Archives // DEV: Method is currently behind a flag, so wait till fully implemented // if (!object || object === 'OPFS') { // if (navigator && navigator.storage && 'getDirectory' in navigator.storage) { // navigator.storage.getDirectory().then(function (handle) { // handle.remove({ recursive: true }).then(function () { // console.debug('All OPFS archives were deleted...'); // }); // }); // } // } }; // 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,' + // ' Archives stored in the Private File System,' + ' and settings! (Archives stored in the OPFS will be preserved.)

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 }, 600); }; // 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; } }, _cookieKeys: function () { // Disabling linter check because this is library code // eslint-disable-next-line no-useless-backreference 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.'); } export default { getItem: settingsStore.getItem, setItem: settingsStore.setItem, removeItem: settingsStore.removeItem, hasItem: settingsStore.hasItem, getCacheNames: getCacheNames, reset: reset, getBestAvailableStorageAPI: getBestAvailableStorageAPI };