Migrate to extension Manifest v3 for Chromium #755 (#984)

Both MV2 and MV3 are built for Chromium. Only MV2 for Firefox for now (until Service Workers are supported as backgroundscript.js).
This commit is contained in:
Jaifroid 2023-06-13 21:21:40 +01:00 committed by GitHub
parent bdfe50562c
commit bd7393e921
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 365 additions and 287 deletions

View File

@ -17,6 +17,8 @@ module.exports = {
'no-extra-parens': 1,
'no-unused-expressions': 1,
'no-unused-vars': 1,
'n/no-callback-literal': 0
'n/no-callback-literal': 0,
'object-shorthand': 0,
'multiline-ternary': 0
}
}

View File

@ -19,11 +19,11 @@ Please follow these guidelines when contributing:
- be sure to test your fix in both "JQuery" mode and "Service Worker" mode (see Configuration);
- run the Unit tests (see below) in at least the above browsers.
If all the tests are working fine, you can finally test the extension versions, like this:
If all the tests are working fine, you can finally test the extension versions. Plese note that we are using Manifest V3 for the Chromium extensions, and Manifest V2
for the Firefox extension, so there are different instructions for the two browser families:
- Remove the '-WIP' from the version key from the manifest.json file present in the root of this repo;
- In Chromium, you can install the extension by loading the root folder with Extensions -> Load Unpacked (with Developer Mode turned ON) -> select the root folder of the repository;
- In Firefox, you can load an extension with Manage Your Extensions -> Debug Add-ons -> Load Temporary Add-on, and then pick any file in the repository.
- In Firefox, you need to rename manifest.json to manifest.v3.json, and then rename manifest.v2.json to manifest.json. Then you can load the extension with Manage Your Extensions -> Debug Add-ons -> Load Temporary Add-on, and then pick any file in the repository.
If your feature works and tests are passing, make a PR, describe the testing you have done, and ask for a code review.

View File

