diff --git a/www/js/app.js b/www/js/app.js
index b48cdd4b..ff6217cf 100644
--- a/www/js/app.js
+++ b/www/js/app.js
@@ -464,7 +464,7 @@ function printIntercept () {
return uiUtil.systemAlert('Sorry, we could not find a document to print! Please load one first.', 'Warning');
}
if (params.contentInjectionMode === 'serviceworker') {
- // Re-establishe lastPageVisit because it is not always set, for example with dynamic loads, in SW mode
+ // Re-establish lastPageVisit because it is not always set, for example with dynamic loads, in SW mode
params.lastPageVisit = articleDocument.location.href.replace(/^.+\/([^/]+\.[zZ][iI][mM]\w?\w?)\/([CA]\/.*$)/, function (m0, zimName, zimURL) {
return decodeURI(zimURL) + '@kiwixKey@' + decodeURI(zimName);
});
@@ -1689,15 +1689,27 @@ document.querySelectorAll('input[name="contentInjectionMode"][type="radio"]').fo
}
// Do the necessary to enable or disable the Service Worker
setContentInjectionMode(this.value);
- // If we're in a PWA UWP app, warn the user that this does not disable the PWA
- if (this.value === 'jquery' && /^http/i.test(window.location.protocol) && /UWP\|PWA/.test(params.appType) &&
- params.allowInternetAccess === 'true') {
- uiUtil.systemAlert(
- 'Please note that switching content injection mode does not revert to local code.
' +
- 'If you wish to exit the PWA, you will need to turn off "Allow Internet access?" above.
'
- );
+
+ /** DEV: PLEASE NOTE THAT "jQuery mode" HAS NOW CHANGED to "Restricted mode", but we still use "jquery" in code */
+
+ // Actions that must be completed after switch to Restricted mode
+ if (this.value === 'jquery') {
+ // Hide the source verification option
+ document.getElementById('enableSourceVerificationCheck').style.display = 'none';
+ // If we're in a PWA UWP app, warn the user that this does not disable the PWA
+ if (/^http/i.test(window.location.protocol) && /UWP\|PWA/.test(params.appType) &&
+ params.allowInternetAccess === 'true') {
+ uiUtil.systemAlert(
+ 'Please note that switching content injection mode does not revert to local code.
' +
+ 'If you wish to exit the PWA, you will need to turn off "Allow Internet access?" above.
'
+ );
+ }
}
if (this.value === 'serviceworker') {
+ document.getElementById('enableSourceVerificationCheck').style.display = '';
+ if (appstate.selectedArchive.isReady() && !(settingsStore.getItem('trustedZimFiles').includes(appstate.selectedArchive.file.name)) && params.sourceVerification) {
+ verifyLoadedArchive(appstate.selectedArchive);
+ }
if (params.manipulateImages || params.allowHTMLExtraction) {
if (!appstate.wikimediaZimLoaded) {
var message = 'Please note that we are disabling "Image manipulation" and/or "Download or open current article" features, as these options ' +
@@ -1855,6 +1867,12 @@ document.getElementById('disableDragAndDropCheck').addEventListener('change', fu
}
});
});
+// Source verification is only makes sense in SW mode as doing the same in jQuery mode is redundant.
+document.getElementById('enableSourceVerificationCheck').style.display = params.contentInjectionMode === ('serviceworker' || 'serviceworkerlocal') ? 'block' : 'none';
+document.getElementById('enableSourceVerificationCheck').addEventListener('change', function () {
+ params.sourceVerification = this.checked;
+ settingsStore.setItem('sourceVerification', this.checked, Infinity);
+});
document.getElementById('hideActiveContentWarningCheck').addEventListener('change', function () {
params.hideActiveContentWarning = this.checked;
settingsStore.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity);
@@ -2418,12 +2436,12 @@ function switchCSSTheme () {
}, 100);
// If the interval has not succeeded after 3 seconds, give up
if (zimitIframe && document.getElementById('configuration').style.display === 'none') {
- setTimeout(function () {
- articleContainer.style.display = '';
- zimitIframe.style.display = '';
+ setTimeout(function (zimitf, articleC) {
+ articleC.style.display = '';
+ zimitf.style.display = '';
clearInterval(interval);
window.dispatchEvent(new Event('resize')); // Force repaint
- }, 3000);
+ }, 3000, zimitIframe, articleContainer);
}
} else if (document.getElementById('configuration').style.display === 'none') {
// We're dealing with a light style, so we just display it
@@ -4067,6 +4085,38 @@ function setLocalArchiveFromFileList (files, fromArchiveList) {
});
}
+/**
+ * Verifies the given archive and switches contentInjectionMode accourdingly
+ * Code to undertake the verification adapted from kiwix/kiwix-js #1192 kindly authored by @Greeshmanth1909
+ *
+ * @param {Object} archive The archive that needs verification
+ *
+ */
+function verifyLoadedArchive (archive) {
+ return uiUtil.systemAlert('Is this ZIM archive from a trusted source?
' +
+ 'Name: ' + archive.file.name + ' ' +
+ 'Creator: ' + archive.creator + ' ' +
+ 'Publisher: ' + archive.publisher + ' ' +
+ 'Scraper: ' + archive.scraper + ' ' +
+ '
Warning: above data can easily be spoofed!
' +
+ 'If you do not trust the source, you can still read the ZIM file in Restricted mode. Closing this window also opens the file in Restricted mode.
' +
+ 'If you mark the file as trusted, this alert will not show again. (Security checks can be disabled in Expert Settings.)
',
+ 'Security alert!', true, 'Open in Restricted mode', 'Trust source').then(response => {
+ if (response) {
+ params.contentInjectionMode = 'serviceworker';
+ var trustedZimFiles = settingsStore.getItem('trustedZimFiles');
+ var updatedTrustedZimFiles = trustedZimFiles + archive.file.name + '|';
+ settingsStore.setItem('trustedZimFiles', updatedTrustedZimFiles, Infinity);
+ // Change radio buttons accordingly
+ document.getElementById('serviceworkerModeRadio').checked = true;
+ } else {
+ // Switch to Restricted mode
+ params.contentInjectionMode = 'jquery';
+ document.getElementById('jQueryModeRadio').checked = true;
+ }
+ });
+}
+
/**
* Functions to be run immediately after the archive is loaded
*
@@ -4185,27 +4235,57 @@ function archiveReadyCallback (archive) {
}
// This ensures the correct icon is set for the newly loaded archive
cssUIThemeGetOrSet(params.cssUITheme);
- if (params.rescan) {
- document.getElementById('btnConfigure').click();
- setTimeout(function () {
+ var displayArchive = function () {
+ if (params.rescan) {
document.getElementById('btnConfigure').click();
- params.rescan = false;
- }, 100);
- } else {
- if (typeof Windows === 'undefined' && typeof window.showOpenFilePicker !== 'function' && !params.useOPFS && !window.dialog) {
- document.getElementById('instructions').style.display = 'none';
+ setTimeout(function () {
+ document.getElementById('btnConfigure').click();
+ params.rescan = false;
+ }, 100);
} else {
- document.getElementById('openLocalFiles').style.display = 'none';
- document.getElementById('rescanStorage').style.display = 'block';
- }
- document.getElementById('usage').style.display = 'none';
- if (params.rememberLastPage && ~params.lastPageVisit.indexOf(params.storedFile.replace(/\.zim(\w\w)?$/, ''))) {
- var lastPage = params.lastPageVisit.replace(/@kiwixKey@.+/, '');
- goToArticle(lastPage);
- } else {
- document.getElementById('btnHome').click();
+ if (typeof Windows === 'undefined' && typeof window.showOpenFilePicker !== 'function' && !params.useOPFS && !window.dialog) {
+ document.getElementById('instructions').style.display = 'none';
+ } else {
+ document.getElementById('openLocalFiles').style.display = 'none';
+ document.getElementById('rescanStorage').style.display = 'block';
+ }
+ document.getElementById('usage').style.display = 'none';
+ if (params.rememberLastPage && ~params.lastPageVisit.indexOf(params.storedFile.replace(/\.zim(\w\w)?$/, ''))) {
+ var lastPage = params.lastPageVisit.replace(/@kiwixKey@.+/, '');
+ goToArticle(lastPage);
+ } else {
+ document.getElementById('btnHome').click();
+ }
}
}
+ // Set contentInjectionMode to serviceWorker when opening a new archive in case the user switched to Restricted mode/jQuery Mode when opening the previous archive
+ if (params.contentInjectionMode === 'jquery') {
+ params.contentInjectionMode = settingsStore.getItem('contentInjectionMode');
+ // Change the radio buttons accordingly
+ switch (settingsStore.getItem('contentInjectionMode')) {
+ case 'serviceworker':
+ document.getElementById('serviceworkerModeRadio').checked = true;
+ // In case we atuo-switched off assetsCache due to switch to Restricted mode, we need to reset
+ params.assetsCache = settingsStore.getItem('asetsCache') !== 'false';
+ break;
+ case 'serviceworkerlocal':
+ document.getElementById('serviceworkerLocalModeRadio').checked = true;
+ break;
+ }
+ }
+ if (settingsStore.getItem('trustedZimFiles') === null) {
+ settingsStore.setItem('trustedZimFiles', '', Infinity);
+ }
+ if (params.sourceVerification && (params.contentInjectionMode === 'serviceworker' || params.contentInjectionMode === 'serviceworkerlocal')) {
+ // Check if source of the zim file can be trusted.
+ if (!(settingsStore.getItem('trustedZimFiles').includes(archive.file.name))) {
+ verifyLoadedArchive(archive).then(function () {
+ displayArchive();
+ });
+ return;
+ }
+ }
+ displayArchive();
}
function loadPackagedArchive () {
@@ -5119,6 +5199,21 @@ var articleLoadedSW = function (dirEntry, container) {
uiUtil.showSlidingUIElements();
var doc = articleWindow ? articleWindow.document : null;
articleDocument = doc;
+ var mimeType = dirEntry.getMimetype();
+ // If we've successfully loaded an HTML document...
+ if (doc && /\bx?html/i.test(mimeType)) {
+ if (params.rememberLastPage) {
+ params.lastPageVisit = dirEntry.namespace + '/' + dirEntry.url + '@kiwixKey@' + appstate.selectedArchive.file.name;
+ } else {
+ params.lastPageVisit = '';
+ }
+ // Turn off failsafe for SW mode
+ settingsStore.setItem('lastPageLoad', 'OK', Infinity);
+ settingsStore.setItem('lastPageVisit', params.lastPageVisit, Infinity);
+ // Set or clear the ZIM store of last page
+ var lastPage = params.rememberLastPage ? dirEntry.namespace + '/' + dirEntry.url : '';
+ settingsStore.setItem(appstate.selectedArchive.file.name, lastPage, Infinity);
+ }
var docBody = doc ? doc.body : null;
if (docBody) {
// Trap clicks in the iframe to enable us to work around the sandbox when opening external links and PDFs
@@ -5133,7 +5228,6 @@ var articleLoadedSW = function (dirEntry, container) {
listenForSearchKeys();
}
// Note that switchCSSTheme() requires access to params.lastPageVisit
- params.lastPageVisit = dirEntry.namespace + '/' + dirEntry.url + '@kiwixKey@' + appstate.selectedArchive.file.name;
if (!appstate.isReplayWorkerAvailable) switchCSSTheme(); // Gets called in articleLoader for replay_iframe
if (appstate.selectedArchive.zimType === 'open') {
// Set relative font size + Stackexchange-family multiplier
@@ -5183,12 +5277,6 @@ var articleLoadedSW = function (dirEntry, container) {
resizeIFrame();
}, 200);
}
- // Turn off failsafe for SW mode
- settingsStore.setItem('lastPageLoad', 'OK', Infinity);
- if (!appstate.isReplayWorkerAvailable) {
- // Because this is loading within docBody, it should only get set for HTML documents
- if (params.rememberLastPage) settingsStore.setItem('lastPageVisit', params.lastPageVisit, Infinity);
- }
uiUtil.clearSpinner();
// If we reloaded the page to print the desktop style, we need to return to the printIntercept dialogue
if (params.printIntercept) printIntercept();
@@ -5205,7 +5293,10 @@ var articleLoadedSW = function (dirEntry, container) {
if (dirEntry) uiUtil.makeReturnLink(dirEntry.getTitleOrUrl());
params.isLandingPage = false;
} else {
- loaded = false;
+ // If we havent' loaded a text-type document, we probably haven't finished loading
+ if (!/^text\//i.test(mimeType)) {
+ loaded = false;
+ }
}
// Show spinner when the article unloads
@@ -6253,7 +6344,8 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
if (downloadAlert) downloadAlert.style.display = 'none';
// Code below will run after we have written the new article to the articleContainer
- var articleLoaded = params.contentInjectionMode === 'serviceworker' ? function () {} : function () {
+ var articleLoaded = function () {
+ if (params.contentInjectionMode === 'serviceworker') return;
// Set a global error handler for articleWindow
articleWindow.onerror = function (msg, url, line, col, error) {
console.error('Error caught in ZIM contents [' + url + ':' + line + ']:\n' + msg, error);
@@ -6379,17 +6471,9 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
// Make sure the article area is displayed
setTab();
checkToolbar();
- var showArticle = function () {
- articleDocument.bgcolor = '';
- docBody.style.display = 'block';
- };
- if ('MSBlobBuilder' in window) {
- // For legacy MS browsers, including UWP, delay causes blank screen on slow systems
- showArticle();
- } else {
- // For Chromium browsers a small delay greatly improves composition
- setTimeout(showArticle, 80);
- }
+ // Show the article
+ articleDocument.bgcolor = '';
+ docBody.style.display = 'block';
// Jump to any anchor parameter
if (anchorParameter) {
var target = articleWindow.document.getElementById(anchorParameter);
diff --git a/www/js/init.js b/www/js/init.js
index b68a664c..fd565b65 100644
--- a/www/js/init.js
+++ b/www/js/init.js
@@ -110,6 +110,7 @@ params['rightClickType'] = getSetting('rightClickType'); // 'single|double|false
params['navButtonsPos'] = getSetting('navButtonsPos') || 'bottom'; // 'top|bottom' A setting that determines where the back-forward nav buttons appear
params['useOPFS'] = getSetting('useOPFS') === true; // A setting that determines whether to use OPFS (experimental)
params['useLegacyZimitSupport'] = getSetting('useLegacyZimitSupport') === true; // A setting that determines whether to force the use of legacy Zimit support
+params['sourceVerification'] = params.contentInjectionMode === 'serviceworker' ? (getSetting('sourceVerification') === null ? true : getSetting('sourceVerification')) : false; // Sets a boolean indicating weather a user trusts the source of zim files
// Do not touch these values unless you know what they do! Some are global variables, some are set programmatically
params['cacheAPI'] = 'kiwixjs-assetsCache'; // Set the global Cache API database or cache name here, and synchronize with Service Worker
@@ -251,6 +252,7 @@ document.getElementById('rememberLastPageCheck').checked = params.rememberLastPa
document.getElementById('displayFileSelectorsCheck').checked = params.showFileSelectors;
document.getElementById('hideActiveContentWarningCheck').checked = params.hideActiveContentWarning;
document.getElementById('useLibzimReaderCheck').checked = params.useLibzim;
+document.getElementById('enableSourceVerificationCheck').checked = getSetting('sourceVerification') === null ? true : getSetting('sourceVerification');
document.getElementById('useLegacyZimitSupportCheck').checked = params.useLegacyZimitSupport;
document.getElementById('alphaCharTxt').value = params.alphaChar;
document.getElementById('omegaCharTxt').value = params.omegaChar;
diff --git a/www/js/lib/zimArchive.js b/www/js/lib/zimArchive.js
index c6a3798c..9b3795ea 100644
--- a/www/js/lib/zimArchive.js
+++ b/www/js/lib/zimArchive.js
@@ -80,7 +80,8 @@ function ZIMArchive (storage, path, callbackReady, callbackError) {
// Further metadata are added in the background below, and can be accessed later
return Promise.all([
that.addMetadataToZIMFile('Creator'),
- that.addMetadataToZIMFile('Language')
+ that.addMetadataToZIMFile('Language'),
+ that.addMetadataToZIMFile('Publisher')
]).then(function () {
console.debug('ZIMArchive ready, metadata will be added in the background');
// Add non-time-critical metadata to archive in background so as not to delay opening of the archive
@@ -92,7 +93,6 @@ function ZIMArchive (storage, path, callbackReady, callbackError) {
that.addMetadataToZIMFile('Date'),
that.addMetadataToZIMFile('Description'),
that.addMetadataToZIMFile('Name'),
- that.addMetadataToZIMFile('Publisher'),
that.addMetadataToZIMFile('Source'),
that.addMetadataToZIMFile('Title')
]).then(function () {