diff --git a/service-worker.js b/service-worker.js index 07c8e037..4bb6df49 100644 --- a/service-worker.js +++ b/service-worker.js @@ -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]); + }); + }); }); } diff --git a/www/js/app.js b/www/js/app.js index f69bcd54..c29c35b8 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -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 && /]*>/i.test(params.transformedHTML)) { + if (transformedHTML && /]*>/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(/(]*)>/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*(?:\n' + htmlArticle : htmlArticle; - params.transDirEntry = dirEntry; + transformedHTML = !/^\s*(?:\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; diff --git a/www/js/lib/transformZimit.js b/www/js/lib/transformZimit.js index 0bd33fb8..ea384150 100644 --- a/www/js/lib/transformZimit.js +++ b/www/js/lib/transformZimit.js @@ -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); diff --git a/www/js/lib/zimArchive.js b/www/js/lib/zimArchive.js index 723bf8a3..b08740bf 100644 --- a/www/js/lib/zimArchive.js +++ b/www/js/lib/zimArchive.js @@ -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} The augmented array of Directory Entries with titles that correspond to search + * @returns {Promise} 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} 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) { diff --git a/www/js/lib/zimfile.js b/www/js/lib/zimfile.js index 50f3739d..d41e790f 100644 --- a/www/js/lib/zimfile.js +++ b/www/js/lib/zimfile.js @@ -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} 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} 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) {