mirror of
https://github.com/kiwix/kiwix-js-pwa.git
synced 2025-08-03 19:38:36 -04:00
314 lines
12 KiB
JavaScript
314 lines
12 KiB
JavaScript
'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('<p><b>WARNING:</b> This will reset the app to a freshly installed state, deleting all app caches,' +
|
|
// ' <b>Archives stored in the Private File System</b>,' +
|
|
' and settings! (Archives stored in the OPFS will be preserved.)<b></p><p>Make sure you have an Internet connection</b>' +
|
|
' if this is an offline PWA, because it will be erased and reloaded.</p>', '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
|
|
};
|