Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide methods for resetting the app and bypassing appCache #797

Merged
merged 24 commits into from
Jan 16, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,15 @@ const APP_CACHE = 'kiwixjs-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 regular expression that matches the Content-Types of assets that may be stored in ASSETS_CACHE
Expand Down Expand Up @@ -231,10 +239,18 @@ self.addEventListener('message', function (event) {
outgoingMessagePort = null;
fetchCaptureEnabled = false;
}
if (event.data.action.useCache) {
// Turns caching on or off (a string value of 'on' turns it on, any other string turns it off)
useCache = event.data.action.useCache === 'on';
console.debug('[SW] Caching was turned ' + event.data.action.useCache);
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);
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
}
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);
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
}
if (event.data.action === 'getCacheNames') {
event.ports[0].postMessage({ 'app': APP_CACHE, 'assets': ASSETS_CACHE });
Expand Down Expand Up @@ -328,7 +344,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) {
Expand All @@ -349,7 +365,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);
Expand All @@ -364,7 +380,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];
Expand Down
5 changes: 5 additions & 0 deletions www/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ button {
margin: 2px;
}

.btn-danger {
background-color: lightyellow;
color: darkred;
}

@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
Expand Down
16 changes: 16 additions & 0 deletions www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,22 @@ <h3>Expert settings</h3>
</div>
</div>
</div>
<div class="card card-danger card-footer">
<div class="row">
<div class="col-sm-5">
<label title="This will return the app to its original settings on launch, and will empty all caches and settings and deregister Service Workers. The app will then reload.">
<button type="button" class="btn btn-danger" id="btnReset">Reset</button> Full app reset
</label>
</div>
<div class="col-sm-7">
<label
title="WARNING: Leaving this checked will prevent offline usage of the PWA. Setting will clear all existing Cache API caches, but assetsCache will be used unless also disabled above. For testing new code with the PWA.">
<input type="checkbox" name="bypassAppCache" id="bypassAppCacheCheck">
<b>Bypass AppCache</b> (<i>disables offline use of PWA!</i>)
</label>
</div>
</div>
</div>
</div>
<br />
<div class="container">
Expand Down
49 changes: 34 additions & 15 deletions www/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// Maximum number of article titles to return (range is 5 - 50, default 25)
params['maxSearchResultsSize'] = settingsStore.getItem('maxSearchResultsSize') || 25;
// 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'] = settingsStore.getItem('useCache') !== 'false';
params['assetsCache'] = settingsStore.getItem('assetsCache') !== 'false';
// A global parameter that disables use of the appCache (it defaults to true unless explicitly turned off in UI)
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
params['appCache'] = settingsStore.getItem('appCache') !== 'false';
// A parameter to set the app theme and, if necessary, the CSS theme for article content (defaults to 'light')
params['appTheme'] = settingsStore.getItem('appTheme') || 'light'; // Currently implemented: light|dark|dark_invert|dark_mwInvert
// A global parameter to turn on/off the use of Keyboard HOME Key to focus search bar
Expand Down Expand Up @@ -157,6 +159,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
uiUtil.applyAppTheme(params.appTheme);
document.getElementById('useHomeKeyToFocusSearchBarCheck').checked = params.useHomeKeyToFocusSearchBar;
switchHomeKeyToFocusSearchBar();
document.getElementById('bypassAppCacheCheck').checked = !params.appCache;
document.getElementById('appVersion').innerHTML = 'Kiwix ' + params.appVersion;
setContentInjectionMode(params.contentInjectionMode);