@ -1,5 +1,5 @@
/**
* backgroundscript.js: Background script for the WebExtension
* backgroundscript.js: Background script for the WebExtension Manifest V2
*
* Copyright 2017 Mossroy and contributors
* License GPL v3:
@ -20,22 +20,20 @@
* along with Kiwix (file LICENSE-GPLv3.txt). If not, see <http://www.gnu.org/licenses/>
*/
/* global chrome, browser */
// In order to work on both Firefox and Chromium/Chrome (and derivatives).
// browser and chrome variables expose almost the same APIs
var genericBrowser;
if (typeof browser !== 'undefined') {
// Firefox
genericBrowser = browser;
}
else {
} else {
// Chromium/Chrome
genericBrowser = chrome;
}
genericBrowser.browserAction.onClicked.addListener(handleClick);
function handleClick(event) {
genericBrowser.tabs.create({
url: genericBrowser.runtime.getURL('/www/index.html')
genericBrowser.browserAction.onClicked.addListener(function () {
var newURL = chrome.runtime.getURL('www/index.html');
chrome.tabs.create({ url: newURL });
});
}

View File

@ -1,9 +1,9 @@
{
"manifest_version": 2,
"manifest_version": 3,
"name": "Kiwix",
"version": "3.8.1",
"description": "Kiwix : offline Wikipedia reader",
"description": "Kiwix Offline Browser",
"icons": {
"16": "www/img/icons/kiwix-16.png",
@ -16,7 +16,7 @@
"128": "www/img/icons/kiwix-128.png"
},
"browser_action": {
"action": {
"default_icon": {
"16": "www/img/icons/kiwix-16.png",
"19": "www/img/icons/kiwix-19.png",
@ -27,21 +27,23 @@
"default_title": "Kiwix"
},
"applications": {
"gecko": {
"id": "kiwix-html5-unlisted@kiwix.org"
}
},
"web_accessible_resources": ["www/index.html"],
"background": {
"scripts": ["webextension/backgroundscript.js"]
"service_worker": "service-worker.js"
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"permissions": ["storage", "activeTab", "scripting"],
"author": "mossroy",
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';",
"sandbox": "sandbox allow-scripts allow-downloads allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self';"
},
"web_accessible_resources": [{
"resources": ["www/index.html", "www/article.html"],
"matches": ["https://*.kiwix.org/*"]
}],
"author": "Kiwix",
"homepage_url": "https://www.kiwix.org",
"offline_enabled": true
}

47
manifest.v2.json Normal file
View File

@ -0,0 +1,47 @@
{
"manifest_version": 2,
"name": "Kiwix",
"version": "3.8.1",
"description": "Kiwix : offline Wikipedia reader",
"icons": {
"16": "www/img/icons/kiwix-16.png",
"19": "www/img/icons/kiwix-19.png",
"32": "www/img/icons/kiwix-32.png",
"38": "www/img/icons/kiwix-38.png",
"48": "www/img/icons/kiwix-48.png",
"64": "www/img/icons/kiwix-64.png",
"90": "www/img/icons/kiwix-90.png",
"128": "www/img/icons/kiwix-128.png"
},
"browser_action": {
"default_icon": {
"16": "www/img/icons/kiwix-16.png",
"19": "www/img/icons/kiwix-19.png",
"32": "www/img/icons/kiwix-32.png",
"38": "www/img/icons/kiwix-38.png",
"64": "www/img/icons/kiwix-64.png"
},
"default_title": "Kiwix"
},
"applications": {
"gecko": {
"id": "kiwix-html5-unlisted@kiwix.org"
}
},
"web_accessible_resources": ["www/index.html"],
"background": {
"scripts": ["backgroundscript.js"]
},
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"author": "mossroy",
"homepage_url": "https://www.kiwix.org",
"offline_enabled": true
}

View File

@ -55,7 +55,7 @@ fi
# Copy only the necessary files in a temporary directory
mkdir -p tmp
rm -rf tmp/*
cp -r www webextension manifest.json manifest.webapp LICENSE-GPLv3.txt service-worker.js README.md tmp/
cp -r www manifest.json manifest.v2.json manifest.webapp LICENSE-GPLv3.txt service-worker.js README.md tmp/
# Remove unwanted files
rm -f tmp/www/js/lib/libzim-*dev.*
@ -64,8 +64,10 @@ rm -f tmp/www/js/lib/libzim-*dev.*
regexpNumericVersion='^[0-9\.]+$'
if [[ $VERSION =~ $regexpNumericVersion ]] ; then
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.json
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.v2.json
else
sed -i -e "s/$VERSION_TO_REPLACE/$MAJOR_NUMERIC_VERSION/" tmp/manifest.json
sed -i -e "s/$VERSION_TO_REPLACE/$MAJOR_NUMERIC_VERSION/" tmp/manifest.v2.json
fi
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.webapp
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/service-worker.js
@ -73,17 +75,26 @@ sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/www/js/app.js
mkdir -p build
rm -rf build/*
# Package for Chromium/Chrome
scripts/package_chrome_extension.sh $DRYRUN $TAG -v $VERSION
# Package for Chromium/Chrome with Manifest V3
scripts/package_chrome_extension.sh -m 3 $DRYRUN $TAG -v $VERSION
# Package for Chromium/Chrome with Manifest V2
cp backgroundscript.js tmp/
rm tmp/manifest.json
mv tmp/manifest.v2.json tmp/manifest.json
scripts/package_chrome_extension.sh -m 2 $DRYRUN $TAG -v $VERSION
# Package for Firefox and Firefox OS
# We have to put a unique version string inside the manifest.json (which Chrome might not have accepted)
# So we take the original manifest again, and replace the version inside it again
cp manifest.json tmp/
# So we take the original manifest v2 again, and replace the version inside it again
cp manifest.v2.json tmp/manifest.json
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION_FOR_MOZILLA_MANIFEST/" tmp/manifest.json
echo ""
scripts/package_firefox_extension.sh $DRYRUN $TAG -v $VERSION
echo ""
scripts/package_firefoxos_app.sh $DRYRUN $TAG -v $VERSION
cp -f ubuntu_touch/* tmp/
sed -i -e "s/$VERSION_TO_REPLACE/$VERSION/" tmp/manifest.json
echo ""
scripts/package_ubuntu_touch_app.sh $DRYRUN $TAG -v $VERSION
# Change permissions on source files to match those expected by the server

View File

@ -3,19 +3,23 @@ BASEDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"/..
cd "$BASEDIR"
# Reading arguments
while getopts tdv: option; do
while getopts m:tdv: option; do
case "${option}" in
m) MV=$OPTARG;; # Optionally indicates the manifest version we're using (2 or 3); if present, the version will be added to filename
t) TAG="-t";; # Indicates that we're releasing a public version from a tag
d) DRYRUN="-d";; # Indicates a dryrun test, that does not modify anything on the network
v) VERSION=${OPTARG};;
esac
done
if [ -n $MV ]; then
echo -e "\nManifest version requested: $MV"
VERSION="MV$MV-$VERSION"
fi
echo "Packaging unsigned Chrome extension, version $VERSION"
cd tmp
zip -r ../build/kiwix-chrome-unsigned-extension-$VERSION.zip www webextension manifest.json LICENSE-GPLv3.txt service-worker.js README.md
zip -r ../build/kiwix-chrome-unsigned-extension-$VERSION.zip www manifest.json LICENSE-GPLv3.txt service-worker.js README.md
cd ..
if [ "${TAG}zz" == "zz" ]; then
if [ -z $TAG ]; then
# Package the extension with Chrome or Chromium, if we're not packaging a public version
if hash chromium-browser 2>/dev/null
then
@ -28,6 +32,7 @@ if [ "${TAG}zz" == "zz" ]; then
echo "Signing the extension for $CHROME_BIN, version $VERSION"
$CHROME_BIN --no-sandbox --pack-extension=tmp --pack-extension-key=./scripts/kiwix-html5.pem
mv tmp.crx build/kiwix-chrome-signed-extension-$VERSION.crx
ls -l build/kiwix-chrome-signed-extension-$VERSION.crx
else
echo "This unsigned extension must be manually uploaded to Google to be signed and distributed from their store"
fi

View File

@ -23,6 +23,8 @@
*/
'use strict';
/* global chrome */
/**
* App version number - ENSURE IT MATCHES VALUE IN app.js
* DEV: Changing this will cause the browser to recognize that the Service Worker has changed, and it will
@ -62,7 +64,6 @@ var useAssetsCache = true;
*/
var useAppCache = true;
/**
* A regular expression that matches the Content-Types of assets that may be stored in ASSETS_CACHE
* Add any further Content-Types you wish to cache to the regexp, separated by '|'
@ -99,74 +100,84 @@ const regexpByteRangeHeader = /^\s*bytes=(\d+)-/;
/**
* The list of files that the app needs in order to run entirely from offline code
*/
let precacheFiles = [
".", // This caches the redirect to www/index.html, in case a user launches the app from its root directory
"manifest.json",
"service-worker.js",
"www/css/app.css",
"www/css/bootstrap.css",
"www/css/kiwixJS_invert.css",
"www/css/kiwixJS_mwInvert.css",
"www/css/transition.css",
"www/img/icons/kiwix-256.png",
"www/img/icons/kiwix-32.png",
"www/img/icons/kiwix-60.png",
"www/img/spinner.gif",
"www/img/Icon_External_Link.png",
"www/index.html",
"www/article.html",
"www/main.html",
"www/js/app.js",
"www/js/init.js",
"www/js/lib/abstractFilesystemAccess.js",
"www/js/lib/arrayFromPolyfill.js",
"www/js/lib/bootstrap.bundle.js",
"www/js/lib/filecache.js",
"www/js/lib/jquery-3.7.0.slim.min.js",
"www/js/lib/promisePolyfill.js",
"www/js/lib/require.js",
"www/js/lib/settingsStore.js",
"www/js/lib/uiUtil.js",
"www/js/lib/utf8.js",
"www/js/lib/util.js",
"www/js/lib/xzdec_wrapper.js",
"www/js/lib/zstddec_wrapper.js",
"www/js/lib/zimArchive.js",
"www/js/lib/zimArchiveLoader.js",
"www/js/lib/zimDirEntry.js",
"www/js/lib/zimfile.js",
"www/js/lib/fontawesome/fontawesome.js",
"www/js/lib/fontawesome/solid.js"
const precacheFiles = [
'.', // This caches the redirect to www/index.html, in case a user launches the app from its root directory
'manifest.json',
'service-worker.js',
'www/css/app.css',
'www/css/bootstrap.css',
'www/css/kiwixJS_invert.css',
'www/css/kiwixJS_mwInvert.css',
'www/css/transition.css',
'www/img/icons/kiwix-256.png',
'www/img/icons/kiwix-32.png',
'www/img/icons/kiwix-60.png',
'www/img/spinner.gif',
'www/img/Icon_External_Link.png',
'www/index.html',
'www/article.html',
'www/main.html',
'www/js/app.js',
'www/js/init.js',
'www/js/lib/abstractFilesystemAccess.js',
'www/js/lib/arrayFromPolyfill.js',
'www/js/lib/bootstrap.bundle.js',
'www/js/lib/filecache.js',
'www/js/lib/jquery-3.7.0.slim.min.js',
'www/js/lib/promisePolyfill.js',
'www/js/lib/require.js',
'www/js/lib/settingsStore.js',
'www/js/lib/uiUtil.js',
'www/js/lib/utf8.js',
'www/js/lib/util.js',
'www/js/lib/xzdec_wrapper.js',
'www/js/lib/zstddec_wrapper.js',
'www/js/lib/zimArchive.js',
'www/js/lib/zimArchiveLoader.js',
'www/js/lib/zimDirEntry.js',
'www/js/lib/zimfile.js',
'www/js/lib/fontawesome/fontawesome.js',
'www/js/lib/fontawesome/solid.js'
];
if ('WebAssembly' in self) {
precacheFiles.push(
"www/js/lib/xzdec-wasm.js",
"www/js/lib/xzdec-wasm.wasm",
"www/js/lib/zstddec-wasm.js",
"www/js/lib/zstddec-wasm.wasm",
"www/js/lib/libzim-wasm.js",
"www/js/lib/libzim-wasm.wasm"
'www/js/lib/xzdec-wasm.js',
'www/js/lib/xzdec-wasm.wasm',
'www/js/lib/zstddec-wasm.js',
'www/js/lib/zstddec-wasm.wasm',
'www/js/lib/libzim-wasm.js',
'www/js/lib/libzim-wasm.wasm'
);
} else {
precacheFiles.push(
"www/js/lib/xzdec-asm.js",
"www/js/lib/zstddec-asm.js",
"www/js/lib/libzim-asm.js"
'www/js/lib/xzdec-asm.js',
'www/js/lib/zstddec-asm.js',
'www/js/lib/libzim-asm.js'
);
}
/**
* If we're in a Chromium extension, add a listener to launch the tab when the icon is clicked
*/
if (typeof chrome !== 'undefined' && chrome.action) {
chrome.action.onClicked.addListener(function () {
var newURL = chrome.runtime.getURL('www/index.html');
chrome.tabs.create({ url: newURL });
});
}
// Process install event
self.addEventListener("install", function (event) {
console.debug("[SW] Install Event processing");
self.addEventListener('install', function (event) {
console.debug('[SW] Install Event processing');
// DEV: We can't skip waiting because too many params are loaded at an early stage from the old file before the new one can activate...
// self.skipWaiting();
// We try to circumvent the browser's cache by adding a header to the Request, and it ensures all files are explicitly versioned
var requests = precacheFiles.map(function (urlPath) {
return new Request(urlPath + '?v' + appVersion, { cache: 'no-cache' });
});
if (!regexpExcludedURLSchema.test(requests[0].url)) event.waitUntil(
caches.open(APP_CACHE).then(function (cache) {
if (!regexpExcludedURLSchema.test(requests[0].url)) {
event.waitUntil(caches.open(APP_CACHE).then(function (cache) {
return Promise.all(
requests.map(function (request) {
return fetch(request).then(function (response) {
@ -178,8 +189,8 @@ self.addEventListener("install", function (event) {
});
})
);
})
);
}));
}
});
// Allow sw to control current page
@ -211,7 +222,7 @@ let fetchCaptureEnabled = false;
*/
self.addEventListener('fetch', function (event) {
// Only cache GET requests
if (event.request.method !== "GET") return;
if (event.request.method !== 'GET') return;
var rqUrl = event.request.url;
var urlObject = new URL(rqUrl);
// Test the URL with parameters removed
@ -231,7 +242,7 @@ self.addEventListener('fetch', function (event) {
// The response was not found in the cache so we look for it in the ZIM
// and add it to the cache if it is an asset type (css or js)
if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(strippedUrl)) {
let range = event.request.headers.get('range');
const range = event.request.headers.get('range');
return fetchUrlFromZIM(urlObject, range).then(function (response) {
// Add css or js assets to ASSETS_CACHE (or update their cache entries) unless the URL schema is not supported
if (regexpCachedContentTypes.test(response.headers.get('Content-Type')) &&
@ -252,7 +263,7 @@ self.addEventListener('fetch', function (event) {
}
return response;
}).catch(function (error) {
console.debug("[SW] Network request failed and no cache.", error);
console.debug('[SW] Network request failed and no cache.', error);
});
}
})
@ -287,7 +298,7 @@ self.addEventListener('fetch', function (event) {
if (useAppCache !== oldValue) console.debug('[SW] Use of appCache was switched to: ' + useAppCache);
}
if (event.data.action === 'getCacheNames') {
event.ports[0].postMessage({ 'app': APP_CACHE, 'assets': ASSETS_CACHE });
event.ports[0].postMessage({ app: APP_CACHE, assets: ASSETS_CACHE });
}
if (event.data.action.checkCache) {
// Checks and returns the caching strategy: checkCache key should contain a sample URL string to test
@ -347,9 +358,9 @@ function fetchUrlFromZIM(urlObject, range) {
// So it's probably better to send all we have: hopefully it will avoid some subsequent requests of
// the browser to get the following chunks (which would trigger some other complete reads in the ZIM file)
// This might be improved in the future with the libzim wasm backend, that should be able to handle ranges.
let partsOfRangeHeader = regexpByteRangeHeader.exec(range);
let begin = partsOfRangeHeader[1];
let end = contentLength - 1;
const partsOfRangeHeader = regexpByteRangeHeader.exec(range);
const begin = partsOfRangeHeader[1];
const end = contentLength - 1;
slicedData = slicedData.slice(begin);
headers.set('Content-Range', 'bytes ' + begin + '-' + end + '/' + contentLength);
@ -360,7 +371,7 @@ function fetchUrlFromZIM(urlObject, range) {
// HTTP status is usually 200, but has to bee 206 when partial content (range) is sent
status: range ? 206 : 200,
statusText: 'OK',
headers: headers
headers
};
var httpResponse = new Response(slicedData, responseInit);
@ -374,8 +385,8 @@ function fetchUrlFromZIM(urlObject, range) {
}
};
outgoingMessagePort.postMessage({
'action': 'askForContent',
'title': titleWithNameSpace
action: 'askForContent',
title: titleWithNameSpace
}, [messageChannel.port2]);
});
}
@ -388,10 +399,14 @@ function fetchUrlFromZIM(urlObject, range) {
*/
function fromCache (cache, requestUrl) {
// Prevents use of Cache API if user has disabled it
if (!(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) return Promise.reject('disabled');
if (!(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) {
return Promise.reject(new Error('disabled'));
}
return caches.open(cache).then(function (cacheObj) {
return cacheObj.match(requestUrl).then(function (matching) {
if (!matching || matching.status === 404) return Promise.reject('no-match');
if (!matching || matching.status === 404) {
return Promise.reject(new Error('no-match'));
}
console.debug('[SW] Supplying ' + requestUrl + ' from ' + cache + '...');
return matching;
});
@ -407,8 +422,9 @@ function fromCache(cache, requestUrl) {
*/
function updateCache (cache, request, response) {
// Prevents use of Cache API if user has disabled it
if (!response.ok || !(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE))
if (!response.ok || !(useAppCache && cache === APP_CACHE || useAssetsCache && cache === ASSETS_CACHE)) {
return Promise.resolve();
}
return caches.open(cache).then(function (cacheObj) {
console.debug('[SW] Adding ' + (request.url || request) + ' to ' + cache + '...');
return cacheObj.put(request, response);

View File

@ -32,7 +32,6 @@
define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore', 'abstractFilesystemAccess'],
function ($, zimArchiveLoader, uiUtil, settingsStore, abstractFilesystemAccess) {
/**
* The delay (in milliseconds) between two "keepalive" messages sent to the ServiceWorker (so that it is not stopped
* by the browser, and keeps the MessageChannel to communicate with the application)
@ -108,7 +107,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
(isServiceWorkerAvailable() ? 'serviceworker' : 'jquery');
// A parameter to circumvent anti-fingerprinting technology in browsers that do not support WebP natively by substituting images
// directly with the canvas elements produced by the WebP polyfill [kiwix-js #835]. NB This is only currently used in jQuery mode.
params['useCanvasElementsForWebpTranscoding']; // Value is determined in uiUtil.determineCanvasElementsWorkaround(), called when setting the content injection mode
params['useCanvasElementsForWebpTranscoding'] = null; // Value is determined in uiUtil.determineCanvasElementsWorkaround(), called when setting the content injection mode
// An object to hold the current search and its state (allows cancellation of search across modules)
appstate['search'] = {
@ -182,11 +181,11 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
document.getElementById('bypassAppCacheCheck').checked = !params.appCache;
document.getElementById('appVersion').textContent = 'Kiwix ' + params.appVersion;
// We check here if we have to warn the user that we switched to ServiceWorkerMode
// This is only needed if the ServiceWorker mode is available, or we are in a Firefox Extension that supports Service Workers
// This is only needed if the ServiceWorker mode is available, or we are in an Extension that supports Service Workers
// outside of the extension environment, AND the user's settings are stuck on jQuery mode, AND the user has not already been
// alerted about the switch to ServiceWorker mode by default
if ((isServiceWorkerAvailable() || isMessageChannelAvailable() && /^moz-extension:/i.test(window.location.protocol))
&& params.contentInjectionMode === 'jquery' && !params.defaultModeChangeAlertDisplayed) {
if ((isServiceWorkerAvailable() || isMessageChannelAvailable() && /^(moz|chrome)-extension:/i.test(window.location.protocol)) &&
params.contentInjectionMode === 'jquery' && !params.defaultModeChangeAlertDisplayed) {
// Attempt to upgrade user to ServiceWorker mode
params.contentInjectionMode = 'serviceworker';
} else if (params.contentInjectionMode === 'serviceworker') {
@ -329,16 +328,12 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
$('#prefix').on('keyup', function (e) {
if (selectedArchive !== null && selectedArchive.isReady()) {
// Prevent processing by keyup event if we already handled the keypress in keydown event
if (keyPressHandled)
keyPressHandled = false;
else
onKeyUpPrefix(e);
if (keyPressHandled) { keyPressHandled = false; } else { onKeyUpPrefix(e); }
}
});
// Restore the search results if user goes back into prefix field
$('#prefix').on('focus', function () {
if (document.getElementById('prefix').value !== '')
document.getElementById('articleListWithHeader').style.display = '';
if (document.getElementById('prefix').value !== '') { document.getElementById('articleListWithHeader').style.display = ''; }
});
// Hide the search results if user moves out of prefix field
$('#prefix').on('blur', function () {
@ -505,7 +500,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
refreshCacheStatus();
});
document.getElementById('disableDragAndDropCheck').addEventListener('change', function () {
params.disableDragAndDrop = this.checked ? true : false;
params.disableDragAndDrop = !!this.checked;
settingsStore.setItem('disableDragAndDrop', params.disableDragAndDrop, Infinity);
uiUtil.systemAlert('<p>We will now attempt to reload the app to apply the new setting.</p>' +
'<p>(If you cancel, then the setting will only be applied when you next start the app.)</p>', 'Reload app', true).then(function (result) {
@ -515,20 +510,20 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
});
});
$('input:checkbox[name=hideActiveContentWarning]').on('change', function () {
params.hideActiveContentWarning = this.checked ? true : false;
params.hideActiveContentWarning = !!this.checked;
settingsStore.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity);
});
$('input:checkbox[name=showUIAnimations]').on('change', function () {
params.showUIAnimations = this.checked ? true : false;
params.showUIAnimations = !!this.checked;
settingsStore.setItem('showUIAnimations', params.showUIAnimations, Infinity);
});
$('input:checkbox[name=useHomeKeyToFocusSearchBar]').on('change', function () {
params.useHomeKeyToFocusSearchBar = this.checked ? true : false;
params.useHomeKeyToFocusSearchBar = !!this.checked;
settingsStore.setItem('useHomeKeyToFocusSearchBar', params.useHomeKeyToFocusSearchBar, Infinity);
switchHomeKeyToFocusSearchBar();
});
$('input:checkbox[name=openExternalLinksInNewTabs]').on('change', function () {
params.openExternalLinksInNewTabs = this.checked ? true : false;
params.openExternalLinksInNewTabs = !!this.checked;
settingsStore.setItem('openExternalLinksInNewTabs', params.openExternalLinksInNewTabs, Infinity);
});
document.getElementById('appThemeSelect').addEventListener('change', function (e) {
@ -578,7 +573,6 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
uiUtil.checkUpdateStatus(appstate);
}, 10000);
// Adds an event listener to kiwix logo and bottom navigation bar which gets triggered when these elements are dragged.
// Returning false prevents their dragging (which can cause some unexpected behavior)
// Doing that in javascript is the only way to make it cross-browser compatible
@ -602,8 +596,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var isIframeAccessible = true;
try {
iframeContentWindow.removeEventListener('keydown', focusPrefixOnHomeKey);
}
catch (err) {
} catch (err) {
console.error('The iframe is probably not accessible', err);
isIframeAccessible = false;
}
@ -615,10 +608,8 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// only for initial empty iFrame loaded using `src` attribute
// in any other case listener gets removed on reloading of iFrame content
iframeContentWindow.addEventListener('keydown', focusPrefixOnHomeKey);
}
// when the feature is not active
else {
// remove event listener for window(outside iframe)
} else {
// When the feature is not active, remove event listener for window (outside iframe)
window.removeEventListener('keydown', focusPrefixOnHomeKey);
// if feature is deactivated and no zim content is loaded yet
iframeContentWindow.removeEventListener('keydown', focusPrefixOnHomeKey);
@ -859,7 +850,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
uriParams += '&appTheme=' + params.appTheme;
uriParams += '&showUIAnimations=' + params.showUIAnimations;
window.location.href = params.referrerExtensionURL + '/www/index.html' + uriParams;
'Beam me down, Scotty!';
console.log('Beam me down, Scotty!');
};
uiUtil.systemAlert(message, 'Warning!', true).then(function (response) {
if (response) {
@ -1018,8 +1009,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
try {
var dummyMessageChannel = new MessageChannel();
if (dummyMessageChannel) return true;
}
catch (e) {
} catch (e) {
return false;
}
return false;
@ -1039,8 +1029,9 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// DEV: See explanation below for why we access localStorage directly here
var PWASuccessfullyLaunched = localStorage.getItem(params.keyPrefix + 'PWA_launch') === 'success';
var allowInternetAccess = settingsStore.getItem('allowInternetAccess') === 'true';
var message = params.defaultModeChangeAlertDisplayed ? '<p>To enable the Service Worker, we ' :
('<p>We shall attempt to switch you to ServiceWorker mode (this is now the default). ' +
var message = params.defaultModeChangeAlertDisplayed
? '<p>To enable the Service Worker, we '
: ('<p>We shall attempt to switch you to ServiceWorker mode (this is now the default). ' +
'It supports more types of ZIM archives and is much more robust.</p><p>We ');
message += 'need one-time access to our secure server so that the app can re-launch as a Progressive Web App (PWA). ' +
'If available, the PWA will work offline, but will auto-update periodically when online as per the ' +
@ -1063,7 +1054,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// regarding the location of the key to be able to retrieve it in init.js before settingsStore is initialized
localStorage.setItem(params.keyPrefix + 'PWA_launch', 'fail');
window.location.href = params.PWAServer + 'www/index.html' + uriParams;
'Beam me up, Scotty!';
console.log('Beam me up, Scotty!');
};
var checkPWAIsOnline = function () {
uiUtil.spinnerDisplay(true, 'Checking server access...');
@ -1169,7 +1160,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
document.getElementById('articleListWithHeader').style.display = 'none';
$('#articleContent').contents().empty();
if (title && !(''===title)) {
if (title && !(title === '')) {
goToArticle(title);
} else if (titleSearch && titleSearch !== '') {
document.getElementById('prefix').value = titleSearch;
@ -1255,9 +1246,9 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
if (storages.length === 1) {
selectedStorage = storages[0];
} else {
uiUtil.systemAlert('Something weird happened with the DeviceStorage API : found a directory without prefix : '
+ archiveDirectory + ', but there were ' + storages.length
+ ' storages found with getDeviceStorages instead of 1', 'Error: unprefixed directory');
uiUtil.systemAlert('Something weird happened with the DeviceStorage API : found a directory without prefix : ' +
archiveDirectory + ', but there were ' + storages.length +
' storages found with getDeviceStorages instead of 1', 'Error: unprefixed directory');
}
}
resetCssCache();
@ -1269,7 +1260,6 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
// callbackError which is called in case of an error
uiUtil.systemAlert(message, label);
});
}
}
@ -1321,7 +1311,6 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
function handleIframeDrop (e) {
e.stopPropagation();
e.preventDefault();
}
function handleFileDrop (packet) {
@ -1390,7 +1379,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
request.response.name = url;
resolve(request.response);
} else {
reject('HTTP status ' + request.status + ' when reading ' + url);
reject(new Error('HTTP status ' + request.status + ' when reading ' + url));
}
}
};
@ -1444,7 +1433,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
appstate.search.status = 'cancelled';
// Initiate a new search object and point appstate.search to it (the zimArchive search object will continue to point to the old object)
// DEV: Technical explanation: the appstate.search is a pointer to an underlying object assigned in memory, and we are here defining a new object
// in memory {'prefix': prefix, 'status': 'init', .....}, and pointing appstate.search to it; the old search object that was passed to selectedArchive
// in memory {prefix: prefix, status: 'init', .....}, and pointing appstate.search to it; the old search object that was passed to selectedArchive
// (zimArchive.js) continues to exist in the scope of the functions initiated by the previous search until all Promises have returned
appstate.search = { prefix: prefix, status: 'init', type: '', size: params.maxSearchResultsSize };
var activeContent = document.getElementById('activeContent');
@ -1479,7 +1468,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
} else if (nbDirEntry >= params.maxSearchResultsSize) {
message = 'First ' + params.maxSearchResultsSize + ' articles found (refine your search).';
} else {
message = 'Finished. ' + (nbDirEntry ? nbDirEntry : 'No') + ' articles found' + (
message = 'Finished. ' + (nbDirEntry || 'No') + ' articles found' + (
reportingSearch.type === 'basic' ? ': try fewer words for full search.' : '.'
);
}
@ -1608,8 +1597,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
if (iframeArticleContent.contentWindow) {
// Configure home key press to focus #prefix only if the feature is in active state
if (params.useHomeKeyToFocusSearchBar)
iframeArticleContent.contentWindow.addEventListener('keydown', focusPrefixOnHomeKey);
if (params.useHomeKeyToFocusSearchBar) { iframeArticleContent.contentWindow.addEventListener('keydown', focusPrefixOnHomeKey); }
if (params.openExternalLinksInNewTabs) {
// Add event listener to iframe window to check for links to external resources
iframeArticleContent.contentWindow.addEventListener('click', function (event) {
@ -1800,10 +1788,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var iframeContentDocument = iframeArticleContent.contentDocument;
if (!iframeContentDocument && window.location.protocol === 'file:') {
uiUtil.systemAlert('You seem to be opening kiwix-js with the file:// protocol, which is blocked by your browser for security reasons.'
+ '<br/><br/>The easiest way to run it is to download and run it as a browser extension (from the vendor store).'
+ '<br/><br/>Else you can open it through a web server : either through a local one (http://localhost/...) or through a remote one (but you need SSL : https://webserver/...)'
+ "<br/><br/>Another option is to force your browser to accept that (but you'll open a security breach) : on Chrome, you can start it with --allow-file-access-from-files command-line argument; on Firefox, you can set privacy.file_unique_origin to false in about:config");
uiUtil.systemAlert('You seem to be opening kiwix-js with the file:// protocol, which is blocked by your browser for security reasons.' +
'<br/><br/>The easiest way to run it is to download and run it as a browser extension (from the vendor store).' +
'<br/><br/>Else you can open it through a web server : either through a local one (http://localhost/...) or through a remote one (but you need SSL : https://webserver/...)' +
"<br/><br/>Another option is to force your browser to accept that (but you'll open a security breach) : on Chrome, you can start it with --allow-file-access-from-files command-line argument; on Firefox, you can set privacy.file_unique_origin to false in about:config");
return;
}
@ -1816,9 +1804,11 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
docBody = docBody ? docBody[0] : null;
if (docBody) {
// Add any missing classes stripped from the <html> tag
if (htmlCSS) htmlCSS.forEach(function (cl) {
if (htmlCSS) {
htmlCSS.forEach(function (cl) {
docBody.classList.add(cl);
});
}
// Deflect drag-and-drop of ZIM file on the iframe to Config
docBody.addEventListener('dragover', handleIframeDragover);
docBody.addEventListener('drop', handleIframeDrop);
@ -1846,8 +1836,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
}
if (iframeArticleContent.contentWindow) {
// Configure home key press to focus #prefix only if the feature is in active state
if (params.useHomeKeyToFocusSearchBar)
iframeArticleContent.contentWindow.addEventListener('keydown', focusPrefixOnHomeKey);
if (params.useHomeKeyToFocusSearchBar) { iframeArticleContent.contentWindow.addEventListener('keydown', focusPrefixOnHomeKey); }
// when unloaded remove eventListener to avoid memory leaks
iframeArticleContent.contentWindow.onunload = function () {
iframeArticleContent.contentWindow.removeEventListener('keydown', focusPrefixOnHomeKey);
@ -1998,7 +1987,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
selectedArchive.getDirEntryByPath(url).then(function (dirEntry) {
if (!dirEntry) {
cssCache.set(url, ''); // Prevent repeated lookups of this unfindable asset
throw 'DirEntry ' + typeof dirEntry;
throw new Error('DirEntry ' + typeof dirEntry);
}
var mimetype = dirEntry.getMimetype();
var readFile = /^text\//i.test(mimetype) ? selectedArchive.readUtf8File : selectedArchive.readBinaryFile;
@ -2069,7 +2058,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var source = mediaSource.getAttribute('src');
source = source ? uiUtil.deriveZimUrlFromRelativeUrl(source, baseUrl) : null;
// We have to exempt text tracks from using deriveZimUrlFromRelativeurl due to a bug in Firefox [kiwix-js #496]
source = source ? source : decodeURIComponent(mediaSource.dataset.kiwixurl);
source = source || decodeURIComponent(mediaSource.dataset.kiwixurl);
if (!source || !regexpZIMUrlWithNamespace.test(source)) {
if (source) console.error('No usable media source was found for: ' + source);
return;
@ -2115,13 +2104,13 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
var stateObj = {};
var urlParameters;
var stateLabel;
if (title && !(''===title)) {
if (title && !(title === '')) {
// Prevents creating a double history for the same page
if (history.state && history.state.title === title) return;
stateObj.title = title;
urlParameters = '?title=' + title;
stateLabel = 'Wikipedia Article : ' + title;
} else if (titleSearch && !(''===titleSearch)) {
} else if (titleSearch && !(titleSearch === '')) {
stateObj.titleSearch = titleSearch;
urlParameters = '?titleSearch=' + titleSearch;
stateLabel = 'Wikipedia search : ' + titleSearch;
@ -2131,7 +2120,6 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
window.history.pushState(stateObj, stateLabel, urlParameters);
}
/**
* Extracts the content of the given article pathname, or a downloadable file, from the ZIM
*
@ -2226,5 +2214,4 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'settingsStore','abstractFilesys
}
});
}
});

View File

@ -19,8 +19,12 @@
* You should have received a copy of the GNU General Public License
* along with Kiwix (file LICENSE-GPLv3.txt). If not, see <http://www.gnu.org/licenses/>
*/
'use strict';
/* eslint-disable no-global-assign */
/* global $, define, webpMachine, webpHero, params */
// DEV: Put your RequireJS definition in the rqDef array below, and any function exports in the function parenthesis of the define statement
// We need to do it this way in order to load WebP polyfills conditionally. The WebP polyfills are only needed by a few old browsers, so loading them
// only if needed saves approximately 1MB of memory.
@ -32,7 +36,6 @@ if (webpMachine) {
}
define(rqDef, function (settingsStore, util) {
/**
* Displays a Bootstrap alert or confirm dialog box depending on the options provided
*
@ -51,7 +54,7 @@ define(rqDef, function(settingsStore, util) {
label = label || (isConfirm ? 'Confirmation' : 'Message');
return util.PromiseQueue.enqueue(function () {
return new Promise(function (resolve, reject) {
if (!message) reject('Missing body message');
if (!message) reject(new Error('Missing body message'));
// Set the text to the modal and its buttons
document.getElementById('approveConfirm').textContent = approveConfirmLabel;
document.getElementById('declineConfirm').textContent = declineConfirmLabel;
@ -339,20 +342,22 @@ define(rqDef, function(settingsStore, util) {
function displayFileDownloadAlert (title, download, contentType, content) {
var downloadAlert = document.getElementById('downloadAlert');
downloadAlert.style.display = 'block';
if (!downloadAlertSetup) downloadAlert.querySelector('button[data-hide]').addEventListener('click', function() {
if (!downloadAlertSetup) {
downloadAlert.querySelector('button[data-hide]').addEventListener('click', function () {
// We are setting up the alert for the first time
downloadAlert.style.display = 'none';
});
}
downloadAlertSetup = true;
// Download code adapted from https://stackoverflow.com/a/19230668/9727685
// Set default contentType if none was provided
if (!contentType) contentType = 'application/octet-stream';
var a = document.createElement('a');
var blob = new Blob([content], { 'type': contentType });
var blob = new Blob([content], { type: contentType });
// If the filename to use for saving has not been specified, construct it from title
var filename = download === true ? title.replace(/^.*\/([^\/]+)$/, '$1') : download;
var filename = download === true ? title.replace(/^.*\/([^/]+)$/, '$1') : download;
// Make filename safe
filename = filename.replace(/[\/\\:*?"<>|]/g, '_');
filename = filename.replace(/[/\\:*?"<>|]/g, '_');
a.href = window.URL.createObjectURL(blob);
a.target = '_blank';
a.type = contentType;
@ -364,8 +369,9 @@ define(rqDef, function(settingsStore, util) {
alertMessage.innerHTML = '<strong>Download</strong> If the download does not start, please tap the following link: ';
// We have to add the anchor to a UI element for Firefox to be able to click it programmatically: see https://stackoverflow.com/a/27280611/9727685
alertMessage.appendChild(a);
try { a.click(); }
catch (err) {
try {
a.click();
} catch (err) {
// If the click fails, user may be able to download by manually clicking the link
// But for IE11 we need to force use of the saveBlob method with the onclick event
if (window.navigator && window.navigator.msSaveBlob) {
@ -406,9 +412,11 @@ define(rqDef, function(settingsStore, util) {
});
}
}
if (updateAlert) updateAlert.querySelector('button[data-hide]').addEventListener('click', function () {
if (updateAlert) {
updateAlert.querySelector('button[data-hide]').addEventListener('click', function () {
updateAlert.style.display = 'none';
});
}
/**
* Checks if a server is accessible by attempting to load a test image from the server
@ -453,11 +461,12 @@ define(rqDef, function(settingsStore, util) {
*/
function isElementInView (el, fully) {
var rect = el.getBoundingClientRect();
if (fully)
if (fully) {
return rect.top > 0 && rect.bottom < window.innerHeight && rect.left > 0 && rect.right < window.innerWidth;
else
} else {
return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
}
}
/**
* Removes the animation effect between various sections
@ -481,7 +490,7 @@ define(rqDef, function(settingsStore, util) {
*
*/
function applyAnimationToSection (section) {
if (section == 'home') {
if (section === 'home') {
if (!$('#configuration').is(':hidden')) {
document.getElementById('configuration').classList.add('slideOut_R');
setTimeout(function () {
@ -498,7 +507,7 @@ define(rqDef, function(settingsStore, util) {
setTimeout(function () {
document.getElementById('articleContent').style.display = '';
}, 300);
} else if (section == 'config') {
} else if (section === 'config') {
if (!$('#about').is(':hidden')) {
$('#about').addClass('slideOut_R');
$('#configuration').addClass('slideIn_R');
@ -515,7 +524,7 @@ define(rqDef, function(settingsStore, util) {
setTimeout(function () {
document.getElementById('configuration').style.display = '';
}, 300);
} else if (section == 'about') {
} else if (section === 'about') {
if (!$('#configuration').is(':hidden')) {
document.getElementById('configuration').classList.add('slideOut_L');
setTimeout(function () {
@ -613,7 +622,7 @@ define(rqDef, function(settingsStore, util) {
// If we are in Config and a real document has been loaded already, expose return link so user can see the result of the change
// DEV: The Placeholder string below matches the dummy article.html that is loaded before any articles are loaded
if (document.getElementById('liConfigureNav').classList.contains('active') && doc &&
doc.title !== "Placeholder for injecting an article into the iframe") {
doc.title !== 'Placeholder for injecting an article into the iframe') {
showReturnLink();
}
}
@ -655,8 +664,8 @@ define(rqDef, function(settingsStore, util) {
function reportSearchProviderToAPIStatusPanel (provider) {
var providerAPI = document.getElementById('searchProviderStatus');
if (providerAPI) { // NB we need this so that tests don't fail
providerAPI.textContent = 'Search Provider: ' + (/^fulltext/.test(provider) ? 'Title + Xapian [' + provider + ']' :
/^title/.test(provider) ? 'Title only [' + provider + ']' : 'Not initialized');
providerAPI.textContent = 'Search Provider: ' + (/^fulltext/.test(provider) ? 'Title + Xapian [' + provider + ']'
: /^title/.test(provider) ? 'Title only [' + provider + ']' : 'Not initialized');
providerAPI.className = /^fulltext/.test(provider) ? 'apiAvailable' : !/ERROR/.test(provider) ? 'apiUnavailable' : 'apiBroken';
}
}
@ -682,8 +691,9 @@ define(rqDef, function(settingsStore, util) {
message += '</p><p style="word-break:break-all;">' + clickedAnchor.href + '</p>';
systemAlert(message, 'Opening external link', true).then(function (response) {
if (response) {
if (!target)
if (!target) {
target = '_blank';
}
window.open(clickedAnchor.href, target);
}
});