diff --git a/KiwixWebApp.jsproj b/KiwixWebApp.jsproj index 9cc86166..f6123867 100644 --- a/KiwixWebApp.jsproj +++ b/KiwixWebApp.jsproj @@ -299,7 +299,7 @@ - + diff --git a/pwabuilder-sw.js b/pwabuilder-sw.js index 014295f9..96afb347 100644 --- a/pwabuilder-sw.js +++ b/pwabuilder-sw.js @@ -51,7 +51,7 @@ const precacheFiles = [ "www/js/init.js", "www/js/lib/bootstrap.js", "www/js/lib/bootstrap.min.js", - "www/js/lib/cookies.js", + "www/js/lib/settingsStore.js", "www/js/lib/filecache.js", "www/js/lib/images.js", "www/js/lib/jquery-3.2.1.slim.js", diff --git a/www/js/init.js b/www/js/init.js index f53b4df0..c44ce53b 100644 --- a/www/js/init.js +++ b/www/js/init.js @@ -56,7 +56,7 @@ params['fileVersion'] = "wikipedia_en_100_maxi_2020-12.zim (23-Dec-2020)"; //Use params['cachedStartPage'] = false; //If you have cached the start page for quick start, give its URI here params['kiwixDownloadLink'] = "https://download.kiwix.org/zim/"; //Include final slash -params['cookieSupport'] = checkCookies(); +params['storeType'] = checkCookies(); params['maxResults'] = ~~(getCookie('maxResults') || 25); //Number of search results to display params['relativeFontSize'] = ~~(getCookie('relativeFontSize') || 100); //Sets the initial font size for articles (as a percentage) - user can adjust using zoom buttons params['relativeUIFontSize'] = ~~(getCookie('relativeUIFontSize') || 100); //Sets the initial font size for UI (as a percentage) - user can adjust using slider in Config @@ -92,7 +92,7 @@ params['localStorage'] = params['localStorage'] || ""; params['pickedFile'] = launchArguments ? launchArguments.files[0] : ""; params['pickedFolder'] = params['pickedFolder'] || ""; params['lastPageVisit'] = getCookie('lastPageVisit') || ""; -params['lastPageVisit'] = params['lastPageVisit'] ? decodeURIComponent(params['lastPageVisit']) : ""; +params.lastPageVisit = params.lastPageVisit ? decodeURIComponent(params.lastPageVisit): ""; params['themeChanged'] = params['themeChanged'] || false; params['allowInternetAccess'] = params['allowInternetAccess'] || false; //Do not get value from cookie, should be explicitly set by user on a per-session basis params['printIntercept'] = false; @@ -146,10 +146,6 @@ document.getElementById('hideToolbarsCheck').indeterminate = params.hideToolbars document.getElementById('hideToolbarsCheck').readOnly = params.hideToolbars === "top"; document.getElementById('hideToolbarsState').innerHTML = (params.hideToolbars === "top" ? "top" : params.hideToolbars ? "both" : "never"); -//Set up packaged Electron app -if (!params.pickedFile && params.storedFile && typeof window.fs !== 'undefined') { - params.pickedFile = params.storedFile; -} //Set up storage types if (params.storedFile && typeof Windows !== 'undefined' && typeof Windows.Storage !== 'undefined') { //UWP Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync(params.archivePath).done(function (folder) { @@ -245,7 +241,7 @@ window.addEventListener('appinstalled', function(e) { function getCookie(name) { var result; - if (params.cookieSupport == 'cookie') { + if (params.storeType == 'cookie') { var regexp = new RegExp('(?:^|;)\\s*' + name + '=([^;]+)(?:;|$)'); result = document.cookie.match(regexp); result = result && result.length > 1 ? result[1] : null; diff --git a/www/js/lib/cookies.js b/www/js/lib/cookies.js deleted file mode 100644 index ba0c611e..00000000 --- a/www/js/lib/cookies.js +++ /dev/null @@ -1,117 +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() -|*| -\*/ - -// Test for cookie support -var storeType = 'cookie'; -document.cookie = 'kiwixCookie=working;expires=Fri, 31 Dec 9999 23:59:59 GMT'; -var kiwixCookie = /kiwixCookie=working/i.test(document.cookie); -if (kiwixCookie) { - document.cookie = 'kiwixCookie=broken;expires=Fri, 31 Dec 9999 23:59:59 GMT'; - kiwixCookie = !/kiwixCookie=working/i.test(document.cookie); -} -document.cookie = 'kiwixCookie=;expires=Thu, 01 Jan 1970 00:00:00 GMT'; -if (!kiwixCookie) { - // Cookies appear to be blocked, so test for localStorage support - var result = false; - try { - result = 'localStorage' in window && window['localStorage'] !== null; - } catch (e) { - console.log('LocalStorage is not supported!'); - } - if (result) storeType = 'local_storage'; -} -console.log('Test2: storeType: ' + storeType); - -var docCookies = { - 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; - } -}; - - return { - getItem: docCookies.getItem, - setItem: docCookies.setItem, - removeItem: docCookies.removeItem, - hasItem: docCookies.hasItem - }; -}); \ 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 00000000..bc076c3d --- /dev/null +++ b/www/js/lib/settingsStore.js @@ -0,0 +1,171 @@ +'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 settingsStore. + * + * 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([ + 'lastContentInjectionMode', 'lastSelectedArchivePath', 'imageDisplay', 'useMathJax', 'version', 'lastSelectedArchive', + 'listOfArchives', 'lastPageVisit', 'cssUITheme', 'cssTheme', 'cssSource', 'removePageMaxWidth', 'hideActiveContentWarning', + 'allowHTMLExtraction' + ].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; 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(); + // 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 + }; +});