Expand Down Expand Up @@ -411,6 +414,21 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// Do the necessary to enable or disable the Service Worker
setContentInjectionMode(this.value);
});
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);
Expand All @@ -431,15 +449,15 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
});
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
resetCssCache();
if ('caches' in window) caches.delete(ASSETS_CACHE);
Expand Down Expand Up @@ -574,28 +592,29 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// Ask Service Worker for its cache status and asset count
navigator.serviceWorker.controller.postMessage({
'action': {
'useCache': params.useCache ? 'on' : 'off',
'assetsCache': params.assetsCache ? 'enable' : 'disable',
'appCache': params.appCache ? 'enable' : 'disable',
'checkCache': window.location.href
}
}, [channel.port2]);
} else {
// No Service Worker has been established, so we resolve the Promise with cssCache details only
resolve({
'type': params.useCache ? 'memory' : 'none',
'type': params.assetsCache ? 'memory' : 'none',
'name': 'cssCache',
'description': params.useCache ? 'Memory' : 'None',
'description': params.assetsCache ? 'Memory' : 'None',
'count': cssCache.size
});
}
});
}

/**
* Refreshes the UI (Configuration) with the cache attributes obtained from getCacheAttributes()
* Refreshes the UI (Configuration) with the cache attributes obtained from getAssetsCacheAttributes()
*/
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
getAssetsCacheAttributes().then(function (cache) {
if (cache.type === 'cacheAPI' && ASSETS_CACHE !== cache.name) {
Expand All @@ -609,7 +628,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// IE11 cannot remove more than one class from a list at a time
card.classList.remove('card-success');
card.classList.remove('card-warning');
if (params.useCache) card.classList.add('card-success');
if (params.assetsCache) card.classList.add('card-success');
else card.classList.add('card-warning');
});
});
Expand Down Expand Up @@ -692,7 +711,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var channel = new MessageChannel();
if (isServiceWorkerAvailable() && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
'action': { 'useCache': 'off' }
'action': { 'assetsCache': 'disable' }
}, [channel.port2]);
}
caches.delete(ASSETS_CACHE);
Expand Down Expand Up @@ -1698,13 +1717,13 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
uiUtil.replaceCSSLinkWithInlineCSS(link, cssContent);
cssFulfilled++;
} else {
if (params.useCache) $('#cachingAssets').show();
if (params.assetsCache) $('#cachingAssets').show();
selectedArchive.getDirEntryByPath(url)
.then(function (dirEntry) {
return selectedArchive.readUtf8File(dirEntry,
function (fileDirEntry, content) {
var fullUrl = fileDirEntry.namespace + "/" + fileDirEntry.url;
if (params.useCache) cssCache.set(fullUrl, content);
if (params.assetsCache) cssCache.set(fullUrl, content);
uiUtil.replaceCSSLinkWithInlineCSS(link, content);
cssFulfilled++;
renderIfCSSFulfilled(fileDirEntry.url);
Expand Down Expand Up @@ -1797,7 +1816,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
* @param {String} title The title of the file to display in the caching message block
*/
function updateCacheStatus(title) {
if (params.useCache && /\.css$|\.js$/i.test(title)) {
if (params.assetsCache && /\.css$|\.js$/i.test(title)) {
var cacheBlock = document.getElementById('cachingAssets');
cacheBlock.style.display = 'block';
title = title.replace(/[^/]+\//g, '').substring(0,18);
Expand Down
119 changes: 118 additions & 1 deletion www/js/lib/settingsStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ define([], function () {
* @type {Array}
*/
var deprecatedKeys = [
'lastContentInjectionMode'
'lastContentInjectionMode',
'useCache'
];

/**
Expand Down Expand Up @@ -91,6 +92,120 @@ 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
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
* @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 cookie entries
if (!object || object === 'cookie') {
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
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...');
Jaifroid marked this conversation as resolved.
Show resolved Hide resolved
}

// 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 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) {
Expand Down Expand Up @@ -179,6 +294,8 @@ define([], function () {
setItem: settingsStore.setItem,
removeItem: settingsStore.removeItem,
hasItem: settingsStore.hasItem,
getCacheNames: getCacheNames,
reset: reset,
getBestAvailableStorageAPI: getBestAvailableStorageAPI
};
});
Loading