diff --git a/www/index.html b/www/index.html index 0462303f..0743197a 100644 --- a/www/index.html +++ b/www/index.html @@ -738,7 +738,7 @@ diff --git a/www/js/app.js b/www/js/app.js index d9f309ed..007f7e59 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -26,8 +26,8 @@ // This uses require.js to structure javascript: // http://requirejs.org/docs/api.html#define -define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'utf8', 'cache', 'images', 'settingsStore', 'transformStyles', 'kiwixServe'], - function ($, zimArchiveLoader, uiUtil, util, utf8, cache, images, settingsStore, transformStyles, kiwixServe) { +define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'utf8', 'cache', 'images', 'settingsStore', 'transformStyles', 'kiwixServe', 'updater'], + function ($, zimArchiveLoader, uiUtil, util, utf8, cache, images, settingsStore, transformStyles, kiwixServe, updater) { /** * The delay (in milliseconds) between two "keepalive" messages @@ -866,6 +866,27 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'utf8', 'cache', 'images }); } + // Check for GitHub updates + function checkUpdateServer() { + if (!params.allowInternetAccess) { + console.log("The update check was blocked because the user has not allowed Internet access.") + return; + } + updater.getLatestUpdates(function (tag, url, releases) { + var updateSpan = document.getElementById('updateStatus'); + if (!tag) { + updateSpan.innerHTML = '[ App is up to date ]'; + console.log('No new update was found.'); + return; + } + console.log('We found this update: [' + tag + '] ' + url, releases); + updateSpan.innerHTML = '[ New update! ]'; + uiUtil.showUpgradeReady(tag.replace(/^v/, ''), 'download', url); + }); + } + // Do check on startup + setTimeout(checkUpdateServer, 15000); + function setActiveBtn(activeBtn) { document.getElementById('btnHome').classList.remove("active"); document.getElementById('btnRandomArticle').classList.remove("active"); @@ -1125,7 +1146,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'utf8', 'cache', 'images 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) && - settingsStore.getItem('allowInternetAccess') === 'true') { + 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.

