Support multiple instances with different ZIMs #462 (#484)

Fixes #218, #219, #462.
This commit is contained in:
Jaifroid 2023-11-08 11:29:47 +00:00 committed by GitHub
parent 935cc36612
commit 17c9312a00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 157 additions and 151 deletions

View File

@ -258,6 +258,7 @@ self.addEventListener('install', function (event) {
// Allow sw to control current page
self.addEventListener('activate', function (event) {
console.debug('[SW] Activate Event processing');
// "Claiming" the ServiceWorker is necessary to make it work right away,
// without the need to reload the page.
// See https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim
@ -271,14 +272,16 @@ self.addEventListener('activate', function (event) {
if (key !== APP_CACHE && key !== ASSETS_CACHE) {
console.debug('[SW] App updated to version ' + appVersion + ': deleting old cache');
return caches.delete(key);
} else {
return Promise.resolve();
}
}));
})
);
});
let outgoingMessagePort = null;
let fetchCaptureEnabled = false;
// For PWA functionality, this should be true unless explicitly disabled, and in fact currently it is never disabled
let fetchCaptureEnabled = true;
/**
* Intercept selected Fetch requests from the browser window
@ -305,7 +308,7 @@ self.addEventListener('fetch', function (event) {
// especially .js assets, where it may be significant). Anchor targets are irreleveant in this context.
// @TODO DEV: This isn't true for Zimit ZIM types! So we will have to send the zimType from app.js
if (cache === APP_CACHE) rqUrl = strippedUrl;
event.respondWith(
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
@ -367,12 +370,12 @@ self.addEventListener('fetch', function (event) {
self.addEventListener('message', function (event) {
if (event.data.action) {
if (event.data.action === 'init') {
// On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener
outgoingMessagePort = event.ports[0];
// On 'init' message, we enable the fetchEventListener
fetchCaptureEnabled = true;
} else if (event.data.action === 'disable') {
// On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener
// outgoingMessagePort = null;
// On 'disable' message, we disable the fetchEventListener
// Note that this code doesn't currently run because the app currently never sends a 'disable' message
// This is because the app may be running as a PWA, and still needs to be able to fetch assets even in jQuery mode
fetchCaptureEnabled = false;
}
var oldValue;
@ -419,10 +422,9 @@ function fetchUrlFromZIM (urlObject, range) {
var anchorTarget = urlObject.hash.replace(/^#/, '');
var uriComponent = urlObject.search.replace(/\?kiwix-display/, '');
var titleWithNameSpace = nameSpace + '/' + title;
var zimName = prefix.replace(/\/$/, '');
// Let's instantiate a new messageChannel, to allow app.js to give us the content
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = function (msgPortEvent) {
var messageListener = function (msgPortEvent) {
if (msgPortEvent.data.action === 'giveContent') {
// Content received from app.js
var contentLength = msgPortEvent.data.content ? (msgPortEvent.data.content.byteLength || msgPortEvent.data.content.length) : null;
@ -481,12 +483,22 @@ function fetchUrlFromZIM (urlObject, range) {
reject(msgPortEvent.data, titleWithNameSpace);
}
};
outgoingMessagePort.postMessage({
action: 'askForContent',
title: titleWithNameSpace,
search: uriComponent,
anchorTarget: anchorTarget
}, [messageChannel.port2]);
// Get all the clients currently being controlled and send them a message
self.clients.matchAll().then(function (clientList) {
clientList.forEach(function (client) {
if (client.frameType !== 'top-level') return;
// Let's instantiate a new messageChannel, to allow app.js to give us the content
var messageChannel = new MessageChannel();
messageChannel.port1.onmessage = messageListener;
client.postMessage({
action: 'askForContent',
title: titleWithNameSpace,
search: uriComponent,
anchorTarget: anchorTarget,
zimFileName: zimName
}, [messageChannel.port2]);
});
});
});
}

View File

@ -63,6 +63,12 @@ articleContainer.kiwixType = 'iframe';
var articleWindow = articleContainer.contentWindow;
var articleDocument;
// The following variables are used to store the current article and its state
var messageChannelWaiting = false;
var transformedHTML = '';
var transDirEntry = null;
/**
* @type ZIMArchive
*/
@ -121,30 +127,8 @@ uiUtil.setupConfigurationToggles();
* @param {Boolean} reload Allows reload of the app on resize
*/
function resizeIFrame (reload) {
var scrollbox = document.getElementById('scrollbox');
var header = document.getElementById('top');
var iframe = document.getElementById('articleContent');
var navbarHeight = document.getElementById('navbar').getBoundingClientRect().height;
// Reset any hidden headers and footers and iframe shift
header.style.zIndex = 1;
header.style.transform = 'translateY(0)';
document.getElementById('footer').style.transform = 'translateY(0)';
iframe.style.transform = 'translateY(-1px)';
iframe.style.height = window.innerHeight + 'px';
// DEV: if we set the iframe with clientHeight, then it takes into account any zoom
// iframe.style.height = document.documentElement.clientHeight - 5 + 'px';
// This is needed to cause a reflow in Zimit ZIMs
setTimeout(function () {
iframe.style.height = document.documentElement.clientHeight + 'px';
}, 5);
// Re-enable top-level scrolling
scrollbox.style.height = window.innerHeight - navbarHeight + 'px';
if (iframe.style.display !== 'none' && document.getElementById('prefix') !== document.activeElement) {
scrollbox.style.height = 0;
}
console.debug('Resizing iframe...');
uiUtil.showSlidingUIElements();
var ToCList = document.getElementById('ToCList');
if (typeof ToCList !== 'undefined') {
ToCList.style.maxHeight = ~~(window.innerHeight * 0.75) + 'px';
@ -2669,27 +2653,29 @@ function refreshCacheStatus () {
}
}
var keepAliveServiceWorkerHandle = null;
var initServiceWorkerHandle = null;
var serviceWorkerRegistration = null;
/**
* Send an 'init' message to the ServiceWorker with a new MessageChannel
* to initialize it, or to keep it alive.
* This MessageChannel allows a 2-way communication between the ServiceWorker
* and the application
* Sends an 'init' message to the ServiceWorker and inititalizes the onmessage event
* When the event is received, it will provide a MessageChannel port to respond to the ServiceWorker
*/
function initOrKeepAliveServiceWorker () {
var delay = DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER;
function initServiceWorkerMessaging () {
// If no ZIM archive is loaded, return (it will be called when one is loaded)
if (!appstate.selectedArchive) return;
if (params.contentInjectionMode === 'serviceworker') {
// Create a new messageChannel
var tmpMessageChannel = new MessageChannel();
tmpMessageChannel.port1.onmessage = handleMessageChannelMessage;
// Send the init message to the ServiceWorker, with this MessageChannel as a parameter
// Create a message listener
navigator.serviceWorker.onmessage = function (event) {
if (event.data.action === 'askForContent') {
handleMessageChannelMessage(event)
}
};
// Send the init message to the ServiceWorker
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
action: 'init'
}, [tmpMessageChannel.port2]);
} else if (keepAliveServiceWorkerHandle) {
});
} else if (initServiceWorkerHandle) {
console.error('The Service Worker is active but is not controlling the current page! We have to reload.');
// Turn off failsafe, as this is a controlled reboot
settingsStore.setItem('lastPageLoad', 'rebooting', Infinity);
@ -2697,13 +2683,9 @@ function initOrKeepAliveServiceWorker () {
} else {
// If this is the first time we are initiating the SW, allow Promises to complete by delaying potential reload till next tick
console.debug('The Service Worker needs more time to load...');
delay = 0;
initServiceWorkerHandle = setTimeout(initServiceWorkerMessaging, 0);
}
// Schedule to do it again regularly to keep the 2-way communication alive.
// See https://github.com/kiwix/kiwix-js/issues/145 to understand why
clearTimeout(keepAliveServiceWorkerHandle);
keepAliveServiceWorkerHandle = setTimeout(initOrKeepAliveServiceWorker, delay, false);
}
}
}
/**
@ -2729,10 +2711,6 @@ function setContentInjectionMode (value) {
navigator.serviceWorker.controller.postMessage({
action: { assetsCache: 'disable' }
}, [channel.port2]);
var channel2 = new MessageChannel();
navigator.serviceWorker.controller.postMessage({
action: 'disable'
}, [channel2.port2]);
}
caches.delete(cache.ASSETS_CACHE);
}
@ -2766,7 +2744,7 @@ function setContentInjectionMode (value) {
// Remove any jQuery hooks from a previous jQuery session
$('#articleContent').contents().remove();
// Create the MessageChannel and send 'init'
initOrKeepAliveServiceWorker();
// initOrKeepAliveServiceWorker();
refreshAPIStatus();
} else {
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
@ -2781,7 +2759,7 @@ function setContentInjectionMode (value) {
// Remove any jQuery hooks from a previous jQuery session
$('#articleContent').contents().remove();
// Create the MessageChannel and send the 'init' message to the ServiceWorker
initOrKeepAliveServiceWorker();
// initOrKeepAliveServiceWorker();
// We need to refresh cache status here on first activation because SW was inaccessible till now
// We also initialize the ASSETS_CACHE constant in SW here
refreshCacheStatus();
@ -2794,7 +2772,7 @@ function setContentInjectionMode (value) {
// We need to re-create the MessageChannel
// and send the 'init' message to the ServiceWorker
// in case it has been stopped and lost its context
initOrKeepAliveServiceWorker();
// initOrKeepAliveServiceWorker();
}
refreshAPIStatus();
}).catch(function (err) {
@ -2823,7 +2801,7 @@ function setContentInjectionMode (value) {
} else {
// We need to set this variable earlier else the Service Worker does not get reactivated
params.contentInjectionMode = value;
initOrKeepAliveServiceWorker();
// initOrKeepAliveServiceWorker();
}
}
$('input:radio[name=contentInjectionMode]').prop('checked', false);
@ -3389,11 +3367,7 @@ function setLocalArchiveFromArchiveList (archive) {
// }
}
}
// Reset the cssDirEntryCache and cssBlobCache. Must be done when archive changes.
if (cssBlobCache) {
cssBlobCache = new Map();
}
appstate.selectedArchive = zimArchiveLoader.loadArchiveFromDeviceStorage(selectedStorage, archive, archiveReadyCallback, function (message, label) {
zimArchiveLoader.loadArchiveFromDeviceStorage(selectedStorage, archive, archiveReadyCallback, function (message, label) {
// callbackError which is called in case of an error
uiUtil.systemAlert(message, label);
});
@ -3850,10 +3824,8 @@ function setLocalArchiveFromFileList (files, fromArchiveList) {
if (files.length > 1 && firstSplitFileIndex === null) {
files = [files[storedFileIndex]];
}
// Reset the cssDirEntryCache and cssBlobCache. Must be done when archive changes.
if (cssBlobCache) cssBlobCache = new Map();
// TODO: Turn this into a Promise
appstate.selectedArchive = zimArchiveLoader.loadArchiveFromFiles(files, archiveReadyCallback, function (message, label) {
zimArchiveLoader.loadArchiveFromFiles(files, archiveReadyCallback, function (message, label) {
// callbackError which is called in case of an error
uiUtil.systemAlert(message, label);
});
@ -3865,7 +3837,14 @@ function setLocalArchiveFromFileList (files, fromArchiveList) {
* @param {ZIMArchive} archive The ZIM archive
*/
function archiveReadyCallback (archive) {
appstate.selectedArchive = archive;
// A blob cache significantly speeds up the loading of CSS files
appstate.selectedArchive.cssBlobCache = new Map();
uiUtil.clearSpinner();
// Initialize the Service Worker
if (params.contentInjectionMode === 'serviceworker') {
initServiceWorkerMessaging();
}
// Ensure that the new ZIM output is initially sent to the iframe (e.g. if the last article was loaded in a window)
// (this only affects jQuery mode)
appstate.target = 'iframe';
@ -4651,6 +4630,7 @@ function readArticle (dirEntry) {
}
};
if (params.rememberLastPage && params.lastPageVisit) lastPage = params.lastPageVisit.replace(/@kiwixKey@.+/, '');
// If we have the HTML of the last loaded page, use it to save lookups
if (params.rememberLastPage && dirEntry.namespace + '/' + dirEntry.url === lastPage) {
if (!params.lastPageHTML) {
// DEV: Timout is needed here to allow time for cache capability to be tested before calling it
@ -4666,6 +4646,12 @@ function readArticle (dirEntry) {
htmlContent = params.lastPageHTML;
goToRetrievedContent(htmlContent);
}
// } else if (params.zimType === 'zimit' && params.contentInjectionMode === 'serviceworker' && !messageChannelWaiting) {
// // DEF: If the messageChannel isn't waiting for transformed HTML, we could instruct the SW to load this article
// // It uses more CPU, as it starts the lookups all over again, but it is arguably a "purer" method especially for Zimit
// var newLocation = '../' + appstate.selectedArchive.file.name + '/' + dirEntry.namespace + '/' + dirEntry.url + '?isKiwixHref';
// loaded = false;
// articleWindow.location.href = newLocation;
} else {
goToRetrievedContent(htmlContent);
}
@ -4771,10 +4757,14 @@ var articleLoadedSW = function (dirEntry) {
// The content is ready : we can hide the spinner
setTab();
setTimeout(function () {
console.debug('articleLoadedSW RESIZING IFRAME');
articleDocument.bgcolor = '';
if (appstate.target === 'iframe') articleContainer.style.display = 'block';
docBody.style.display = 'block';
}, 30);
// Some contents need this to be able to display correctly (e.g. masonry landing pages)
iframe.style.height = 'auto';
resizeIFrame();
}, 200);
// Turn off failsafe for SW mode
settingsStore.setItem('lastPageLoad', 'OK', Infinity);
// Because this is loading within docBody, it should only get set for HTML documents
@ -4827,13 +4817,27 @@ function handleMessageChannelMessage (event) {
} else {
// We received a message from the ServiceWorker
if (event.data.action === 'askForContent') {
// Check that the zimFileId in the messageChannel event data is the same as the one in the currently open archive
// Because the SW broadcasts its request to all open tabs or windows, we need to check that the request is for this instance
if (event.data.zimFileName !== appstate.selectedArchive.file.name) {
console.warn('SW request does not match this instansce', '[zimFileName:' + event.data.zimFileName + ' !== ' + appstate.selectedArchive.file.name + ']');
if (params.zimType === 'zimit' && /\/\/youtubei.*player/.test(event.data.title)) {
// DEV: This is a hack to allow YouTube videos to play in Zimit archives:
// Because links are embedded in a nested iframe, the SW cannot identify the top-level window from which to request the ZIM content
// Until we find a way to tell where it is coming from, we allow the request through and try to load the content
console.warn('>>> Allowing passthrough to process YouTube video <<<');
} else {
return;
}
}
loaded = false;
// Zimit archives store URLs encoded, and also need the URI component (search parameter) if any
var title = params.zimType === 'zimit' ? encodeURI(event.data.title) + event.data.search : event.data.title;
// If it's an asset, we have to mark the dirEntry so that we don't load it if it has an html MIME type
var titleIsAsset = /\.(png|gif|jpe?g|svg|css|js|mpe?g|webp|webm|woff2?|eot|mp[43])(\?|$)/i.test(title);
// For Zimit archives, articles will have a special parameter added to the URL to help distinguish an article from an asset
if (params.zimType === 'zimit') {
titleIsAsset = !/\??isKiwixHref/.test(title);
titleIsAsset = titleIsAsset || !/\??isKiwixHref/.test(title);
}
title = title.replace(/\??isKiwixHref/, ''); // Only applies to Zimit archives (added in transformZimit.js)
if (appstate.selectedArchive && appstate.selectedArchive.landingPageUrl === title) params.isLandingPage = true;
@ -4841,14 +4845,14 @@ function handleMessageChannelMessage (event) {
if (!anchorParameter && event.data.anchorTarget) anchorParameter = event.data.anchorTarget;
// Intercept landing page if already transformed (because this might have a fake dirEntry)
// Note that due to inconsistencies in Zimit archives, we need to test the encoded and the decoded version of the title
if (params.transformedHTML && params.transDirEntry && (title === params.transDirEntry.namespace + '/' + params.transDirEntry.url ||
decodeURIComponent(title) === params.transDirEntry.namespace + '/' + params.transDirEntry.url)) {
if (transformedHTML && transDirEntry && (title === transDirEntry.namespace + '/' + transDirEntry.url ||
decodeURIComponent(title) === transDirEntry.namespace + '/' + transDirEntry.url)) {
var message = {
action: 'giveContent',
title: title,
mimetype: 'text/html'
};
postTransformedHTML(message, messagePort, params.transDirEntry);
postTransformedHTML(message, messagePort, transDirEntry);
return;
}
var readFile = function (dirEntry) {
@ -4897,11 +4901,11 @@ function handleMessageChannelMessage (event) {
mimetype: mimetype,
imageDisplay: imageDisplayMode
};
if (!params.transformedHTML) {
if (!transformedHTML) {
// It's an unstransformed html file, so we need to do some content transforms and wait for the HTML to be available
if (!~params.lastPageVisit.indexOf(dirEntry.url)) params.lastPageVisit = '';
// Tell the read routine that the request comes from a messageChannel
appstate.messageChannelWaiting = true;
messageChannelWaiting = true;
readArticle(dirEntry);
setTimeout(postTransformedHTML, 300, message, messagePort, dirEntry);
} else {
@ -4913,7 +4917,6 @@ function handleMessageChannelMessage (event) {
}
var cacheKey = appstate.selectedArchive.file.name + '/' + title;
cache.getItemFromCacheOrZIM(appstate.selectedArchive, cacheKey, dirEntry).then(function (content) {
console.debug('SW read binary file for: ' + dirEntry.namespace + '/' + dirEntry.url);
if (params.zimType === 'zimit' && loadingArticle) {
// We need to work around the redirection script in all Zimit HTML files in case we're loading the HTML in a new window
// The script doesn't fire in the iframe, but it does in the new window, so we need to edit it
@ -4983,10 +4986,10 @@ function handleMessageChannelMessage (event) {
}
function postTransformedHTML (thisMessage, thisMessagePort, thisDirEntry) {
if (params.transformedHTML && /<html[^>]*>/i.test(params.transformedHTML)) {
if (transformedHTML && /<html[^>]*>/i.test(transformedHTML)) {
// Because UWP app window can only be controlled from the Service Worker, we have to allow all images
// to be called from any external windows. NB messageChannelWaiting is only true when user requested article from a UWP window
if (/UWP/.test(params.appType) && (appstate.target === 'window' || appstate.messageChannelWaiting) &&
if (/UWP/.test(params.appType) && (appstate.target === 'window' || messageChannelWaiting) &&
params.imageDisplay) { thisMessage.imageDisplay = 'all'; }
// We need to do the same for Gutenberg and PHET ZIMs
if (params.imageDisplay && (/gutenberg|phet/i.test(appstate.selectedArchive.file.name)
@ -4995,13 +4998,13 @@ function postTransformedHTML (thisMessage, thisMessagePort, thisDirEntry) {
thisMessage.imageDisplay = 'all';
}
// Let's send the content to the ServiceWorker
thisMessage.content = params.transformedHTML;
params.transformedHTML = '';
params.transDirEntry = null;
thisMessage.content = transformedHTML;
transformedHTML = '';
transDirEntry = null;
loaded = false;
// If loading the iframe, we can hide the frame for UWP apps (for others, the doc should already be hidden)
// NB Test for messageChannelWaiting filters out requests coming from a UWP window
if (articleContainer.kiwixType === 'iframe' && !appstate.messageChannelWaiting) {
if (articleContainer.kiwixType === 'iframe' && !messageChannelWaiting) {
if (/UWP/.test(params.appType)) {
articleContainer.style.display = 'none';
setTimeout(function () {
@ -5025,12 +5028,12 @@ function postTransformedHTML (thisMessage, thisMessagePort, thisDirEntry) {
}
}
thisMessagePort.postMessage(thisMessage);
appstate.messageChannelWaiting = false;
messageChannelWaiting = false;
// Failsafe to turn off spinner
setTimeout(function () {
uiUtil.clearSpinner();
}, 5000);
} else if (appstate.messageChannelWaiting) {
} else if (messageChannelWaiting) {
setTimeout(postTransformedHTML, 500, thisMessage, thisMessagePort, thisDirEntry);
}
}
@ -5079,12 +5082,6 @@ params.containsMathTexRaw = false;
params.containsMathTex = false;
params.containsMathSVG = false;
// Stores a url to direntry mapping and is refered to/updated anytime there is a css lookup
// When archive changes these caches should be reset.
// Currently happens only in setLocalArchiveFromFileList and setLocalArchiveFromArchiveList.
// var cssDirEntryCache = new Map(); //This one is never hit!
var cssBlobCache = new Map();
/**
* Display the the given HTML article in the web page,
* and convert links to javascript calls
@ -5514,9 +5511,9 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
}
function resolveCSS (title, index) {
if (cssBlobCache && cssBlobCache.has(title)) {
if (appstate.selectedArchive.cssBlobCache.has(title)) {
console.log('*** cssBlobCache hit ***');
blobArray.push([title, cssBlobCache.get(title)]);
blobArray.push([title, appstate.selectedArchive.cssBlobCache.get(title)]);
injectCSS();
} else {
var cacheKey = appstate.selectedArchive.file.name + '/' + title;
@ -5532,17 +5529,13 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
}
var newURL = cssBlob ? [title, URL.createObjectURL(cssBlob)] : [title, ''];
blobArray.push(newURL);
if (cssBlobCache) {
cssBlobCache.set(newURL[0], newURL[1]);
}
appstate.selectedArchive.cssBlobCache.set(newURL[0], newURL[1]);
injectCSS(); // DO NOT move this: it must run within .then function to pass correct values
}).catch(function (err) {
console.error(err);
var newURL = [title, ''];
blobArray.push(newURL);
if (cssBlobCache) {
cssBlobCache.set(newURL[0], newURL[1]);
}
appstate.selectedArchive.cssBlobCache.set(newURL[0], newURL[1]);
injectCSS();
});
}
@ -5779,7 +5772,7 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
// Note that UWP apps cannot communicate to a newly opened window except via postmessage, but Service Worker can still
// control the Window. Additionally, Edge Legacy cannot build the DOM for a completely hidden document, hence we catch
// these browser types with 'MSBlobBuilder' (and also IE11).
if (!(/UWP/.test(params.appType) && (appstate.target === 'window' || appstate.messageChannelWaiting))) {
if (!(/UWP/.test(params.appType) && (appstate.target === 'window' || messageChannelWaiting))) {
htmlArticle = htmlArticle.replace(/(<html\b[^>]*)>/i, '$1 bgcolor="' +
(cssUIThemeGetOrSet(params.cssTheme, true) !== 'light' ? 'grey' : 'whitesmoke') + '">');
// NB Don't hide the document body if we don't have any window management, because native loading of documents in a new tab is slow, and we can't
@ -5820,21 +5813,20 @@ function displayArticleContentInContainer (dirEntry, htmlArticle) {
if (params.zimType === 'zimit') htmlArticle = htmlArticle.replace(/!(window._WBWombat)/, '$1');
// Add doctype if missing so that scripts run in standards mode
// (quirks mode prevents katex from running, and is incompatible with jQuery)
params.transformedHTML = !/^\s*(?:<!DOCTYPE|<\?xml)\s+/i.test(htmlArticle) ? '<!DOCTYPE html>\n' + htmlArticle : htmlArticle;
params.transDirEntry = dirEntry;
transformedHTML = !/^\s*(?:<!DOCTYPE|<\?xml)\s+/i.test(htmlArticle) ? '<!DOCTYPE html>\n' + htmlArticle : htmlArticle;
transDirEntry = dirEntry;
// We will need the encoded URL on article load so that we can set the iframe's src correctly,
// but we must not encode the '/' character or else relative links may fail [kiwix-js #498]
var encodedUrl = params.zimType === 'zimit' ? dirEntry.url : encodeURI(dirEntry.url);
// .replace(/[^/]+/g, function (matchedSubstring) {
// return encodeURIComponent(matchedSubstring);
// });
// If the request was not initiated by an existing controlled window, we instantiate the request here
if (!appstate.messageChannelWaiting) {
if (!messageChannelWaiting) {
// We put the ZIM filename as a prefix in the URL, so that browser caches are separate for each ZIM file
var newLocation = '../' + appstate.selectedArchive.file.name + '/' + dirEntry.namespace + '/' + encodedUrl;
var newLocation = '../' + appstate.selectedArchive.file.name + '/' + dirEntry.namespace + '/' + encodedUrl + (params.zimType === 'zimit' ? '?isKiwixHref' : '');
if (navigator.serviceWorker.controller) {
loaded = false;
articleWindow.location.href = newLocation;
} else {
console.error('No Service Worker controller found while waiting for transformed HTML to be loaded!');
}
}
return;

View File

@ -125,12 +125,13 @@ var regexpInlineScriptsNotMaths = /<(script\b(?![^>]+type\s*=\s*["'](?:math\/|te
* @param {String} mimetype The reported mimetype of the data (this is also in the dirEntry)
* @returns {String} The transformed data string
*/
function transformReplayUrls (dirEntry, data, mimetype, callback) {
function transformReplayUrls (dirEntry, data, mimetype) {
/**
* Transform URL links in HTML files
* Note that some Zimit ZIMs have mimeteypes like 'text/html;raw=true', so we can't simply match 'text/html'
* Other ZIMs have a mimetype like 'html' (with no 'text/'), so we have to match as generically as possible
*/
// console.debug('**** Transforming URLs in ' + dirEntry.namespace + '/' + dirEntry.url + ' ****');
var indexRoot = window.location.pathname.replace(/[^/]+$/, '') + encodeURI(appstate.selectedArchive.file.name);
if (/\bx?html\b/i.test(mimetype)) {
var zimitPrefix = data.match(regexpGetZimitPrefix);

View File

@ -307,15 +307,17 @@ ZIMArchive.prototype.findDirEntriesWithPrefix = function (search, callback, noIn
search.rgxPrefix = null;
var prefix = search.prefix;
// Launch a full-text search if possible
if (LZ && !search.searchUrlIndex) that.findDirEntriesFromFullTextSearch(search, dirEntries).then(function (fullTextDirEntries) {
// If user initiated a new search, cancel this one
// In particular, do not set the search status back to 'complete'
// as that would cause outdated results to unexpectedly pop up
if (search.status === 'cancelled') return callback([], search);
dirEntries = fullTextDirEntries;
search.status = 'complete';
callback(dirEntries, search);
});
if (LZ && !search.searchUrlIndex) {
that.findDirEntriesFromFullTextSearch(search, dirEntries).then(function (fullTextDirEntries) {
// If user initiated a new search, cancel this one
// In particular, do not set the search status back to 'complete'
// as that would cause outdated results to unexpectedly pop up
if (search.status === 'cancelled') return callback([], search);
dirEntries = fullTextDirEntries;
search.status = 'complete';
callback(dirEntries, search);
});
}
if (isPrefixRegExp) {
// User has initiated a regular expression search - note the only regexp special character allowed in the alphanumeric part is \s
prefix = isPrefixRegExp[1].replace(/\\s/g, ' ');
@ -331,7 +333,7 @@ ZIMArchive.prototype.findDirEntriesWithPrefix = function (search, callback, noIn
callback([], search);
return;
}
}
}
var prefixNameSpaces = '';
if (search.searchUrlIndex) {
var rgxSplitPrefix = /^[-ABCHIJMUVWX]\//;
@ -386,8 +388,7 @@ ZIMArchive.prototype.findDirEntriesWithPrefix = function (search, callback, noIn
search.status = 'complete';
callback(dirEntries, search);
});
}
else search.status = 'complete';
} else search.status = 'complete';
return callback(dirEntries, search);
}
// Dynamically populate list of articles
@ -465,7 +466,7 @@ ZIMArchive.prototype.getContentNamespace = function () {
* @param {Integer} startIndex The index number with which to commence the search, or null
*/
ZIMArchive.prototype.findDirEntriesWithPrefixCaseSensitive = function (prefix, search, callback, startIndex) {
// Save the value of startIndex because value of null has a special meaning in combination with prefix:
// Save the value of startIndex because value of null has a special meaning in combination with prefix:
// produces a list of matches starting with first match and then next x dirEntries thereafter
var saveStartIndex = startIndex;
startIndex = startIndex || 0;
@ -478,8 +479,8 @@ ZIMArchive.prototype.findDirEntriesWithPrefixCaseSensitive = function (prefix, s
articleCount = this.file.entryCount;
searchFunction = appstate.selectedArchive.file.dirEntryByUrlIndex;
}
util.binarySearch(startIndex, articleCount, function(i) {
return searchFunction(i).then(function(dirEntry) {
util.binarySearch(startIndex, articleCount, function (i) {
return searchFunction(i).then(function (dirEntry) {
if (search.status === 'cancelled') return 0;
var ns = dirEntry.namespace;
var ti = search.searchUrlIndex ? dirEntry.url : dirEntry.getTitleOrUrl();
@ -506,8 +507,8 @@ ZIMArchive.prototype.findDirEntriesWithPrefixCaseSensitive = function (prefix, s
}, true).then(function (firstIndex) {
var vDirEntries = [];
var addDirEntries = function (index, lastTitle) {
if (search.status === 'cancelled' || search.found >= search.size || index >= articleCount
|| lastTitle && !~lastTitle.indexOf(prefix) || index - firstIndex >= search.window) {
if (search.status === 'cancelled' || search.found >= search.size || index >= articleCount ||
lastTitle && !~lastTitle.indexOf(prefix) || index - firstIndex >= search.window) {
// DEV: Diagnostics to be removed before merge
if (vDirEntries.length) {
console.debug('Scanned ' + (index - firstIndex) + ' titles for "' + prefix +
@ -541,7 +542,7 @@ ZIMArchive.prototype.findDirEntriesWithPrefixCaseSensitive = function (prefix, s
});
};
return addDirEntries(firstIndex);
}).then(function(objWithIndex) {
}).then(function (objWithIndex) {
return callback(objWithIndex.dirEntries, objWithIndex.nextStart);
});
};
@ -552,7 +553,7 @@ ZIMArchive.prototype.findDirEntriesWithPrefixCaseSensitive = function (prefix, s
* @param {Object} search The appstate.search object
* @param {Array} dirEntries The array of already found Directory Entries
* @param {Integer} number Optional positive number of search results requested (otherwise params.maxSearchResults will be used)
* @returns {Promise<callbackDirEntry>} The augmented array of Directory Entries with titles that correspond to search
* @returns {Promise<callbackDirEntry>} The augmented array of Directory Entries with titles that correspond to search
*/
ZIMArchive.prototype.findDirEntriesFromFullTextSearch = function (search, dirEntries, number) {
var cns = this.getContentNamespace();
@ -655,10 +656,10 @@ ZIMArchive.prototype.readUtf8File = function (dirEntry, callback) {
return callback(dirEntry, '');
}
var cns = appstate.selectedArchive.getContentNamespace();
return dirEntry.readData().then(function(data) {
return dirEntry.readData().then(function (data) {
var mimetype = dirEntry.getMimetype();
if (window.TextDecoder) {
data = new TextDecoder('utf-8').decode(data);
data = new TextDecoder('utf-8').decode(data);
} else {
// Support for IE11 and Edge Legacy - only support UTF-8 decoding
data = utf8.parse(data);
@ -691,9 +692,9 @@ ZIMArchive.prototype.readUtf8File = function (dirEntry, callback) {
} else {
// DEV: Note that we cannot terminate regex below with $ because there is a (rogue?) mimetype
// of 'text/html;raw=true'
if (params.zimType === 'zimit' && /\/(?:html|css|javascript)\b/i.test(mimetype)) {
if (params.zimType === 'zimit' && /\/(?:x?html|css|javascript)\b/i.test(mimetype)) {
data = transformZimit.transformReplayUrls(dirEntry, data, mimetype);
}
}
callback(dirEntry, data);
}
}).catch(function (e) {
@ -729,7 +730,7 @@ ZIMArchive.prototype.readBinaryFile = function (dirEntry, callback) {
} else {
// DEV: Note that we cannot terminate regex below with $ because there is a (rogue?) mimetype
// of 'text/html;raw=true'
if (params.zimType === 'zimit' && /\/(?:html|css|javascript)\b/i.test(mimetype)) {
if (params.zimType === 'zimit' && /\/(?:x?html|css|javascript)\b/i.test(mimetype)) {
data = transformZimit.transformReplayUrls(dirEntry, utf8.parse(data), mimetype);
}
callback(dirEntry, data);
@ -759,9 +760,9 @@ ZIMArchive.prototype.getDirEntryByPath = function (path, zimitResolving, origina
path = revisedPath;
}
}
return util.binarySearch(0, this.file.entryCount, function(i) {
return that.file.dirEntryByUrlIndex(i).then(function(dirEntry) {
var url = dirEntry.namespace + "/" + dirEntry.url;
return util.binarySearch(0, this.file.entryCount, function (i) {
return that.file.dirEntryByUrlIndex(i).then(function (dirEntry) {
var url = dirEntry.namespace + '/' + dirEntry.url;
if (path < url) {
return -1;
} else if (path > url) {
@ -799,7 +800,7 @@ ZIMArchive.prototype.getDirEntryByPath = function (path, zimitResolving, origina
var search = {
rgxPrefix: new RegExp('.*' + rgxPath, 'i'),
searchUrlIndex: true,
lc: true, // Make the comparator (e.g. dirEntry.url) lowercase
lc: true, // Make the comparator (e.g. dirEntry.url) lowercase
size: 1,
found: 0
}
@ -807,7 +808,7 @@ ZIMArchive.prototype.getDirEntryByPath = function (path, zimitResolving, origina
} else {
var newpath = path.replace(/^((?:A|C\/A)\/)[^/]+\/(.+)$/, '$1$2');
if (newpath === path) return null; // No further paths to explore!
console.log("Article " + path + " not available, but moving up one directory to compensate for ZIM coding error...");
console.log('Article ' + path + ' not available, but moving up one directory to compensate for ZIM coding error...');
return that.getDirEntryByPath(newpath);
}
} else {
@ -821,10 +822,10 @@ ZIMArchive.prototype.getDirEntryByPath = function (path, zimitResolving, origina
/**
* Initiate a fuzzy search for dirEntries matching the search object
* @param {String} path Human-readable path to search for
* @param {Object} search The search object
* @param {Object} search The search object
* @returns {Promise<DirEntry>} A Promise that resolves to a Directory Entry, or null if not found
*/
function fuzzySearch(path, search) {
function fuzzySearch (path, search) {
return new Promise(function (resolve, reject) {
console.log('Initiating fuzzy search for ' + path + '...');
uiUtil.pollSpinner('Fuzzy search for ' + path + '...', true);
@ -850,7 +851,7 @@ function fuzzySearch(path, search) {
}
/**
*
*
* @param {callbackDirEntry} callback
*/
ZIMArchive.prototype.getRandomDirEntry = function (callback) {

View File

@ -70,10 +70,11 @@ params.decompressorAPI = {
/**
* A variable to keep track of the currently loaded ZIM archive, e.g., for labelling cache entries
* The ID is temporary and is reset to 0 at each session start; it is incremented by 1 each time a new ZIM is loaded
* The ID is temporary and is reset to a random number at each session start; it is incremented by 1 each time a new ZIM is loaded
* It allows for up to 10,000 distinct ZIM archives to be loaded in any one session
* @type {Integer}
*/
var tempFileId = 0;
var tempFileId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER / 10000);
/**
* A Map to keep track of temporary File IDs
@ -229,8 +230,7 @@ ZIMFile.prototype.dirEntry = function (offset) {
* @returns {Promise<DirEntry>} A Promise for the requested DirEntry
*/
ZIMFile.prototype.dirEntryByUrlIndex = function (index) {
var that = appstate.selectedArchive.file;
if (!that) return Promise.resolve(null);
var that = this || appstate.selectedArchive.file;
return that._readInteger(that.urlPtrPos + index * 8, 8).then(function (dirEntryPos) {
return that.dirEntry(dirEntryPos);
});
@ -242,7 +242,7 @@ ZIMFile.prototype.dirEntryByUrlIndex = function (index) {
* @returns {Promise<DirEntry>} A Promise for the requested DirEntry
*/
ZIMFile.prototype.dirEntryByTitleIndex = function (index) {
var that = appstate.selectedArchive.file;
var that = this || appstate.selectedArchive.file;
// Use v1 title pointerlist if available, or fall back to legacy v0 list
var ptrList = that.articlePtrPos || that.titlePtrPos;
return that._readInteger(ptrList + index * 4, 4).then(function (urlIndex) {