diff --git a/nbproject/project.properties b/nbproject/project.properties
index cee96d90b..da5520123 100644
--- a/nbproject/project.properties
+++ b/nbproject/project.properties
@@ -1,5 +1,5 @@
auxiliary.org-netbeans-modules-javascript2-requirejs.enabled=true
-auxiliary.org-netbeans-modules-javascript2-requirejs.mappings={zimArchiveLoader , www/js/lib/zimArchiveLoader.js}{cookies , www/js/lib/cookies.js}{jquery , www/js/lib/jquery-3.2.1.slim.js}{abstractFilesystemAccess , www/js/lib/abstractFilesystemAccess.js}{q , www/js/lib/q.js}{uiUtil , www/js/lib/uiUtil.js}{utf8 , www/js/lib/utf8.js}{util , www/js/lib/util.js}{xzdec_wrapper , www/js/lib/xzdec_wrapper.js}{zimArchive , www/js/lib/zimArchive.js}{zimDirEntry , www/js/lib/zimDirEntry.js}{zimfile , www/js/lib/zimfile.js}
+auxiliary.org-netbeans-modules-javascript2-requirejs.mappings={zimArchiveLoader , www/js/lib/zimArchiveLoader.js}{settingsStore , www/js/lib/settingsStore.js}{jquery , www/js/lib/jquery-3.2.1.slim.js}{abstractFilesystemAccess , www/js/lib/abstractFilesystemAccess.js}{q , www/js/lib/q.js}{uiUtil , www/js/lib/uiUtil.js}{utf8 , www/js/lib/utf8.js}{util , www/js/lib/util.js}{xzdec_wrapper , www/js/lib/xzdec_wrapper.js}{zimArchive , www/js/lib/zimArchive.js}{zimDirEntry , www/js/lib/zimDirEntry.js}{zimfile , www/js/lib/zimfile.js}
config.folder=
file.reference.git-kiwix-js=.
files.encoding=UTF-8
diff --git a/www/index.html b/www/index.html
index 35284f925..130ab4695 100644
--- a/www/index.html
+++ b/www/index.html
@@ -339,6 +339,7 @@
Expert settings
diff --git a/www/js/app.js b/www/js/app.js
index 843119850..f5836a606 100644
--- a/www/js/app.js
+++ b/www/js/app.js
@@ -26,8 +26,8 @@
// This uses require.js to structure javascript:
// http://requirejs.org/docs/api.html#define
-define(['jquery', 'zimArchiveLoader', 'uiUtil', 'cookies','abstractFilesystemAccess','q'],
- function($, zimArchiveLoader, uiUtil, cookies, abstractFilesystemAccess, Q) {
+define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesystemAccess','q'],
+ function($, zimArchiveLoader, uiUtil, settingsStore, abstractFilesystemAccess, Q) {
/**
* Maximum number of articles to display in a search
@@ -65,16 +65,17 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'cookies','abstractFilesystemAcc
*/
var selectedArchive = null;
- // Set parameters and associated UI elements from cookie
+ // Set parameters and associated UI elements from the Settings Store
// DEV: The params global object is declared in init.js so that it is available to modules
- params['hideActiveContentWarning'] = cookies.getItem('hideActiveContentWarning') === 'true';
- params['showUIAnimations'] = cookies.getItem('showUIAnimations') ? cookies.getItem('showUIAnimations') === 'true' : true;
+ params['storeType'] = settingsStore.getBestAvailableStorageAPI(); // A parameter to determine the Settings Store API in use
+ params['hideActiveContentWarning'] = settingsStore.getItem('hideActiveContentWarning') === 'true';
+ params['showUIAnimations'] = settingsStore.getItem('showUIAnimations') ? settingsStore.getItem('showUIAnimations') === 'true' : true;
document.getElementById('hideActiveContentWarningCheck').checked = params.hideActiveContentWarning;
document.getElementById('showUIAnimationsCheck').checked = params.showUIAnimations;
// A global parameter that turns caching on or off and deletes the cache (it defaults to true unless explicitly turned off in UI)
- params['useCache'] = cookies.getItem('useCache') !== 'false';
+ params['useCache'] = settingsStore.getItem('useCache') !== 'false';
// A parameter to set the app theme and, if necessary, the CSS theme for article content (defaults to 'light')
- params['appTheme'] = cookies.getItem('appTheme') || 'light'; // Currently implemented: light|dark|dark_invert|dark_mwInvert
+ params['appTheme'] = settingsStore.getItem('appTheme') || 'light'; // Currently implemented: light|dark|dark_invert|dark_mwInvert
document.getElementById('appThemeSelect').value = params.appTheme;
uiUtil.applyAppTheme(params.appTheme);
@@ -322,27 +323,27 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'cookies','abstractFilesystemAcc
});
$('input:checkbox[name=hideActiveContentWarning]').on('change', function (e) {
params.hideActiveContentWarning = this.checked ? true : false;
- cookies.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity);
+ settingsStore.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity);
});
$('input:checkbox[name=showUIAnimations]').on('change', function (e) {
params.showUIAnimations = this.checked ? true : false;
- cookies.setItem('showUIAnimations', params.showUIAnimations, Infinity);
+ settingsStore.setItem('showUIAnimations', params.showUIAnimations, Infinity);
});
document.getElementById('appThemeSelect').addEventListener('change', function (e) {
params.appTheme = e.target.value;
- cookies.setItem('appTheme', params.appTheme, Infinity);
+ settingsStore.setItem('appTheme', params.appTheme, Infinity);
uiUtil.applyAppTheme(params.appTheme);
});
document.getElementById('cachedAssetsModeRadioTrue').addEventListener('change', function (e) {
if (e.target.checked) {
- cookies.setItem('useCache', true, Infinity);
+ settingsStore.setItem('useCache', true, Infinity);
params.useCache = true;
refreshCacheStatus();
}
});
document.getElementById('cachedAssetsModeRadioFalse').addEventListener('change', function (e) {
if (e.target.checked) {
- cookies.setItem('useCache', false, Infinity);
+ settingsStore.setItem('useCache', false, Infinity);
params.useCache = false;
// Delete all caches
resetCssCache();
@@ -385,8 +386,16 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'cookies','abstractFilesystemAcc
$('#serviceWorkerStatus').removeClass("apiAvailable apiUnavailable")
.addClass("apiUnavailable");
}
- apiStatusPanel.classList.add(apiPanelClass);
+ // Update Settings Store section of API panel with API name
+ var settingsStoreStatusDiv = document.getElementById('settingsStoreStatus');
+ var apiName = params.storeType === 'cookie' ? 'Cookie' : params.storeType === 'local_storage' ? 'Local Storage' : 'None';
+ settingsStoreStatusDiv.innerHTML = 'Settings Storage API in use: ' + apiName;
+ settingsStoreStatusDiv.classList.remove('apiAvailable', 'apiUnavailable');
+ settingsStoreStatusDiv.classList.add(params.storeType === 'none' ? 'apiUnavailable' : 'apiAvailable');
+ apiPanelClass = params.storeType === 'none' ? 'card-warning' : apiPanelClass;
+ // Add a warning colour to the API Status Panel if any of the above tests failed
+ apiStatusPanel.classList.add(apiPanelClass);
}
/**
@@ -560,13 +569,13 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'cookies','abstractFilesystemAcc
$('input:radio[name=contentInjectionMode]').prop('checked', false);
$('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true);
contentInjectionMode = value;
- // Save the value in a cookie, so that to be able to keep it after a reload/restart
- cookies.setItem('lastContentInjectionMode', value, Infinity);
+ // Save the value in the Settings Store, so that to be able to keep it after a reload/restart
+ settingsStore.setItem('lastContentInjectionMode', value, Infinity);
refreshCacheStatus();
}
- // At launch, we try to set the last content injection mode (stored in a cookie)
- var lastContentInjectionMode = cookies.getItem('lastContentInjectionMode');
+ // At launch, we try to set the last content injection mode (stored in Settings Store)
+ var lastContentInjectionMode = settingsStore.getItem('lastContentInjectionMode');
if (lastContentInjectionMode) {
setContentInjectionMode(lastContentInjectionMode);
}
@@ -620,10 +629,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'cookies','abstractFilesystemAcc
*/
var storages = [];
function searchForArchivesInPreferencesOrStorage() {
- // First see if the list of archives is stored in the cookie
- var listOfArchivesFromCookie = cookies.getItem("listOfArchives");
- if (listOfArchivesFromCookie !== null && listOfArchivesFromCookie !== undefined && listOfArchivesFromCookie !== "") {
- var directories = listOfArchivesFromCookie.split('|');
+ // First see if the list of archives is stored in the Settings Store
+ var listOfArchivesFromSettingsStore = settingsStore.getItem("listOfArchives");
+ if (listOfArchivesFromSettingsStore !== null && listOfArchivesFromSettingsStore !== undefined && listOfArchivesFromSettingsStore !== "") {
+ var directories = listOfArchivesFromSettingsStore.split('|');
populateDropDownListOfArchives(directories);
}
else {
@@ -706,12 +715,12 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'cookies','abstractFilesystemAcc
comboArchiveList.options[i] = new Option(archiveDirectory, archiveDirectory);
}
}
- // Store the list of archives in a cookie, to avoid rescanning at each start
- cookies.setItem("listOfArchives", archiveDirectories.join('|'), Infinity);
+ // Store the list of archives in the Settings Store, to avoid rescanning at each start
+ settingsStore.setItem("listOfArchives", archiveDirectories.join('|'), Infinity);
$('#archiveList').on('change', setLocalArchiveFromArchiveList);
if (comboArchiveList.options.length > 0) {
- var lastSelectedArchive = cookies.getItem("lastSelectedArchive");
+ var lastSelectedArchive = settingsStore.getItem("lastSelectedArchive");
if (lastSelectedArchive !== null && lastSelectedArchive !== undefined && lastSelectedArchive !== "") {
// Attempt to select the corresponding item in the list, if it exists
if ($("#archiveList option[value='"+lastSelectedArchive+"']").length > 0) {
@@ -770,7 +779,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'cookies','abstractFilesystemAcc
}
resetCssCache();
selectedArchive = zimArchiveLoader.loadArchiveFromDeviceStorage(selectedStorage, archiveDirectory, function (archive) {
- cookies.setItem("lastSelectedArchive", archiveDirectory, Infinity);
+ settingsStore.setItem("lastSelectedArchive", archiveDirectory, Infinity);
// The archive is set : go back to home page to start searching
$("#btnHome").click();
});
diff --git a/www/js/lib/cookies.js b/www/js/lib/cookies.js
deleted file mode 100644
index 87dc4475f..000000000
--- a/www/js/lib/cookies.js
+++ /dev/null
@@ -1,69 +0,0 @@
-'use strict';
-define([], function() {
-/*\
-|*|
-|*| :: cookies.js ::
-|*|
-|*| A complete cookies reader/writer framework with full unicode support.
-|*|
-|*| https://developer.mozilla.org/en-US/docs/DOM/document.cookie
-|*|
-|*| This framework is released under the GNU Public License, version 3 or later.
-|*| http://www.gnu.org/licenses/gpl-3.0-standalone.html
-|*|
-|*| Syntaxes:
-|*|
-|*| * docCookies.setItem(name, value[, end[, path[, domain[, secure]]]])
-|*| * docCookies.getItem(name)
-|*| * docCookies.removeItem(name[, path])
-|*| * docCookies.hasItem(name)
-|*| * docCookies.keys()
-|*|
-\*/
-
-var docCookies = {
- getItem: function (sKey) {
- return unescape(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
- },
- setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
- 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.toGMTString();
- break;
- }
- }
- document.cookie = escape(sKey) + "=" + escape(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
- return true;
- },
- removeItem: function (sKey, sPath) {
- if (!sKey || !this.hasItem(sKey)) { return false; }
- document.cookie = escape(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + (sPath ? "; path=" + sPath : "");
- return true;
- },
- hasItem: function (sKey) {
- return (new RegExp("(?:^|;\\s*)" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
- },
- keys: /* optional method: you can safely remove it! */ function () {
- var aKeys = document.cookie.replace(/((?:^|\s*;)[^\=]+)(?=;|$)|^\s*|\s*(?:\=[^;]*)?(?:\1|$)/g, "").split(/\s*(?:\=[^;]*)?;\s*/);
- for (var nIdx = 0; nIdx < aKeys.length; nIdx++) { aKeys[nIdx] = unescape(aKeys[nIdx]); }
- return aKeys;
- }
-};
-
-return {
- getItem: docCookies.getItem,
- setItem: docCookies.setItem,
- removeItem: docCookies.removeItem,
- hasItem: docCookies.hasItem,
- keys: docCookies.keys
- };
-});
\ No newline at end of file
diff --git a/www/js/lib/settingsStore.js b/www/js/lib/settingsStore.js
new file mode 100644
index 000000000..51cc34aae
--- /dev/null
+++ b/www/js/lib/settingsStore.js
@@ -0,0 +1,170 @@
+'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([
+ 'hideActiveContentWarning', 'showUIAnimations', 'appTheme', 'useCache',
+ 'lastContentInjectionMode', 'listOfArchives', 'lastSelectedArchive'
+ ].join('|'));
+
+ /**
+ * A constant to set 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
+ * @type {String}
+ */
+ const keyPrefix = 'kiwixjs-';
+
+ // 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';
+ 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';
+ 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();
+ // 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;
+ }
+
+ 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,
+ getBestAvailableStorageAPI: getBestAvailableStorageAPI
+ };
+});
\ No newline at end of file