diff --git a/i18n/en.jsonp.js b/i18n/en.jsonp.js index bf12d49fa..0125d229f 100644 --- a/i18n/en.jsonp.js +++ b/i18n/en.jsonp.js @@ -66,8 +66,14 @@ document.localeJson = { "configure-display-apptheme-info": "[ Show article with applied theme ]", "configure-language-selector-default": "Language", "configure-language-selector-other": "More soon...", - "configure-library-mirrors": "Library Mirrors", + "configure-library-all-unreachable": "All library servers are currently unreachable.", "configure-library-altlibrary": "The library at", + "configure-library-connecting": "Connecting to Library", + "configure-library-incompatible": "Browser is incompatible with primary library", + "configure-library-mirrors": "Library Mirrors", + "configure-library-primary-unreachable": "

Primary server unreachable.

", + "configure-library-trying-alternative": "Attempting to contact backup server", + "configure-library-trying-primary": "Attempting to contact primary library server", "configure-library-unreachable": "appears to be unreachable. Please try one of these mirrors:", "configure-performance-settings-title": "Performance / compatibility", "configure-performance-panel-header": "Caching and preview settings", diff --git a/i18n/es.jsonp.js b/i18n/es.jsonp.js index 5c8f8202c..e624bacf0 100644 --- a/i18n/es.jsonp.js +++ b/i18n/es.jsonp.js @@ -66,9 +66,15 @@ document.localeJson = { "configure-display-apptheme-info": "[ Mostrar artículo con tema seleccionado ]", "configure-language-selector-default": "Idioma", "configure-language-selector-other": "Más pronto...", - "configure-library-mirrors": "Espejos de la biblioteca", + "configure-library-all-unreachable": "Todos los servidores de la biblioteca están inalcanzables actualmente.", "configure-library-altlibrary": "La biblioteca en", - "configure-library-unreachable": "parece ser inalcanzable. Por favor, pruebe uno de estos espejos:", + "configure-library-connecting": "Conectando a la biblioteca", + "configure-library-incompatible": "Navegador no es compatible con la biblioteca primaria", + "configure-library-mirrors": "Espejos de la biblioteca", + "configure-library-primary-unreachable": "

Servidor primario inalcanzable.

", + "configure-library-trying-alternative": "Intentando contactar con el servidor de respaldo", + "configure-library-trying-primary": "Intentando contactar con el servidor de la biblioteca primaria", + "configure-library-unreachable": "parece inalcanzable. Por favor, pruebe uno de estos espejos:", "configure-performance-settings-title": "Rendimiento y compatibilidad", "configure-performance-panel-header": "Ajustes de caché y vista previa", "configure-performance-cacheassets-description": "Kiwix JS puede acelerar la visualización de artículos almacenando en caché los activos:", diff --git a/i18n/fr.jsonp.js b/i18n/fr.jsonp.js index c1805be61..db2b95948 100644 --- a/i18n/fr.jsonp.js +++ b/i18n/fr.jsonp.js @@ -66,8 +66,14 @@ document.localeJson = { "configure-display-apptheme-info": "[ Afficher l'article avec le thème sélectionné ]", "configure-language-selector-default": "Langue", "configure-language-selector-other": "Bientôt plus...", - "configure-library-mirrors": "Miroirs de bibliothèque", + "configure-library-all-unreachable": "Tous les serveurs de bibliothèque sont actuellement inaccessibles.", "configure-library-altlibrary": "La bibliothèque à", + "configure-library-connecting": "Connexion à la bibliothèque", + "configure-library-incompatible": "Navigateur est incompatible avec la bibliothèque principale", + "configure-library-mirrors": "Miroirs de bibliothèque", + "configure-library-primary-unreachable": "

Serveur principal inaccessible.

", + "configure-library-trying-alternative": "Tentative de contact avec le serveur de sauvegarde", + "configure-library-trying-primary": "Tentative de contact avec le serveur de bibliothèque principal", "configure-library-unreachable": "semble être inaccessible. Veuillez essayer un de ces miroirs :", "configure-performance-settings-title": "Performances et compatibilité", "configure-performance-panel-header": "Mise en caché et prévisualisation", diff --git a/service-worker.js b/service-worker.js index 6f2342ca1..c72202a11 100644 --- a/service-worker.js +++ b/service-worker.js @@ -137,6 +137,7 @@ const precacheFiles = [ 'www/js/lib/arrayFromPolyfill.js', 'www/js/lib/filecache.js', 'www/js/lib/cache.js', + 'www/js/lib/kiwixLibrary.js', 'www/js/lib/popovers.js', 'www/js/lib/promisePolyfill.js', 'www/js/lib/settingsStore.js', diff --git a/www/js/app.js b/www/js/app.js index cdd25e345..fc89b096d 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -35,6 +35,7 @@ import popovers from './lib/popovers.js'; import settingsStore from './lib/settingsStore.js'; import abstractFilesystemAccess from './lib/abstractFilesystemAccess.js'; import translateUI from './lib/translateUI.js'; +import kiwixLibrary from './lib/kiwixLibrary.js'; if (params.abort) { // If the app was loaded only to pass a message from the remote code, then we exit immediately @@ -1791,37 +1792,11 @@ async function handleFileDrop (packet) { const btnLibrary = document.getElementById('btnLibrary'); btnLibrary.addEventListener('click', function (e) { - e.preventDefault(); const libraryContent = document.getElementById('libraryContent'); const libraryIframe = libraryContent.contentWindow.document.getElementById('libraryIframe'); - libraryIframe.src = 'about:blank'; // Empty the iframe - const xhr = new XMLHttpRequest(); - xhr.open('HEAD', params.libraryUrl, true); - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status >= 200 && xhr.status < 300) { - try { - // eslint-disable-next-line no-new-func - Function('try{}catch{}')(); // Tests the browser can run code that will be used by the library - libraryIframe.setAttribute('src', params.libraryUrl); - uiUtil.tabTransitionToSection('library', params.showUIAnimations); - resizeIFrame(); - } catch (error) { - console.warn('Browser cannot run code in the library iframe', error); - handleLibraryError(libraryIframe); - } - } else { - console.warn('Library server ' + params.libraryUrl + ' is unreachable...'); - handleLibraryError(libraryIframe); - } - } - }; - xhr.onerror = function () { - handleLibraryError(libraryIframe); - }; - xhr.send(); uiUtil.tabTransitionToSection('library', params.showUIAnimations); resizeIFrame(); + kiwixLibrary.loadLibrary(libraryIframe); }); // Add keyboard activation for library button btnLibrary.addEventListener('keydown', function (e) { @@ -1830,44 +1805,7 @@ btnLibrary.addEventListener('keydown', function (e) { btnLibrary.click(); } }); -// Error handler for library iframe -function handleLibraryError (iframe) { - iframe.onload = function () { - iframe.onload = null; - setTimeout(function () { - try { - if (!iframe.contentWindow || !iframe.contentWindow.document) { - console.warn('Library server ' + params.altLibraryUrl + ' is unreachable...'); - throw new Error('Iframe content not accessible'); - } - } catch (error) { - let htmlDoc = ''; - htmlDoc += translateUI.t('configure-library-mirrors') || 'Library Mirrors'; - htmlDoc += '

'; - htmlDoc += translateUI.t('configure-library-mirrors') || 'Library Mirrors'; - htmlDoc += '

' - htmlDoc += translateUI.t('configure-library-altlibrary') || 'The library at'; - htmlDoc += ' ' + params.altLibraryUrl + ' '; - htmlDoc += translateUI.t('configure-library-unreachable') || 'appears to be unreachable. Please try one of these mirrors:'; - htmlDoc += '

'; - iframe.onload = function () { - iframe.onload = null; - iframe.contentWindow.document.open(); - iframe.contentWindow.document.write(htmlDoc); - iframe.contentWindow.document.close(); - uiUtil.tabTransitionToSection('library', params.showUIAnimations); - resizeIFrame(); - } - iframe.src = 'about:blank'; - } - }, 500); // Adjust the timeout if too long - }; - iframe.setAttribute('src', params.altLibraryUrl); -}; + // Add event listener to link which allows user to show file selectors document.getElementById('selectorsDisplayLink').addEventListener('click', function (e) { e.preventDefault(); diff --git a/www/js/lib/kiwixLibrary.js b/www/js/lib/kiwixLibrary.js new file mode 100644 index 000000000..bd807d953 --- /dev/null +++ b/www/js/lib/kiwixLibrary.js @@ -0,0 +1,312 @@ +/** + * kiwixLibrary.js : A module for loading a Library of Kiwix offline resources into an iframe + * + * Copyright 2024 Jaifroid and contributors + * Licence GPL v3: + * + * This file is part of Kiwix. + * + * Kiwix is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public Licence as published by + * the Free Software Foundation, either version 3 of the Licence, or + * (at your option) any later version. + * + * Kiwix is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public Licence for more details. + * + * You should have received a copy of the GNU General Public Licence + * along with Kiwix (file LICENSE-GPLv3.txt). If not, see + */ + +'use strict'; + +/* global params */ + +import translateUI from './translateUI.js'; + +/** + * Tests if browser can execute code required for library.kiwix.org functionality + * @returns {boolean} True if the browser can execute required code in the library iframe + */ +function canExecuteCode () { + try { + // eslint-disable-next-line no-new-func + Function('try{}catch{}')(); + return true; + } catch (error) { + console.warn('Browser cannot run required code for ' + params.libraryUrl, error); + return false; + } +} + +/** + * Attempts to load a URL header and returns a Promise with the result + * @param {String} url A URL to check + * @returns {Promise} A promise that resolves if the URL is reachable, or rejects with an error + */ +function checkUrl (url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('HEAD', url, true); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(); + } else { + reject(new Error(`Server ${url} returned status ${xhr.status}`)); + } + } + }; + xhr.onerror = () => reject(new Error(`Cannot reach ${url}`)); + xhr.send(); + }); +} + +/** + * Creates the base HTML template with a status message area and loading indicator + * @returns {String} The base HTML template + */ +function createBaseHtml () { + const title = translateUI.t('configure-library-connecting') || 'Connecting to Library'; + return ` + + + ${title} + + + +
+

+ ${title} + ... + +

+
+
+
+ + + `; +} + +/** + * Updates the status message in the iframe with loading indicator + * @param {Object} frame The iframe DOM object + * @param {String} message The status message to display in the iframe + */ +function updateStatus (frame, message) { + try { + const statusElement = frame.contentWindow.document.getElementById('statusMessage'); + if (statusElement) { + statusElement.innerHTML = `

${message}

`; + } + } catch (e) { + console.warn('Could not update status:', e); + } +} + +/** + * Initialize the iframe with the base template + * @param {Objet} frame The iframe DOM object to initialize + * @returns {Promise} A promise that resolves when the iframe is initialized + */ +function initializeFrame (frame) { + return new Promise((resolve) => { + frame.src = 'about:blank'; + frame.onload = () => { + frame.onload = null; + frame.contentWindow.document.open(); + frame.contentWindow.document.write(createBaseHtml()); + frame.contentWindow.document.close(); + resolve(); // Signal that initialization is complete + }; + }); +} + +/** + * Creates HTML content for the mirror list + * @returns {String} The HTML content for the mirror list + */ +function createMirrorListHtml () { + const mirrorListText = translateUI.t('configure-library-mirrors') || 'Library Mirrors'; + const unreachableMsg = translateUI.t('configure-library-unreachable') || 'appears to be unreachable. Please try one of these mirrors:'; + const altLibraryMsg = translateUI.t('configure-library-altlibrary') || 'The library at'; + + let html = ` +

${mirrorListText}

+

+ ${altLibraryMsg} ${params.altLibraryUrl} ${unreachableMsg} +

+ '; + return html; +} + +/** + * Displays the mirror list in the iframe + * @param {Object} frame The iframe DOM object in which to display the mirror list + */ +function showMirrorList (frame) { + try { + const doc = frame.contentWindow.document; + // Remove the loading heading since we're showing mirrors + const mainHeading = doc.getElementById('mainHeading'); + if (mainHeading && mainHeading.parentNode) { + // Using the traditional removeChild method for IE11 compatibility + mainHeading.parentNode.removeChild(mainHeading); + } + // Update status with error message + const statusElement = doc.getElementById('statusMessage'); + const mirrorListElement = doc.getElementById('mirrorList'); + + if (statusElement && mirrorListElement) { + const allUnreachableMsg = translateUI.t('configure-library-all-unreachable') || + 'All library servers are currently unreachable.'; + // Show error message in status area + statusElement.innerHTML = `

${allUnreachableMsg}

`; + // Show mirror list + mirrorListElement.innerHTML = createMirrorListHtml(); + } + } catch (e) { + console.warn('Could not show mirror list:', e); + } +} + +/** + * Main library loading logic + * @param {Object} iframe The iframe DOM object into which to load the library + */ +async function loadLibrary (iframe) { + await initializeFrame(iframe); + try { + // First check browser compatibility + if (!canExecuteCode()) { + const incompatibilityError = new Error('Browser cannot execute code'); + incompatibilityError.name = 'BrowserIncompatibilityError'; + throw incompatibilityError; + } + // Try primary library URL + const tryingPrimaryMsg = translateUI.t('configure-library-trying-primary') || + 'Attempting to contact primary library server'; + updateStatus(iframe, tryingPrimaryMsg + ' ' + params.libraryUrl); + await checkUrl(params.libraryUrl); + iframe.src = params.libraryUrl; + } catch (primaryError) { + let tryingAlternativeMsg; + if (primaryError.name === 'BrowserIncompatibilityError') { + // For browser incompatibility, combine both messages + console.warn('Browser compatibility check failed: Browser cannot execute code in library iframe'); + const incompatibilityMsg = translateUI.t('configure-library-incompatible') || + 'Browser is incompatible with primary library'; + tryingAlternativeMsg = translateUI.t('configure-library-trying-alternative') || + 'Attempting to contact backup server'; + // Show both messages together, maintaining the error styling for the incompatibility message + updateStatus(iframe, + `

${incompatibilityMsg}

+

${tryingAlternativeMsg} ${params.altLibraryUrl}

`); + } else { + // For connection errors, show the standard unreachable message + console.warn('Primary library unreachable:', primaryError); + tryingAlternativeMsg = translateUI.t('configure-library-primary-unreachable') || + '

Primary server unreachable.

'; + tryingAlternativeMsg += translateUI.t('configure-library-trying-alternative') || + 'Attempting to contact backup server'; + updateStatus(iframe, tryingAlternativeMsg + ' ' + params.altLibraryUrl + '

'); + } + try { + // Try alternative library URL + await checkUrl(params.altLibraryUrl); + iframe.src = params.altLibraryUrl; + } catch (alternativeError) { + console.warn('Alternative library unreachable:', alternativeError); + showMirrorList(iframe); + } + } +} + +export default { + loadLibrary +};