From 3b76c0c2ba65cd56f3c988e3929198237acf3658 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Wed, 12 Jan 2022 17:00:55 +0000 Subject: [PATCH 1/7] Backport of feature --- service-worker.js | 184 +++++++++++++++++++++--------------- www/css/app.css | 10 ++ www/index.html | 72 ++++++++------ www/js/app.js | 27 ++++-- www/js/init.js | 7 +- www/js/lib/cache.js | 25 +++-- www/js/lib/settingsStore.js | 131 ++++++++++++++++++++++++- 7 files changed, 335 insertions(+), 121 deletions(-) diff --git a/service-worker.js b/service-worker.js index 0c3964e06..efc2df678 100644 --- a/service-worker.js +++ b/service-worker.js @@ -53,7 +53,14 @@ const APP_CACHE = 'kiwix-appCache-' + appVersion; * Caching is on by default but can be turned off by the user in Configuration * @type {Boolean} */ -var useCache = true; +var useAssetsCache = true; + +/** + * A global Boolean that governs whether the APP_CACHE will be used + * This is an expert setting in Configuration + * @type {Boolean} + */ + var useAppCache = true; /** * A Boolean that governs whether images are displayed @@ -254,89 +261,112 @@ self.addEventListener('activate', function (event) { let outgoingMessagePort = null; let fetchCaptureEnabled = false; -self.addEventListener('fetch', intercept); - -// Look up fetch in cache, and if it does not exist, try to get it from the network -function intercept(event) { - // Test if we're in an Electron app - // DEV: Electron uses the file:// protocol and hacks it to work with SW, but it has CORS issues when using the Fetch API to fetch local files, - // so we must bypass it here if we're fetching a local file - if (/^file:/i.test(event.request.url) && ! (regexpZIMUrlWithNamespace.test(event.request.url) && /\.zim\w{0,2}\//i.test(event.request.url))) return; - // console.debug('[SW] Service Worker ' + (event.request.method === "GET" ? 'intercepted ' : 'noted ') + event.request.url, event.request.method); - if (event.request.method !== "GET") return; - // Don't cache download links - if (regexpKiwixDownloadLinks.test(event.request.url)) return; - // Remove any querystring except 'kiwix-display' - var rqUrl = event.request.url.replace(/\?(?!kiwix-display)[^?]+$/i, ''); - // Select cache depending on request format - var cache = /\.zim\//i.test(rqUrl) ? ASSETS_CACHE : APP_CACHE; - if (cache === ASSETS_CACHE && !fetchCaptureEnabled) return; - event.respondWith( +/** + * Intercept selected Fetch requests from the browser window + */ +self.addEventListener('fetch', function intercept(event) { + // Test if we're in an Electron app + // DEV: Electron uses the file:// protocol and hacks it to work with SW, but it has CORS issues when using the Fetch API to fetch local files, + // so we must bypass it here if we're fetching a local file + if (/^file:/i.test(event.request.url) && !(regexpZIMUrlWithNamespace.test(event.request.url) && /\.zim\w{0,2}\//i.test(event.request.url))) return; + // console.debug('[SW] Service Worker ' + (event.request.method === "GET" ? 'intercepted ' : 'noted ') + event.request.url, event.request.method); + if (event.request.method !== "GET") return; + // Don't cache download links + if (regexpKiwixDownloadLinks.test(event.request.url)) return; + // Remove any querystring except 'kiwix-display' + var rqUrl = event.request.url.replace(/\?(?!kiwix-display)[^?]+$/i, ''); + // Select cache depending on request format + var cache = /\.zim\//i.test(rqUrl) ? ASSETS_CACHE : APP_CACHE; + if (cache === ASSETS_CACHE && !fetchCaptureEnabled) return; + event.respondWith( // First see if the content is in the cache fromCache(cache, rqUrl).then(function (response) { // The response was found in the cache so we respond with it return response; }, function () { - // The response was not found in the cache so we look for it on the server - if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(rqUrl)) { - if (imageDisplay !== 'all' && /(^|\/)[IJ]\/.*\.(jpe?g|png|svg|gif|webp)($|[?#])(?!kiwix-display)/i.test(rqUrl)) { - // If the user has disabled the display of images, and the browser wants an image, respond with empty SVG - // A URL with "?kiwix-display" query string acts as a passthrough so that the regex will not match and - // the image will be fetched by app.js - // DEV: If you need to hide more image types, add them to regex below and also edit equivalent regex in app.js - var svgResponse; - if (imageDisplay === 'manual') - svgResponse = ""; - else - svgResponse = ""; - return new Response(svgResponse, { - headers: { - 'Content-Type': 'image/svg+xml' - } - }); - } - return fetchRequestFromZIM(event).then(function (response) { - // Add css or js assets to ASSETS_CACHE (or update their cache entries) unless the URL schema is not supported - if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) && - !regexpExcludedURLSchema.test(event.request.url)) { - event.waitUntil(updateCache(ASSETS_CACHE, event.request, response.clone())); + // The response was not found in the cache so we look for it in the ZIM + // and add it to the cache if it is an asset type (css or js) + if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(rqUrl)) { + if (imageDisplay !== 'all' && /(^|\/)[IJ]\/.*\.(jpe?g|png|svg|gif|webp)($|[?#])(?!kiwix-display)/i.test(rqUrl)) { + // If the user has disabled the display of images, and the browser wants an image, respond with empty SVG + // A URL with "?kiwix-display" query string acts as a passthrough so that the regex will not match and + // the image will be fetched by app.js + // DEV: If you need to hide more image types, add them to regex below and also edit equivalent regex in app.js + var svgResponse; + if (imageDisplay === 'manual') + svgResponse = ""; + else + svgResponse = ""; + return new Response(svgResponse, { + headers: { + 'Content-Type': 'image/svg+xml' + } + }); } - return response; - }).catch(function (msgPortData, title) { - console.error('Invalid message received from app.js for ' + title, msgPortData); - return msgPortData; - }); - } else { - // It's not an asset, or it doesn't match a ZIM URL pattern, so we should fetch it with Fetch API - return fetch(event.request).then(function (response) { - // If request was success, add or update it in the cache - if (!regexpExcludedURLSchema.test(rqUrl) && !/\.zim\w{0,2}$/i.test(rqUrl)) { - event.waitUntil(updateCache(APP_CACHE, event.request, response.clone())); - } - return response; - }).catch(function (error) { - console.debug("[SW] Network request failed and no cache.", error); - }); - } - }) - ); -} + return fetchRequestFromZIM(event).then(function (response) { + // Add css or js assets to ASSETS_CACHE (or update their cache entries) unless the URL schema is not supported + if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) && + !regexpExcludedURLSchema.test(event.request.url)) { + event.waitUntil(updateCache(ASSETS_CACHE, event.request, response.clone())); + } + return response; + }).catch(function (msgPortData, title) { + console.error('Invalid message received from app.js for ' + title, msgPortData); + return msgPortData; + }); + } else { + // It's not an asset, or it doesn't match a ZIM URL pattern, so we should fetch it with Fetch API + return fetch(event.request).then(function (response) { + // If request was successful, add or update it in the cache, but be careful not to cache the ZIM archive itself! + if (!regexpExcludedURLSchema.test(rqUrl) && !/\.zim\w{0,2}$/i.test(rqUrl)) { + event.waitUntil(updateCache(APP_CACHE, event.request, response.clone())); + } + return response; + }).catch(function (error) { + console.debug("[SW] Network request failed and no cache.", error); + }); + } + }) + ); +}); /** - * Handle custom commands 'init' and 'disable' from app.js + * Handle custom commands sent from app.js */ self.addEventListener('message', function (event) { - if (event.data.action === 'init') { - // On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener - outgoingMessagePort = event.ports[0]; - fetchCaptureEnabled = true; - } - if (event.data.action === 'disable') { - // On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener - outgoingMessagePort = null; - fetchCaptureEnabled = false; - self.removeEventListener('fetch', intercept); - } + if (event.data.action) { + if (event.data.action === 'init') { + // On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener + outgoingMessagePort = event.ports[0]; + fetchCaptureEnabled = true; + } else if (event.data.action === 'disable') { + // On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener + outgoingMessagePort = null; + fetchCaptureEnabled = false; + } + var oldValue; + if (event.data.action.assetsCache) { + // Turns caching on or off (a string value of 'enable' turns it on, any other string turns it off) + oldValue = useAssetsCache; + useAssetsCache = event.data.action.assetsCache === 'enable'; + if (useAssetsCache !== oldValue) console.debug('[SW] Use of assetsCache was switched to: ' + event.data.action.assetsCache); + } + if (event.data.action.appCache) { + // Enables or disables use of appCache + oldValue = useAppCache; + useAppCache = event.data.action.appCache === 'enable'; + if (useAppCache !== oldValue) console.debug('[SW] Use of appCache was switched to: ' + event.data.action.appCache); + } + if (event.data.action === 'getCacheNames') { + event.ports[0].postMessage({ 'app': APP_CACHE, 'assets': ASSETS_CACHE }); + } + if (event.data.action.checkCache) { + // Checks and returns the caching strategy: checkCache key should contain a sample URL string to test + testCacheAndCountAssets(event.data.action.checkCache).then(function (cacheArr) { + event.ports[0].postMessage({ type: cacheArr[0], name: cacheArr[1], description: cacheArr[2], count: cacheArr[3] }); + }); + } + } }); /** @@ -424,7 +454,7 @@ function removeUrlParameters(url) { */ function fromCache(cache, requestUrl) { // Prevents use of Cache API if user has disabled it - if (!useCache && cache === ASSETS_CACHE) return Promise.reject('disabled'); + if (!useAppCache && cache === APP_CACHE || !useAssetsCache && cache === ASSETS_CACHE) return Promise.reject('disabled'); return caches.open(cache).then(function (cacheObj) { return cacheObj.match(requestUrl).then(function (matching) { if (!matching || matching.status === 404) { @@ -445,7 +475,7 @@ function fromCache(cache, requestUrl) { */ function updateCache(cache, request, response) { // Prevents use of Cache API if user has disabled it - if (!useCache && cache === ASSETS_CACHE) return Promise.resolve(); + if (!useAppCache && cache === APP_CACHE || !useAssetsCache && cache === ASSETS_CACHE) return Promise.resolve(); return caches.open(cache).then(function (cacheObj) { console.debug('[SW] Adding ' + request.url + ' to ' + cache + '...'); return cacheObj.put(request, response); @@ -460,7 +490,7 @@ function updateCache(cache, request, response) { */ function testCacheAndCountAssets(url) { if (regexpExcludedURLSchema.test(url)) return Promise.resolve(['custom', 'custom', 'Custom', '-']); - if (!useCache) return Promise.resolve(['none', 'none', 'None', 0]); + if (!useAssetsCache) return Promise.resolve(['none', 'none', 'None', 0]); return caches.open(ASSETS_CACHE).then(function (cache) { return cache.keys().then(function (keys) { return ['cacheAPI', ASSETS_CACHE, 'Cache API', keys.length]; diff --git a/www/css/app.css b/www/css/app.css index 16a3ce594..c8bd0982e 100644 --- a/www/css/app.css +++ b/www/css/app.css @@ -295,6 +295,16 @@ footer .btn-xs { z-index:initial !important; } +.btn-danger { + background-color: lightyellow !important; + color: darkred !important; +} + +.btn-danger:hover { + background-color: red !important; + color: lightyellow !important; +} + .darkfooter { background: rgba(34,34,34,0.8) !important; } diff --git a/www/index.html b/www/index.html index 3e3d996e1..6779a9a64 100644 --- a/www/index.html +++ b/www/index.html @@ -983,37 +983,55 @@

Performance settings

-
-
-

Expert settings

-
-
-
Content injection mode
-
-

See About (Expert Settings) for an explanation of the difference between these modes:

- - -
+
+

Expert settings

+
+
Content injection mode
+
+

See About (Expert Settings) for an + explanation of the difference between these modes:

+ + +
+
+

diff --git a/www/js/app.js b/www/js/app.js index 678943695..a3e02a84a 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1201,6 +1201,21 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett } params.themeChanged = true; }); + document.getElementById('btnReset').addEventListener('click', function () { + settingsStore.reset(); + }); + document.getElementById('bypassAppCacheCheck').addEventListener('change', function () { + if (params.contentInjectionMode !== 'serviceworker') { + alert('This setting can only be used in Service Worker mode!'); + this.checked = false; + } else { + params.appCache = !this.checked; + settingsStore.setItem('appCache', params.appCache, Infinity); + settingsStore.reset('cacheAPI'); + } + // This will also send any new values to Service Worker + refreshCacheStatus(); + }); $('input:checkbox[name=hideActiveContentWarning]').on('change', function () { params.hideActiveContentWarning = this.checked ? true : false; settingsStore.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity); @@ -1576,15 +1591,15 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett }); document.getElementById('cachedAssetsModeRadioTrue').addEventListener('change', function (e) { if (e.target.checked) { - settingsStore.setItem('useCache', true, Infinity); - params.useCache = true; + settingsStore.setItem('assetsCache', true, Infinity); + params.assetsCache = true; refreshCacheStatus(); } }); document.getElementById('cachedAssetsModeRadioFalse').addEventListener('change', function (e) { if (e.target.checked) { - settingsStore.setItem('useCache', false, Infinity); - params.useCache = false; + settingsStore.setItem('assetsCache', false, Infinity); + params.assetsCache = false; // Delete all caches cache.clear('all', refreshCacheStatus); } @@ -1839,7 +1854,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett */ function refreshCacheStatus() { // Update radio buttons and checkbox - document.getElementById('cachedAssetsModeRadio' + (params.useCache ? 'True' : 'False')).checked = true; + document.getElementById('cachedAssetsModeRadio' + (params.assetsCache ? 'True' : 'False')).checked = true; // Get cache attributes, then update the UI with the obtained data cache.count(function (c) { document.getElementById('cacheUsed').innerHTML = c.description; @@ -1850,7 +1865,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett // IE11 cannot remove more than one class from a list at a time card.classList.remove('panel-success'); card.classList.remove('panel-warning'); - if (params.useCache) card.classList.add('panel-success'); + if (params.assetsCache) card.classList.add('panel-success'); else card.classList.add('panel-warning'); }); }); diff --git a/www/js/init.js b/www/js/init.js index b33c7262b..eb0360d1e 100644 --- a/www/js/init.js +++ b/www/js/init.js @@ -85,7 +85,8 @@ params['imageDisplay'] = getSetting('imageDisplay') != null ? getSetting('imageD params['manipulateImages'] = getSetting('manipulateImages') != null ? getSetting('manipulateImages') : false; //Makes dataURIs by default instead of BLOB URIs for images params['hideToolbars'] = getSetting('hideToolbars') != null ? getSetting('hideToolbars') : true; //Set default to true (hides both), 'top' (hides top only), or false (no hiding) params['rememberLastPage'] = getSetting('rememberLastPage') != null ? getSetting('rememberLastPage') : true; //Set default option to remember the last visited page between sessions -params['useCache'] = getSetting('useCache') != null ? getSetting('useCache') : true; // Whether to use cache by default or not +params['assetsCache'] = getSetting('assetsCache') != null ? getSetting('assetsCache') : true; // Whether to use cache by default or not +params['appCache'] = getSetting('appCache') !== false; // Will be true by default unless explicitly set to false params['useMathJax'] = getSetting('useMathJax') != null ? getSetting('useMathJax') : true; //Set default to true to display math formulae with MathJax, false to use fallback SVG images only //params['showFileSelectors'] = getCookie('showFileSelectors') != null ? getCookie('showFileSelectors') : false; //Set to true to display hidden file selectors in packaged apps params['showFileSelectors'] = true; //False will cause file selectors to be hidden on each load of the app (by ignoring cookie) @@ -248,6 +249,7 @@ if (/^http/i.test(window.location.protocol) && params.allowInternetAccess === nu document.getElementById('allowInternetAccessCheck').checked = true; params.allowInternetAccess = true; } +document.getElementById('bypassAppCacheCheck').checked = !params.appCache; // If we're in a PWA served from http, change the app titles if (/^http/i.test(window.location.protocol)) { Array.prototype.slice.call(document.querySelectorAll('span.identity')).forEach(function (ele) { @@ -440,9 +442,10 @@ function getBestAvailableStorageAPI() { require.config({ //enforceDefine: true, //This is for debugging IE errors baseUrl: 'js/lib', - config: { '../app': { params: params } }, + // config: { '../app': { params: params } }, paths: { 'jquery': 'jquery-3.2.1.slim', + 'cache' : 'cache', //'jquery': 'jquery-3.2.1', //'bootstrap': 'bootstrap' 'bootstrap': 'bootstrap.min', diff --git a/www/js/lib/cache.js b/www/js/lib/cache.js index 3d4fc1050..117eaa217 100644 --- a/www/js/lib/cache.js +++ b/www/js/lib/cache.js @@ -54,7 +54,6 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) { // Test for Cache API if('caches' in window && /https?:/i.test(window.location.protocol)) { assetsCache.capability = 'cacheAPI|' + assetsCache.capability; - console.log('Cache API is available, but in development in this app'); } else { console.log('CacheAPI is not supported' + (/https?:/i.test(window.location.protocol) ? '' : ' with the ' + window.location.protocol + ' protocol')); @@ -129,6 +128,18 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) { callback({'type': type, 'description': description, 'count': cacheCount}); } }); + // Refresh instructions to Service Worker + if (navigator.serviceWorker && navigator.serviceWorker.controller) { + // Create a Message Channel + var channel = new MessageChannel(); + navigator.serviceWorker.controller.postMessage({ + 'action': { + 'assetsCache': params.assetsCache ? 'enable' : 'disable', + 'appCache': params.appCache ? 'enable' : 'disable', + 'checkCache': window.location.href + } + }, [channel.port2]); + } } /** @@ -327,7 +338,7 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) { function setItem(key, contents, callback) { // Prevent use of storage if user has deselected the option in Configuration // or if the asset is of the wrong type - if (params.useCache === false || !regexpKeyTypes.test(key)) { + if (params.assetsCache === false || !regexpKeyTypes.test(key)) { callback(-1); return; } @@ -434,7 +445,7 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) { if (!/\.css$|\.js$/.test(key)) { document.getElementById('cachingAssets').style.display = 'none'; document.getElementById('searchingArticles').style.display = 'block'; - } else if (params.useCache !== false) { + } else if (params.assetsCache !== false) { var shortTitle = key.replace(/[^/]+\//g, '').substring(0, 18); document.getElementById('cachingAssets').innerHTML = 'Getting ' + shortTitle + '...'; document.getElementById('cachingAssets').style.display = 'block'; @@ -501,7 +512,7 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) { */ function clear(items, callback) { if (!/lastpages|all|reset/.test(items)) { - callback(false); + if (callback) callback(false); return; } // Delete cookie entries with a key containing '.zim' or '.zimaa' etc. followed by article namespace @@ -566,7 +577,7 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) { idxDB('clear', function() { result = result ? result + " (" + itemsCount + " items deleted)" : "no assets to delete"; console.log("cache.clear: " + result); - if (!/^cacheAPI/.test(capability)) callback(itemsCount); + if (!/^cacheAPI/.test(capability) && callback) callback(itemsCount); }); }); } @@ -578,7 +589,7 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) { cacheAPI('clear', function() { result = result ? result + " (" + itemsCount + " items deleted)" : "no assets to delete"; console.log("cache.clear: " + result); - callback(itemsCount); + if (callback) callback(itemsCount); }); }); } @@ -586,7 +597,7 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) { if (!/^cacheAPI|indexedDB/.test(capability)) { result = result ? result + " (" + itemsCount + " items deleted)" : "no assets to delete"; console.log("cache.clear: " + result); - callback(itemsCount); + if (callback) callback(itemsCount); } } diff --git a/www/js/lib/settingsStore.js b/www/js/lib/settingsStore.js index c9f559978..bf50c14c7 100644 --- a/www/js/lib/settingsStore.js +++ b/www/js/lib/settingsStore.js @@ -46,7 +46,7 @@ define([], function () { * @type {Array} */ var deprecatedKeys = [ - 'lastContentInjectionMode', 'lastPageHTML', 'lastPageVisit', 'version' + 'lastContentInjectionMode', 'lastPageHTML', 'lastPageVisit', 'version', 'useCache' ]; /** @@ -95,6 +95,128 @@ define([], function () { return type; } + /** + * Performs a full app reset, deleting all caches and settings + * Or, if a paramter 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) { + // If no specific object was specified, we are doing a general reset, so ask user for confirmation + if (!object && !confirm('WARNING: This will reset the app to a freshly installed state, deleting all app caches and settings!')) return; + + // 1. Clear any remaining cookie entries + if (!object || object === 'cookie') { + var cookieKeys = /(?:^|;)\s*([^=]+)=([^;]*)/ig; + var currentCookie = document.cookie; + var cookieCrumb = cookieKeys.exec(currentCookie); + var cook = false; + while (cookieCrumb !== null) { + // If the cookie key starts with the keyPrefix + if (~params.keyPrefix.indexOf(decodeURIComponent(cookieCrumb[0]))) { + cook = true; + key = cookieCrumb[1]; + // This expiry date will cause the browser to delete the cookie on next page refresh + document.cookie = key + '=;expires=Thu, 21 Sep 1979 00:00:01 UTC;'; + } + cookieCrumb = cookieKeys.exec(currentCookie); + } + if (cook) console.debug('All cookie keys were expiered...'); + } + + // 2. Clear any localStorage settings + if (!object || object === 'localStorage') { + if (params.storeType === 'local_storage') { + localStorage.clear(); + console.debug('All Local Storage settings were deleted...'); + } + } + + // 3. Clear any IndexedDB entries + if (!object || object === 'indexedDB') { + if (/indexedDB/.test(assetsCache.capability)) { + var cache = require('cache'); + cache.clear('reset'); + } + } + + // 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(); + } + }); + } + } + + // 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.reload(); + }, 300); + }; + 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) { @@ -183,6 +305,11 @@ define([], function () { setItem: settingsStore.setItem, removeItem: settingsStore.removeItem, hasItem: settingsStore.hasItem, - getBestAvailableStorageAPI: getBestAvailableStorageAPI + getCacheNames: getCacheNames, + reset: reset, + getBestAvailableStorageAPI: getBestAvailableStorageAPI, + function() { + return require('cache').clear; + } }; }); From e2c1452e5f123b55fd004bd229a891b702dec952 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Wed, 12 Jan 2022 22:07:49 +0000 Subject: [PATCH 2/7] Adaptations for UWP --- service-worker.js | 4 +- www/css/app.css | 10 +++ www/js/lib/settingsStore.js | 117 ++++++++++++++++++------------------ www/js/lib/uiUtil.js | 22 ++++--- 4 files changed, 86 insertions(+), 67 deletions(-) diff --git a/service-worker.js b/service-worker.js index efc2df678..acf295a59 100644 --- a/service-worker.js +++ b/service-worker.js @@ -349,13 +349,13 @@ self.addEventListener('fetch', function intercept(event) { // Turns caching on or off (a string value of 'enable' turns it on, any other string turns it off) oldValue = useAssetsCache; useAssetsCache = event.data.action.assetsCache === 'enable'; - if (useAssetsCache !== oldValue) console.debug('[SW] Use of assetsCache was switched to: ' + event.data.action.assetsCache); + if (useAssetsCache !== oldValue) console.debug('[SW] Use of assetsCache was switched to: ' + useAssetsCache); } if (event.data.action.appCache) { // Enables or disables use of appCache oldValue = useAppCache; useAppCache = event.data.action.appCache === 'enable'; - if (useAppCache !== oldValue) console.debug('[SW] Use of appCache was switched to: ' + event.data.action.appCache); + if (useAppCache !== oldValue) console.debug('[SW] Use of appCache was switched to: ' + useAppCache); } if (event.data.action === 'getCacheNames') { event.ports[0].postMessage({ 'app': APP_CACHE, 'assets': ASSETS_CACHE }); diff --git a/www/css/app.css b/www/css/app.css index c8bd0982e..672bc19c3 100644 --- a/www/css/app.css +++ b/www/css/app.css @@ -300,11 +300,21 @@ footer .btn-xs { color: darkred !important; } +.dark .btn-danger { + background-color: darkslategrey !important; + color: gainsboro !important; +} + .btn-danger:hover { background-color: red !important; color: lightyellow !important; } +.dark .btn-danger:hover { + background-color: darkred !important; + color: lightyellow !important; +} + .darkfooter { background: rgba(34,34,34,0.8) !important; } diff --git a/www/js/lib/settingsStore.js b/www/js/lib/settingsStore.js index bf50c14c7..219d0286b 100644 --- a/www/js/lib/settingsStore.js +++ b/www/js/lib/settingsStore.js @@ -97,70 +97,76 @@ define([], function () { /** * Performs a full app reset, deleting all caches and settings - * Or, if a paramter is supplied, deletes or disables the object + * 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) { - // If no specific object was specified, we are doing a general reset, so ask user for confirmation - if (!object && !confirm('WARNING: This will reset the app to a freshly installed state, deleting all app caches and settings!')) return; - - // 1. Clear any remaining cookie entries - if (!object || object === 'cookie') { - var cookieKeys = /(?:^|;)\s*([^=]+)=([^;]*)/ig; - var currentCookie = document.cookie; - var cookieCrumb = cookieKeys.exec(currentCookie); - var cook = false; - while (cookieCrumb !== null) { - // If the cookie key starts with the keyPrefix - if (~params.keyPrefix.indexOf(decodeURIComponent(cookieCrumb[0]))) { - cook = true; - key = cookieCrumb[1]; - // This expiry date will cause the browser to delete the cookie on next page refresh - document.cookie = key + '=;expires=Thu, 21 Sep 1979 00:00:01 UTC;'; + 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); } - cookieCrumb = cookieKeys.exec(currentCookie); + if (foundCrumb) console.debug('All cookie keys were expired...'); } - if (cook) console.debug('All cookie keys were expiered...'); - } - // 2. Clear any localStorage settings - if (!object || object === 'localStorage') { - if (params.storeType === 'local_storage') { - localStorage.clear(); - console.debug('All Local Storage settings were deleted...'); + // 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)) { - var cache = require('cache'); - cache.clear('reset'); + // 3. Clear any IndexedDB entries + if (!object || object === 'indexedDB') { + if (/indexedDB/.test(assetsCache.capability)) { + var cache = require('cache'); + cache.clear('reset'); + } } - } - // 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(); - } - }); + // 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(); } - } 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(); - } + }); + } + }; + var uiUtil = require('uiUtil'); + // 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 and settings!', + 'Warning!', 'OK', performReset, 'Cancel', + function () { + console.debug('User cancelled'); }); } } @@ -307,9 +313,6 @@ define([], function () { hasItem: settingsStore.hasItem, getCacheNames: getCacheNames, reset: reset, - getBestAvailableStorageAPI: getBestAvailableStorageAPI, - function() { - return require('cache').clear; - } + getBestAvailableStorageAPI: getBestAvailableStorageAPI }; }); diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index 0d99a34b9..ee957a6f2 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -575,20 +575,26 @@ define(rqDef, function(util) { * * @param {String} message The message to display * @param {String} title The message title - * @param {String} btn1 An optional button to display - * @param {Function} btn1Func An optional function to run when btn1 is selected - * @param {String} btn2 An optional secondary button to display - * @param {Function} btn2Func An optional function to run when btn2 is selected + * @param {String} btnOK An optional button to display with an OK function + * @param {Function} btnOKFn An optional function to run when btnOK is selected + * @param {String} btnCancel An optional secondary button to display with a cancel function + * @param {Function} btnCancelFn An optional function to run when btnCancel is selected */ - function systemAlert(message, title, btn1, btn1Func, btn2, btn2Func) { + function systemAlert(message, title, btnOK, btnOKFn, btnCancel, btnCancelFn) { // Test for UWP if (typeof Windows !== 'undefined' && typeof Windows.UI !== 'undefined' && typeof Windows.UI.Popups !== 'undefined') { var dialog = new Windows.UI.Popups.MessageDialog(message); - if (btn1 && btn1Func) dialog.commands.append(new Windows.UI.Popups.UICommand(btn1, btn1Func)); - if (btn2 && btn2Func) dialog.commands.append(new Windows.UI.Popups.UICommand(btn2, btn2Func)); + if (btnOK && btnOKFn) dialog.commands.append(new Windows.UI.Popups.UICommand(btnOK, btnOKFn)); + if (btnCancel && btnCancelFn) dialog.commands.append(new Windows.UI.Popups.UICommand(btnCancel, btnCancelFn)); dialog.showAsync(); } else { - alert(message); + if (btnOK && btnOKFn || btnCancel && btnCancelFn) { + var response = confirm(message); + if (response) btnOKFn(); + else if (btnCancel && btnCancelFn) btnCancelFn(); + } else { + alert(message); + } } } From a9102685f6bcf1200e25f2c8610a45231704e069 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Thu, 13 Jan 2022 06:01:03 +0000 Subject: [PATCH 3/7] Update app.js --- www/js/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/js/app.js b/www/js/app.js index a3e02a84a..63abe2659 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -1206,7 +1206,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett }); document.getElementById('bypassAppCacheCheck').addEventListener('change', function () { if (params.contentInjectionMode !== 'serviceworker') { - alert('This setting can only be used in Service Worker mode!'); + uiUtil.systemAlert('This setting can only be used in Service Worker mode!'); this.checked = false; } else { params.appCache = !this.checked; From e0f5a556faf3cdbc3f3b37c12dba89c8ca231ef2 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Thu, 13 Jan 2022 14:23:00 +0000 Subject: [PATCH 4/7] Prevent user switching to jQuery mode if Bypass App Cache is selected --- www/index.html | 46 +++++++++++++++++++++++----------------------- www/js/app.js | 13 +++++++++++-- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/www/index.html b/www/index.html index 6779a9a64..0c0fb42ec 100644 --- a/www/index.html +++ b/www/index.html @@ -280,7 +280,7 @@

  • Changelog and Features
  • Title search syntax and alphabetical ZIM Archive Index
  • Link handling, image download, tabs and windows
  • -
  • Expert or Experimental Settings
  • +
  • Technical Information
  • Privacy Policy
  • Feedback and Support
  • Credits and Licence
  • @@ -325,7 +325,7 @@

    Step 1: Download some content

    You can browse the Kiwix repository on the Configuration page, and the app will provide instructions according to the type of file you download. Alternatively, visit https://download.kiwix.org/zim/ using a regular computer. To use all the features of - ZIM archives that have dynamic content (e.g. PhET), you may need to switch the app to the experimental PWA/ServiceWorker mode (see Expert Settings in + ZIM archives that have dynamic content (e.g. PhET), you may need to switch the app to the experimental PWA/ServiceWorker mode (see Compatibility settings in configuration, but this mode is not available on Windows Mobile or on old browsers).

    Step 2: Copy the content onto your device

    @@ -451,7 +451,7 @@

    Link handling, image download, tabs and windows

    works smoothly with every option.

    The app can usually operate in two modes: "JQuery" (which manipulates the content of the article) and "ServiceWorker" (which tries to get out of the way and let the ZIM do its - thing - see below under Expert settings for a fuller explanation). Some contexts, like mobile or old browsers, only support JQuery mode. Desktop systems + thing - see below under Technical information for a fuller explanation). Some contexts, like mobile or old browsers, only support JQuery mode. Desktop systems and modern browsers support both modes. JQuery mode doesn't support active or dynamic content. You will usually get an Active Content Warning popping up at the top of the landing page (Home) if you open a ZIM that requires you to switch to ServiceWorker mode. If you can't switch, you can often still access content in these archives by using title search or typing a space in title search to show the ZIM Archive Index (see above), but some things, especially any proprietary User Interface, @@ -489,10 +489,10 @@

    Link handling, image download, tabs and windows

    -

    Expert or experimental settings

    +

    Technical information

    Depending on your browser or framework, this app may be capable of running in two different modes, which we call - "JQuery Mode" and "ServiceWorker Mode" for short. There is a toggle under Expert Settings + "JQuery Mode" and "ServiceWorker Mode" for short. There is a toggle under Compatibility settings in Configuration. Here is an explanation of what these modes do:

    • @@ -858,7 +858,7 @@

      Display settings

    -
    -

    Expert settings

    -
    +

    Compatibility settings

    +
    Content injection mode
    -

    See About (Expert Settings) for an - explanation of the difference between these modes:

    +

    See About (Technical settings) for an explanation of the difference between these modes:

    -