' @@ -1188,8 +1209,11 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'utf8', 'cache', 'images }); } } - settingsStore.setItem('allowInternetAccess', false, Infinity); + } else { + // We can check for updates if the user has allowed Internet access + checkUpdateServer(); } + settingsStore.setItem('allowInternetAccess', params.allowInternetAccess, Infinity); }); $('input:checkbox[name=cssCacheMode]').on('change', function (e) { params.cssCache = this.checked ? true : false; diff --git a/www/js/init.js b/www/js/init.js index ae0cb655..7da8e0aa 100644 --- a/www/js/init.js +++ b/www/js/init.js @@ -99,7 +99,7 @@ params['alphaChar'] = getSetting('alphaChar') || 'A'; //Set default start of alp params['omegaChar'] = getSetting('omegaChar') || 'Z'; //Set default end of alphabet string params['contentInjectionMode'] = getSetting('contentInjectionMode') || ((navigator.serviceWorker && !/^(ms-appx-web:)$/i.test(window.location.protocol) && !/Android/.test(params.appType) && !window.nw) ? 'serviceworker' : 'jquery'); // Deafault to SW mode if the browser supports it -params['allowInternetAccess'] = getSetting('allowInternetAccess'); +params['allowInternetAccess'] = getSetting('allowInternetAccess') !== null ? getSetting('allowInternetAccess') : true; params['openExternalLinksInNewTabs'] = getSetting('openExternalLinksInNewTabs') !== null ? getSetting('openExternalLinksInNewTabs') : true; // Parameter to turn on/off opening external links in new tab params['windowOpener'] = getSetting('windowOpener'); // 'tab|window|false' A setting that determines whether right-click/long-press of a ZIM link opens a new window/tab params['rightClickType'] = getSetting('rightClickType'); // 'single|double|false' A setting that determines whether a single or double right-click is used to open a new window/tab diff --git a/www/js/lib/uiUtil.js b/www/js/lib/uiUtil.js index 9da604a8..c1483be9 100644 --- a/www/js/lib/uiUtil.js +++ b/www/js/lib/uiUtil.js @@ -674,17 +674,19 @@ define(rqDef, function(util) { /** * Shows that an upgrade is ready to install * @param {String} ver The version of the upgrade - * @param {String} type Either 'load' or 'install' according to the type of upgrade + * @param {String} type Either 'load', 'install' or 'download' according to the type of upgrade + * @param {String} url An optional download URL */ - function showUpgradeReady(ver, type) { + function showUpgradeReady(ver, type, url) { params.upgradeNeeded = true; document.getElementById('alertBoxPersistent').innerHTML = '
\n' + ' ×\n' + ' \n' + '
\n'; - document.getElementById('persistentMessage').innerHTML = 'Version ' + ver + ' is ready to ' - + type + '! (Re-launch app to ' + type + '.)'; + document.getElementById('persistentMessage').innerHTML = 'Version ' + ver + + (url ? ' is available to ' + type + '! Go to ' + url + '' + : ' is ready to ' + type + '! (Re-launch app to ' + type + '.)'); } /** diff --git a/www/js/lib/updater.js b/www/js/lib/updater.js new file mode 100644 index 00000000..63e2b15b --- /dev/null +++ b/www/js/lib/updater.js @@ -0,0 +1,114 @@ +/** + * updater.js : Functions for checking and initiating app updtes + * + * Copyright 2013-2022 Jaifroid, Mossroy and contributors + * License 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 License as published by + * the Free Software Foundation, either version 3 of the License, 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 License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Kiwix (file LICENSE-GPLv3.txt). If not, see + */ + +'use strict'; + +define(['uiUtil'], function (uiUtil) { + + /** + * The update server configuration + */ + params.updateServer = { + url: 'https://api.github.com/repos/kiwix/kiwix-js-windows/', + releases: 'releases' + }; + + // A RegExp prototype string to match the current app's releases + const baseApp = (params.packagedFile && /wikivoyage/.test(params.packagedFile)) ? 'wikivoyage' : + (params.packagedFile && /wikmed|mdwiki/.test(params.packagedFile)) ? 'wikimed' : + 'windows|electron|kiwixwebapp_'; // Default value + + // A RegExp to match download URLs of releases + const regexpMatchGitHubReleases = RegExp('"browser_download_url[":\\s]+"(https:.*download\\/([^\\/]+).*(?:' + baseApp + ')[^"]+)"', 'ig'); + + /** + * Get and return the JSON list of releases from the update server's REST API + * + * @param {Function} callback The function to call with the data + * @returns {String} A JSON string containing hierarchical release data + */ + function getReleasesObject(callback) { + uiUtil.XHR(params.updateServer.url + params.updateServer.releases, 'text', + function (response, mimetype, status) { + if (status === 200) { + callback(response); + } else callback(''); + } + ); + } + + /** + * A function to get the latest updates from a GitHub releases source + * Only updates that are greater than the current update are returned + * Attempts to match by channel, but also matches non-channel releases + * + * @param {Function} callback A function to call back with the results + * @returns {Object} Calls back with update tag, update URL, and array of releases + */ + function getLatestUpdates(callback) { + var updatedReleases = []; + var currentRelease = params.appVersion.replace(/^v?([\d.]+)/, '$1'); + var currentReleaseChannel = params.appVersion.replace(/^[v\d.]+/, ''); + var updateTag; + var channelMatchedTag; + var updateUrl; + var channelMatchedUpdateUrl; + getReleasesObject(function (releases) { + var releaseFile; + var releaseVersion; + var releaseChannel; + // Loop through every line in releases + var matchedRelease = regexpMatchGitHubReleases.exec(releases); + while (matchedRelease != null) { + releaseFile = matchedRelease[1]; + releaseVersion = matchedRelease[2].replace(/^v?([\d.]+).*/, '$1'); + releaseChannel = matchedRelease[2].replace(/^[v\d.]+/, ''); + // Compare the releases using a version-type comparison + if (releaseVersion.localeCompare(currentRelease, { numeric: true, sensitivity: 'base' }) === 1) { + if (!channelMatchedTag && currentReleaseChannel === releaseChannel) { + channelMatchedTag = matchedRelease[2]; + channelMatchedUpdateUrl = releaseFile.replace(/\/download\//, '/tag/').replace(/[^/]+$/, ''); + } + if (!updateTag) updateTag = matchedRelease[2]; + if (!updateUrl) updateUrl = releaseFile.replace(/\/download\//, '/tag/').replace(/[^/]+$/, ''); + updatedReleases.push(releaseFile) + } + matchedRelease = regexpMatchGitHubReleases.exec(releases); + } + // We should now have a list of all candidate updates, and candidate channel update + // Compare the channel-matched update wiht the update, and if they are same underlying version number, choose channel match + if (updateTag && updateTag.replace(/^v?([\d.]+).*/, '$1') === channelMatchedTag.replace(/^v?([\d.]+).*/, '$1')) { + updateTag = channelMatchedTag; + updateUrl = channelMatchedUpdateUrl; + } + callback(updateTag, updateUrl, updatedReleases); + }); + } + + + /** + * Functions and classes exposed by this module + */ + return { + getLatestUpdates: getLatestUpdates + }; +}); \ No newline at end of file