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