mirror of
https://github.com/kiwix/kiwix-js-pwa.git
synced 2025-09-17 16:29:42 -04:00
Use flexible, adaptive caching and split appcache and assetscache
Former-commit-id: 90733545b8784ebbd6ae74a43e13349762b1d587 [formerly 1fb5fd48e1c084c0813ad47d0771204a52c9770a] [formerly 0361a9fa05d4e8063a150172a3c6adcfef946b56] [formerly d8d77598de0b001f5fc60bc58eccb8eb673a609b [formerly 49a5574bcac4fe4a2f945b3f1f9e8c8fb946ea8e [formerly 5d24bdba777c0cc6e8f2a1857e82d3fb9282378b]]] Former-commit-id: 14ac63d892cfc8c7df292cdc2fa26c0a74d191f0 [formerly 0bace1751b875550d897ca1368b396b4f6ccae61 [formerly 0a77fb56d2811e33ebf81fd2ec9cb1ade54f2c8b]] Former-commit-id: 2078b106016fe16cec442278a464bf555d0ef902 [formerly f3bb117918e9f4577a4e73e7973e7dc70ee5d9dd] Former-commit-id: 5c26226eb043c8a0b475904fe99fdfcaee3d0e16
This commit is contained in:
parent
f39e3f2975
commit
d9ec8c20c3
@ -3,7 +3,7 @@ FROM nginx:latest
|
||||
EXPOSE 80
|
||||
|
||||
COPY ./manifest.json /usr/share/nginx/html
|
||||
COPY ./pwabuilder-sw.js /usr/share/nginx/html
|
||||
COPY ./service-worker.js /usr/share/nginx/html
|
||||
COPY index.html /usr/share/nginx/html
|
||||
COPY CHANGELOG.md /usr/share/nginx/html
|
||||
COPY LICENSE /usr/share/nginx/html
|
||||
|
@ -174,7 +174,7 @@
|
||||
<None Include="Package.StoreAssociation.xml" />
|
||||
<None Include="KiwixWebApp_StoreKey.pfx" />
|
||||
<Content Include="manifest.json" />
|
||||
<Content Include="pwabuilder-sw.js" />
|
||||
<Content Include="service-worker.js" />
|
||||
<Content Include="www\-\static\main.css" />
|
||||
<Content Include="www\-\s\css_modules\content.parsoid.css" />
|
||||
<Content Include="www\-\s\css_modules\ext.cite.a11y.css" />
|
||||
|
@ -54,7 +54,7 @@
|
||||
"asar": false,
|
||||
"files": [
|
||||
"archives/**",
|
||||
"pwabuilder-sw.js",
|
||||
"service-worker.js",
|
||||
"index.html",
|
||||
"CHANGELOG.md",
|
||||
"LICENCE",
|
||||
|
@ -22,7 +22,7 @@
|
||||
"nwVersion": "0.14.7",
|
||||
"output": "bld/nwjs/win-x86-xp",
|
||||
"files": [
|
||||
"pwabuilder-sw.js",
|
||||
"service-worker.js",
|
||||
"index.html",
|
||||
"CHANGELOG.md",
|
||||
"LICENCE",
|
||||
@ -40,7 +40,7 @@
|
||||
"output": "bld/nwjs/win-x64",
|
||||
"files": [
|
||||
"archives/**",
|
||||
"pwabuilder-sw.js",
|
||||
"service-worker.js",
|
||||
"index.html",
|
||||
"CHANGELOG.md",
|
||||
"LICENCE",
|
||||
@ -54,7 +54,7 @@
|
||||
"nwVersion": "0.58.0",
|
||||
"output": "bld/nwjs/win-x86",
|
||||
"files": [
|
||||
"pwabuilder-sw.js",
|
||||
"service-worker.js",
|
||||
"index.html",
|
||||
"CHANGELOG.md",
|
||||
"LICENCE",
|
||||
|
354
pwabuilder-sw.js
354
pwabuilder-sw.js
@ -1,348 +1,14 @@
|
||||
// Service Worker with Cache-first network, with some code from pwabuilder.com
|
||||
'use strict';
|
||||
|
||||
// App version number - ENSURE IT MATCHES VALUE IN init.js
|
||||
// DEV: Changing this will cause the browser to recognize that the Service Worker has changed, and it will download and
|
||||
// install a new copy
|
||||
const appVersion = '1.8.5';
|
||||
|
||||
// Kiwix ZIM Archive Download Server in regex form
|
||||
// DEV: The server URL is defined in init.js, but is not available to us in SW
|
||||
const regexpKiwixDownloadLinks = /download\.kiwix\.org/i;
|
||||
|
||||
// Pattern for ZIM file namespace - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces
|
||||
// In our case, there is also the ZIM file name, used as a prefix in the URL
|
||||
const regexpZIMUrlWithNamespace = /(?:^|\/)([^\/]+\/)([-ABCIJMUVWX])\/(.+)/;
|
||||
|
||||
const CACHE = "kiwix-precache-" + appVersion;
|
||||
let precacheFiles = [
|
||||
".",
|
||||
"manifest.json",
|
||||
"pwabuilder-sw.js",
|
||||
"www/-/mw/ext.cite.styles.css",
|
||||
"www/-/mw/ext.cite.ux-enhancements.css",
|
||||
"www/-/mw/ext.math.scripts.css",
|
||||
"www/-/mw/ext.math.styles.css",
|
||||
"www/-/mw/ext.kartographer.frame.css",
|
||||
"www/-/mw/ext.kartographer.link.css",
|
||||
"www/-/mw/ext.kartographer.style.css",
|
||||
"www/-/mw/ext.scribunto.logs.css",
|
||||
"www/-/mw/ext.tmh.thumbnail.styles.css",
|
||||
"www/-/mw/inserted_style.css",
|
||||
"www/-/mw/inserted_style_mobile.css",
|
||||
"www/-/mw/mediawiki.page.gallery.styles.css",
|
||||
"www/-/mw/mobile.css",
|
||||
"www/-/mw/mw.MediaWikiPlayer.loader.css",
|
||||
"www/-/mw/mw.PopUpMediaTransform.css",
|
||||
"www/-/mw/mw.TMHGalleryHook.js.css",
|
||||
"www/-/mw/style.css",
|
||||
"www/-/s/css_modules/content.parsoid.css",
|
||||
"www/-/s/css_modules/ext.cite.a11y.css",
|
||||
"www/-/s/css_modules/ext.cite.styles.css",
|
||||
"www/-/s/css_modules/ext.cite.ux-enhancements.css",
|
||||
"www/-/s/css_modules/ext.inputBox.styles.css",
|
||||
"www/-/s/css_modules/ext.kartographer.frame.css",
|
||||
"www/-/s/css_modules/ext.kartographer.link.css",
|
||||
"www/-/s/css_modules/ext.kartographer.style.css",
|
||||
"www/-/s/css_modules/inserted_style.css",
|
||||
"www/-/s/css_modules/inserted_style_mobile.css",
|
||||
"www/-/s/css_modules/mobile.css",
|
||||
"www/-/s/css_modules/style.css",
|
||||
"www/-/style.css",
|
||||
"www/-/s/style.css",
|
||||
"www/-/s/style-dark.css",
|
||||
"www/-/s/style-dark-invert.css",
|
||||
"www/-/s/style-mobile.css",
|
||||
"www/-/s/vector.css",
|
||||
"www/I/COVID-19_lifecycle.jpg",
|
||||
"www/I/s/Icon_External_Link.png",
|
||||
"www/I/s/Icons-mini-file_acrobat.gif",
|
||||
"www/css/app.css",
|
||||
"www/css/bootstrap.min.css",
|
||||
"www/fonts/glyphicons-halflings-regular.woff2",
|
||||
"www/img/icons/kiwix-256.png",
|
||||
"www/img/icons/kiwix-192.png",
|
||||
"www/img/icons/kiwix-32.png",
|
||||
"www/img/icons/kiwix-60.png",
|
||||
"www/img/icons/kiwix-blue-32.png",
|
||||
"www/img/icons/kiwix-midnightblue-90.png",
|
||||
"www/img/icons/wikimed-blue-32.png",
|
||||
"www/img/icons/wikimed-lightblue-32.png",
|
||||
"www/img/icons/wikivoyage-90-white.png",
|
||||
"www/img/icons/wikivoyage-black-32.png",
|
||||
"www/img/icons/wikivoyage-white-32.png",
|
||||
"www/img/icons/map_marker-30px.png",
|
||||
"www/img/icons/map_marker-18px.png",
|
||||
"www/img/spinner.gif",
|
||||
"www/index.html",
|
||||
"www/article.html",
|
||||
"www/js/app.js",
|
||||
"www/js/init.js",
|
||||
"www/js/lib/arrayFromPolyfill.js",
|
||||
"www/js/lib/bootstrap.js",
|
||||
"www/js/lib/bootstrap.min.js",
|
||||
"www/js/lib/cache.js",
|
||||
"www/js/lib/filecache.js",
|
||||
"www/js/lib/images.js",
|
||||
"www/js/lib/jquery-3.2.1.slim.js",
|
||||
"www/js/lib/kiwixServe.js",
|
||||
"www/js/lib/promisePolyfill.js",
|
||||
"www/js/lib/require.js",
|
||||
"www/js/lib/settingsStore.js",
|
||||
"www/js/lib/transformStyles.js",
|
||||
"www/js/lib/uiUtil.js",
|
||||
"www/js/lib/utf8.js",
|
||||
"www/js/lib/util.js",
|
||||
//"www/js/lib/webpHeroBundle_0.0.0-dev.27.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/katex/katex.min.js",
|
||||
"www/js/katex/katex.min.css",
|
||||
"www/js/katex/contrib/mathtex-script-type.min.js",
|
||||
"www/js/katex/fonts/KaTeX_AMS-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Main-Bold.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Main-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Math-Italic.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Size1-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Size2-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Size3-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Size4-Regular.woff2"
|
||||
];
|
||||
|
||||
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"
|
||||
);
|
||||
} else {
|
||||
precacheFiles.push(
|
||||
"www/js/lib/xzdec-asm.js",
|
||||
"www/js/lib/zstddec-asm.js"
|
||||
);
|
||||
}
|
||||
|
||||
// DEV: add any URL schemata that should be excluded from caching with the Cache API to the regex below
|
||||
// As of 08-2019 the chrome-extension: schema is incompatible with the Cache API
|
||||
// 'example-extension' is included to show how to add another schema if necessary
|
||||
var excludedURLSchema = /^(?:file|chrome-extension|example-extension):/i;
|
||||
|
||||
self.addEventListener("install", function (event) {
|
||||
console.log("[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();
|
||||
var requests = precacheFiles.map(function(url) {
|
||||
return new Request(url + '?v' + appVersion, { cache: 'no-cache' });
|
||||
});
|
||||
if (!excludedURLSchema.test(requests[0].url)) event.waitUntil(
|
||||
caches.open(CACHE).then(function (cache) {
|
||||
return Promise.all(
|
||||
requests.map(function (request) {
|
||||
return fetch(request).then(function (response) {
|
||||
// Fail on 404, 500 etc
|
||||
if (!response.ok) throw Error('Could not fetch ' + request.url);
|
||||
return cache.put(request.url.replace(/\?v[^?/]+$/, ''), response);
|
||||
}).catch(function (err) {
|
||||
console.error("There was an error pre-caching files", err);
|
||||
// Self-destroying service-worker - see https://github.com/NekR/self-destroying-sw
|
||||
self.addEventListener('install', function(e) {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function(e) {
|
||||
self.registration.unregister()
|
||||
.then(function() {
|
||||
return self.clients.matchAll();
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Allow sw to control current page
|
||||
self.addEventListener('activate', function (event) {
|
||||
console.log("[SW] Claiming clients for current page");
|
||||
event.waitUntil(
|
||||
caches.keys().then(function (keyList) {
|
||||
return Promise.all(keyList.map(function (key) {
|
||||
console.log('[SW] Current cache key is ' + key);
|
||||
if (key !== CACHE) {
|
||||
console.log("[SW] App updated to version " + appVersion + ": deleting old cache")
|
||||
return caches.delete(key);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* A Boolean that governs whether images are displayed
|
||||
* app.js can alter this variable via messaging
|
||||
*/
|
||||
let imageDisplay;
|
||||
|
||||
let outgoingMessagePort = null;
|
||||
let fetchCaptureEnabled = false;
|
||||
|
||||
/**
|
||||
* Handle custom commands 'init' and 'disable' from app.js
|
||||
*/
|
||||
self.addEventListener('message', function (event) {
|
||||
if (event.data.action === 'init') {
|
||||
// On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener
|
||||
outgoingMessagePort = event.ports[0];
|
||||
fetchCaptureEnabled = true;
|
||||
}
|
||||
if (event.data.action === 'disable') {
|
||||
// On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener
|
||||
outgoingMessagePort = null;
|
||||
fetchCaptureEnabled = false;
|
||||
self.removeEventListener('fetch', intercept);
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', intercept);
|
||||
|
||||
// Look up fetch in cache, and if it does not exist, try to get it from the network
|
||||
function intercept(event) {
|
||||
// Test if we're in an Electron app
|
||||
// DEV: Electron uses the file:// protocol and hacks it to work with SW, but it has CORS issues when using the Fetch API to fetch local files,
|
||||
// so we must bypass it here if we're fetching a local file
|
||||
if (/^file:/i.test(event.request.url) && ! (regexpZIMUrlWithNamespace.test(event.request.url) && /\.zim\w{0,2}\//i.test(event.request.url))) return;
|
||||
// console.debug('[SW] Service Worker ' + (event.request.method === "GET" ? 'intercepted ' : 'noted ') + event.request.url, event.request.method);
|
||||
if (event.request.method !== "GET") return;
|
||||
// Don't cache download links
|
||||
if (regexpKiwixDownloadLinks.test(event.request.url)) return;
|
||||
// Remove any querystring except 'kiwix-display'
|
||||
var rqUrl = event.request.url.replace(/\?(?!kiwix-display)[^?]+$/i, '');
|
||||
event.respondWith(
|
||||
fromCache(rqUrl).then(function (response) {
|
||||
console.debug('[SW] Supplying ' + rqUrl + ' from CACHE...');
|
||||
return response;
|
||||
},
|
||||
function () {
|
||||
// The response was not found in the cache so we look for it on the server
|
||||
if (/\.zim\w{0,2}\//i.test(rqUrl) && regexpZIMUrlWithNamespace.test(rqUrl)) {
|
||||
if (imageDisplay !== 'all' && /(^|\/)[IJ]\/.*\.(jpe?g|png|svg|gif|webp)($|[?#])(?!kiwix-display)/i.test(rqUrl)) {
|
||||
// If the user has disabled the display of images, and the browser wants an image, respond with empty SVG
|
||||
// A URL with "?kiwix-display" query string acts as a passthrough so that the regex will not match and
|
||||
// the image will be fetched by app.js
|
||||
// DEV: If you need to hide more image types, add them to regex below and also edit equivalent regex in app.js
|
||||
var svgResponse;
|
||||
if (imageDisplay === 'manual')
|
||||
svgResponse = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><rect width='1' height='1' style='fill:lightblue'/></svg>";
|
||||
else
|
||||
svgResponse = "<svg xmlns='http://www.w3.org/2000/svg'/>";
|
||||
return new Response(svgResponse, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Let's ask app.js for that content
|
||||
return new Promise(function (resolve, reject) {
|
||||
var nameSpace;
|
||||
var title;
|
||||
var titleWithNameSpace;
|
||||
var regexpResult = regexpZIMUrlWithNamespace.exec(rqUrl);
|
||||
var prefix = regexpResult[1];
|
||||
nameSpace = regexpResult[2];
|
||||
title = regexpResult[3];
|
||||
|
||||
// We need to remove the potential parameters in the URL
|
||||
title = removeUrlParameters(decodeURIComponent(title));
|
||||
|
||||
titleWithNameSpace = nameSpace + '/' + title;
|
||||
|
||||
// Let's instantiate a new messageChannel, to allow app.js to give us the content
|
||||
var messageChannel = new MessageChannel();
|
||||
messageChannel.port1.onmessage = function (msgEvent) {
|
||||
if (msgEvent.data.action === 'giveContent') {
|
||||
// Content received from app.js
|
||||
var contentLength = msgEvent.data.content ? msgEvent.data.content.byteLength : null;
|
||||
var contentType = msgEvent.data.mimetype;
|
||||
// Set the imageDisplay variable if it has been sent in the event data
|
||||
imageDisplay = typeof msgEvent.data.imageDisplay !== 'undefined' ?
|
||||
msgEvent.data.imageDisplay : imageDisplay;
|
||||
var headers = new Headers();
|
||||
if (contentLength) headers.set('Content-Length', contentLength);
|
||||
// Prevent CORS issues in PWAs
|
||||
if (contentLength) headers.set('Access-Control-Allow-Origin', '*');
|
||||
if (contentType) headers.set('Content-Type', contentType);
|
||||
// Test if the content is a video or audio file
|
||||
// See kiwix-js #519 and openzim/zimwriterfs #113 for why we test for invalid types like "mp4" or "webm" (without "video/")
|
||||
// The full list of types produced by zimwriterfs is in https://github.com/openzim/zimwriterfs/blob/master/src/tools.cpp
|
||||
if (contentLength >= 1 && /^(video|audio)|(^|\/)(mp4|webm|og[gmv]|mpeg)$/i.test(contentType)) {
|
||||
// In case of a video (at least), Chrome and Edge need these HTTP headers else seeking doesn't work
|
||||
// (even if we always send all the video content, not the requested range, until the backend supports it)
|
||||
headers.set('Accept-Ranges', 'bytes');
|
||||
headers.set('Content-Range', 'bytes 0-' + (contentLength - 1) + '/' + contentLength);
|
||||
}
|
||||
var responseInit = {
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: headers
|
||||
};
|
||||
|
||||
var httpResponse = new Response(msgEvent.data.content, responseInit);
|
||||
|
||||
// Add or update css or javascript assets to the cache
|
||||
if (!excludedURLSchema.test(rqUrl) && /(text|application)\/(css|javascript)/i.test(contentType)) {
|
||||
updateCache(event.request, httpResponse.clone());
|
||||
}
|
||||
|
||||
// Let's send the content back from the ServiceWorker
|
||||
resolve(httpResponse);
|
||||
} else if (msgEvent.data.action === 'sendRedirect') {
|
||||
resolve(Response.redirect(prefix + msgEvent.data.redirectUrl));
|
||||
} else {
|
||||
console.error('Invalid message received from app.js for ' + titleWithNameSpace, msgEvent.data);
|
||||
reject(msgEvent.data);
|
||||
}
|
||||
};
|
||||
outgoingMessagePort.postMessage({
|
||||
'action': 'askForContent',
|
||||
'title': titleWithNameSpace
|
||||
}, [messageChannel.port2]);
|
||||
});
|
||||
} else {
|
||||
// It's not a ZIM URL
|
||||
return fetch(event.request).then(function (response) {
|
||||
// If request was success, add or update it in the cache
|
||||
if (!excludedURLSchema.test(rqUrl) && !/\.zim\w{0,2}$/i.test(rqUrl)) {
|
||||
event.waitUntil(updateCache(event.request, response.clone()));
|
||||
}
|
||||
return response;
|
||||
}).catch(function (error) {
|
||||
console.debug("[SW] Network request failed and no cache.", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function fromCache(request) {
|
||||
// Check to see if you have it in the cache
|
||||
// Return response
|
||||
// If not in the cache, then return
|
||||
return caches.open(CACHE).then(function (cache) {
|
||||
return cache.match(request).then(function (matching) {
|
||||
if (!matching || matching.status === 404) {
|
||||
return Promise.reject("no-match");
|
||||
}
|
||||
return matching;
|
||||
.then(function(clients) {
|
||||
clients.forEach(client => client.navigate(client.url))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateCache(request, response) {
|
||||
if (!excludedURLSchema.test(request.url||request)) {
|
||||
return caches.open(CACHE).then(function (cache) {
|
||||
return cache.put(request, response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Removes parameters and anchors from a URL
|
||||
function removeUrlParameters(url) {
|
||||
return url.replace(/([^?#]+)[?#].*$/, "$1");
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ param (
|
||||
$release_uri = 'https://api.github.com/repos/kiwix/kiwix-js-windows/actions/workflows/publish-docker.yaml/dispatches'
|
||||
|
||||
$app_params = Select-String 'appVersion' "$PSScriptRoot\..\www\js\init.js" -List
|
||||
$serviceworker = Select-String 'appVersion' "$PSScriptRoot\..\pwabuilder-sw.js" -List
|
||||
$serviceworker = Select-String 'appVersion' "$PSScriptRoot\..\service-worker.js" -List
|
||||
$suggested_build = ''
|
||||
$app_tag = ''
|
||||
if ($app_params -match 'params\[[''"]appVersion[''"]]\s*=\s*[''"]([^''"]+)') {
|
||||
@ -35,15 +35,15 @@ $sw_tag = ''
|
||||
if ($serviceworker -match 'appVersion\s*=\s*[''"]([^''"]+)') {
|
||||
$sw_tag = $matches[1]
|
||||
if ($sw_tag -ne $app_tag) {
|
||||
"*** WARNING: The version in init.js [$app_tag] does not match the version in pwabuilder-sw.js [$sw_tag]! ***"
|
||||
"*** WARNING: The version in init.js [$app_tag] does not match the version in service-worker.js [$sw_tag]! ***"
|
||||
"Please correct before continuing.`n"
|
||||
exit
|
||||
} else {
|
||||
"`nVersion in init.js: $app_tag"
|
||||
"Version in pwabuilder-sw.js: $sw_tag`n"
|
||||
"Version in service-worker.js: $sw_tag`n"
|
||||
}
|
||||
} else {
|
||||
"*** WARNING: App version is incorrectly set in pwabuilder-sw.js.`nPlease correct before continuing.`n"
|
||||
"*** WARNING: App version is incorrectly set in service-worker.js.`nPlease correct before continuing.`n"
|
||||
exit
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ foreach ($build in $builds) {
|
||||
# Copy latest binary x64
|
||||
cp $buildLocation\* $fullTarget -Recurse
|
||||
$root = $PSScriptRoot -replace 'scripts.*$', ''
|
||||
cp $root\package.json, $root\pwabuilder-sw.js, $root\index.html, $root\CHANGELOG.md, $root\LICENSE, $root\www $fullTarget -Recurse
|
||||
cp $root\package.json, $root\service-worker.js, $root\index.html, $root\CHANGELOG.md, $root\LICENSE, $root\www $fullTarget -Recurse
|
||||
"Copying archive..."
|
||||
md $archiveFolder
|
||||
cp "$root\archives\$PackagedArchive", "$root\archives\*.txt", "$root\archives\README.md" $archiveFolder
|
||||
|
@ -22,7 +22,7 @@ $release_uri = 'https://api.github.com/repos/kiwix/kiwix-js-windows/releases'
|
||||
$github_token = Get-Content -Raw "$PSScriptRoot/github_token"
|
||||
|
||||
$init_params = Get-Content -Raw "$PSScriptRoot\..\www\js\init.js"
|
||||
$serviceworker = Select-String 'appVersion' "$PSScriptRoot\..\pwabuilder-sw.js" -List
|
||||
$serviceworker = Select-String 'appVersion' "$PSScriptRoot\..\service-worker.js" -List
|
||||
|
||||
$file_tag = ''
|
||||
if ($init_params -match 'params\[[''"]appVersion[''"]]\s*=\s*[''"]([^''"]+)') {
|
||||
@ -35,15 +35,15 @@ $sw_tag = ''
|
||||
if ($serviceworker -match 'appVersion\s*=\s*[''"]([^''"]+)') {
|
||||
$sw_tag = 'v' + $matches[1]
|
||||
if ($sw_tag -ne $file_tag) {
|
||||
"`n*** WARNING: The version in init.js [$file_tag] does not match the version in pwabuilder-sw.js [$sw_tag]! ***"
|
||||
"`n*** WARNING: The version in init.js [$file_tag] does not match the version in service-worker.js [$sw_tag]! ***"
|
||||
"Please correct before continuing.`n"
|
||||
exit
|
||||
} else {
|
||||
"`nVersion in init.js: $file_tag"
|
||||
"Version in pwabuilder-sw.js: $sw_tag"
|
||||
"Version in service-worker.js: $sw_tag"
|
||||
}
|
||||
} else {
|
||||
"`n*** WARNING: App version is incorrectly set in pwabuilder-sw.js.`nPlease correct before continuing.`n"
|
||||
"`n*** WARNING: App version is incorrectly set in service-worker.js.`nPlease correct before continuing.`n"
|
||||
exit
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* service-worker.js : Service Worker implementation,
|
||||
* pwabuilder.js : Service Worker implementation,
|
||||
* in order to capture the HTTP requests made by an article, and respond with the
|
||||
* corresponding content, coming from the archive
|
||||
*
|
||||
@ -24,45 +24,304 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* A global Boolean that governs whether images are displayed
|
||||
* app.js can alter this variable via messaging
|
||||
* App version number - ENSURE IT MATCHES VALUE IN init.js
|
||||
* DEV: Changing this will cause the browser to recognize that the Service Worker has changed, and it will
|
||||
* download and install a new copy; we have to hard code this here because it is needed before any other file
|
||||
* is cached in APP_CACHE
|
||||
*/
|
||||
var imageDisplay;
|
||||
|
||||
self.addEventListener('install', function(event) {
|
||||
event.waitUntil(self.skipWaiting());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', function(event) {
|
||||
// "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
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
var regexpRemoveUrlParameters = new RegExp(/([^?#]+)[?#].*$/);
|
||||
|
||||
// This function is duplicated from uiUtil.js
|
||||
// because using requirejs would force to add the 'fetch' event listener
|
||||
// after the initial evaluation of this script, which is not supported any more
|
||||
// in recent versions of the browsers.
|
||||
// Cf https://bugzilla.mozilla.org/show_bug.cgi?id=1181127
|
||||
// TODO : find a way to avoid this duplication
|
||||
const appVersion = '1.8.5';
|
||||
|
||||
/**
|
||||
* Removes parameters and anchors from a URL
|
||||
* @param {type} url
|
||||
* @returns {String} same URL without its parameters and anchors
|
||||
* The name of the Cache API cache in which assets defined in regexpCachedContentTypes will be stored
|
||||
* The value is sometimes needed here before it can be passed from app.js, so we have to duplicate it
|
||||
* @type {String}
|
||||
*/
|
||||
function removeUrlParameters(url) {
|
||||
return url.replace(regexpRemoveUrlParameters, "$1");
|
||||
// DEV: Ensure this matches the name defined in app.js
|
||||
const ASSETS_CACHE = 'kiwixjs-assetsCache';
|
||||
|
||||
/**
|
||||
* The name of the application cache to use for caching online code so that it can be used offline
|
||||
* The cache name is made up of the prefix below and the appVersion: this is necessary so that when
|
||||
* the app is updated, a new cache is created. The new cache will start being used after the user
|
||||
* restarts the app, when we will also delete the old cache.
|
||||
* @type {String}
|
||||
*/
|
||||
const APP_CACHE = 'kiwixjs-appCache-' + appVersion;
|
||||
|
||||
/**
|
||||
* A global Boolean that governs whether ASSETS_CACHE will be used
|
||||
* Caching is on by default but can be turned off by the user in Configuration
|
||||
* @type {Boolean}
|
||||
*/
|
||||
var useCache = true;
|
||||
|
||||
/**
|
||||
* A Boolean that governs whether images are displayed
|
||||
* app.js can alter this variable via messaging
|
||||
*/
|
||||
let imageDisplay;
|
||||
|
||||
// Kiwix ZIM Archive Download Server in regex form
|
||||
// DEV: The server URL is defined in init.js, but is not available to us in SW
|
||||
const regexpKiwixDownloadLinks = /download\.kiwix\.org/i;
|
||||
|
||||
|
||||
/**
|
||||
* 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 '|'
|
||||
* @type {RegExp}
|
||||
*/
|
||||
var regexpCachedContentTypes = /text\/css|text\/javascript|application\/javascript/i;
|
||||
|
||||
/**
|
||||
* A regular expression that excludes listed schemata from caching attempts
|
||||
* As of 08-2019 the chrome-extension: schema is incompatible with the Cache API
|
||||
* 'example-extension' is included to show how to add another schema if necessary
|
||||
* @type {RegExp}
|
||||
*/
|
||||
var regexpExcludedURLSchema = /^(?:file|chrome-extension|example-extension):/i;
|
||||
|
||||
/**
|
||||
* Pattern for ZIM file namespace: see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces
|
||||
* In our case, there is also the ZIM file name used as a prefix in the URL
|
||||
* @type {RegExp}
|
||||
*/
|
||||
const regexpZIMUrlWithNamespace = /(?:^|\/)([^/]+\/)([-ABCIJMUVWX])\/(.+)/;
|
||||
|
||||
/**
|
||||
* 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/-/mw/ext.cite.styles.css",
|
||||
"www/-/mw/ext.cite.ux-enhancements.css",
|
||||
"www/-/mw/ext.math.scripts.css",
|
||||
"www/-/mw/ext.math.styles.css",
|
||||
"www/-/mw/ext.kartographer.frame.css",
|
||||
"www/-/mw/ext.kartographer.link.css",
|
||||
"www/-/mw/ext.kartographer.style.css",
|
||||
"www/-/mw/ext.scribunto.logs.css",
|
||||
"www/-/mw/ext.tmh.thumbnail.styles.css",
|
||||
"www/-/mw/inserted_style.css",
|
||||
"www/-/mw/inserted_style_mobile.css",
|
||||
"www/-/mw/mediawiki.page.gallery.styles.css",
|
||||
"www/-/mw/mobile.css",
|
||||
"www/-/mw/mw.MediaWikiPlayer.loader.css",
|
||||
"www/-/mw/mw.PopUpMediaTransform.css",
|
||||
"www/-/mw/mw.TMHGalleryHook.js.css",
|
||||
"www/-/mw/style.css",
|
||||
"www/-/s/css_modules/content.parsoid.css",
|
||||
"www/-/s/css_modules/ext.cite.a11y.css",
|
||||
"www/-/s/css_modules/ext.cite.styles.css",
|
||||
"www/-/s/css_modules/ext.cite.ux-enhancements.css",
|
||||
"www/-/s/css_modules/ext.inputBox.styles.css",
|
||||
"www/-/s/css_modules/ext.kartographer.frame.css",
|
||||
"www/-/s/css_modules/ext.kartographer.link.css",
|
||||
"www/-/s/css_modules/ext.kartographer.style.css",
|
||||
"www/-/s/css_modules/inserted_style.css",
|
||||
"www/-/s/css_modules/inserted_style_mobile.css",
|
||||
"www/-/s/css_modules/mobile.css",
|
||||
"www/-/s/css_modules/style.css",
|
||||
"www/-/style.css",
|
||||
"www/-/s/style.css",
|
||||
"www/-/s/style-dark.css",
|
||||
"www/-/s/style-dark-invert.css",
|
||||
"www/-/s/style-mobile.css",
|
||||
"www/-/s/vector.css",
|
||||
"www/I/COVID-19_lifecycle.jpg",
|
||||
"www/I/s/Icon_External_Link.png",
|
||||
"www/I/s/Icons-mini-file_acrobat.gif",
|
||||
"www/css/app.css",
|
||||
"www/css/bootstrap.min.css",
|
||||
"www/fonts/glyphicons-halflings-regular.woff2",
|
||||
"www/img/icons/kiwix-256.png",
|
||||
"www/img/icons/kiwix-192.png",
|
||||
"www/img/icons/kiwix-32.png",
|
||||
"www/img/icons/kiwix-60.png",
|
||||
"www/img/icons/kiwix-blue-32.png",
|
||||
"www/img/icons/kiwix-midnightblue-90.png",
|
||||
"www/img/icons/wikimed-blue-32.png",
|
||||
"www/img/icons/wikimed-lightblue-32.png",
|
||||
"www/img/icons/wikivoyage-90-white.png",
|
||||
"www/img/icons/wikivoyage-black-32.png",
|
||||
"www/img/icons/wikivoyage-white-32.png",
|
||||
"www/img/icons/map_marker-30px.png",
|
||||
"www/img/icons/map_marker-18px.png",
|
||||
"www/img/spinner.gif",
|
||||
"www/index.html",
|
||||
"www/article.html",
|
||||
"www/js/app.js",
|
||||
"www/js/init.js",
|
||||
"www/js/lib/arrayFromPolyfill.js",
|
||||
"www/js/lib/bootstrap.js",
|
||||
"www/js/lib/bootstrap.min.js",
|
||||
"www/js/lib/cache.js",
|
||||
"www/js/lib/filecache.js",
|
||||
"www/js/lib/images.js",
|
||||
"www/js/lib/jquery-3.2.1.slim.js",
|
||||
"www/js/lib/kiwixServe.js",
|
||||
"www/js/lib/promisePolyfill.js",
|
||||
"www/js/lib/require.js",
|
||||
"www/js/lib/settingsStore.js",
|
||||
"www/js/lib/transformStyles.js",
|
||||
"www/js/lib/uiUtil.js",
|
||||
"www/js/lib/utf8.js",
|
||||
"www/js/lib/util.js",
|
||||
//"www/js/lib/webpHeroBundle_0.0.0-dev.27.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/katex/katex.min.js",
|
||||
"www/js/katex/katex.min.css",
|
||||
"www/js/katex/contrib/mathtex-script-type.min.js",
|
||||
"www/js/katex/fonts/KaTeX_AMS-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Main-Bold.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Main-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Math-Italic.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Size1-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Size2-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Size3-Regular.woff2",
|
||||
"www/js/katex/fonts/KaTeX_Size4-Regular.woff2"
|
||||
];
|
||||
|
||||
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"
|
||||
);
|
||||
} else {
|
||||
precacheFiles.push(
|
||||
"www/js/lib/xzdec-asm.js",
|
||||
"www/js/lib/zstddec-asm.js"
|
||||
);
|
||||
}
|
||||
|
||||
var outgoingMessagePort = null;
|
||||
var fetchCaptureEnabled = false;
|
||||
self.addEventListener('fetch', fetchEventListener);
|
||||
|
||||
self.addEventListener('message', function (event) {
|
||||
// Process install event
|
||||
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) {
|
||||
return Promise.all(
|
||||
requests.map(function (request) {
|
||||
return fetch(request).then(function (response) {
|
||||
// Fail on 404, 500 etc
|
||||
if (!response.ok) throw Error('Could not fetch ' + request.url);
|
||||
return cache.put(request.url.replace(/\?v[^?/]+$/, ''), response);
|
||||
}).catch(function (err) {
|
||||
console.error('There was an error pre-caching files', err);
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Allow sw to control current page
|
||||
self.addEventListener('activate', function (event) {
|
||||
console.debug('[SW] Claiming clients for current page');
|
||||
// Check all the cache keys, and delete any old caches
|
||||
event.waitUntil(
|
||||
caches.keys().then(function (keyList) {
|
||||
return Promise.all(keyList.map(function (key) {
|
||||
console.debug('[SW] Current cache key is ' + key);
|
||||
if (key !== APP_CACHE && key !== ASSETS_CACHE) {
|
||||
console.debug('[SW] App updated to version ' + appVersion + ': deleting old cache');
|
||||
return caches.delete(key);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
let outgoingMessagePort = null;
|
||||
let fetchCaptureEnabled = false;
|
||||
|
||||
self.addEventListener('fetch', intercept);
|
||||
|
||||
// Look up fetch in cache, and if it does not exist, try to get it from the network
|
||||
function intercept(event) {
|
||||
// Test if we're in an Electron app
|
||||
// DEV: Electron uses the file:// protocol and hacks it to work with SW, but it has CORS issues when using the Fetch API to fetch local files,
|
||||
// so we must bypass it here if we're fetching a local file
|
||||
if (/^file:/i.test(event.request.url) && ! (regexpZIMUrlWithNamespace.test(event.request.url) && /\.zim\w{0,2}\//i.test(event.request.url))) return;
|
||||
// console.debug('[SW] Service Worker ' + (event.request.method === "GET" ? 'intercepted ' : 'noted ') + event.request.url, event.request.method);
|
||||
if (event.request.method !== "GET") return;
|
||||
// Don't cache download links
|
||||
if (regexpKiwixDownloadLinks.test(event.request.url)) return;
|
||||
// Remove any querystring except 'kiwix-display'
|
||||
var rqUrl = event.request.url.replace(/\?(?!kiwix-display)[^?]+$/i, '');
|
||||
// Select cache depending on request format
|
||||
var cache = /\.zim\//i.test(rqUrl) ? ASSETS_CACHE : APP_CACHE;
|
||||
if (cache === ASSETS_CACHE && !fetchCaptureEnabled) 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
|
||||
return response;
|
||||
}, function () {
|
||||
// The response was not found in the cache so we look for it on the server
|
||||
if (cache === ASSETS_CACHE && regexpZIMUrlWithNamespace.test(rqUrl)) {
|
||||
if (imageDisplay !== 'all' && /(^|\/)[IJ]\/.*\.(jpe?g|png|svg|gif|webp)($|[?#])(?!kiwix-display)/i.test(rqUrl)) {
|
||||
// If the user has disabled the display of images, and the browser wants an image, respond with empty SVG
|
||||
// A URL with "?kiwix-display" query string acts as a passthrough so that the regex will not match and
|
||||
// the image will be fetched by app.js
|
||||
// DEV: If you need to hide more image types, add them to regex below and also edit equivalent regex in app.js
|
||||
var svgResponse;
|
||||
if (imageDisplay === 'manual')
|
||||
svgResponse = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><rect width='1' height='1' style='fill:lightblue'/></svg>";
|
||||
else
|
||||
svgResponse = "<svg xmlns='http://www.w3.org/2000/svg'/>";
|
||||
return new Response(svgResponse, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml'
|
||||
}
|
||||
});
|
||||
}
|
||||
return fetchRequestFromZIM(event).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')) &&
|
||||
!regexpExcludedURLSchema.test(event.request.url)) {
|
||||
event.waitUntil(updateCache(ASSETS_CACHE, event.request, response.clone()));
|
||||
}
|
||||
return response;
|
||||
}).catch(function (msgPortData, title) {
|
||||
console.error('Invalid message received from app.js for ' + title, msgPortData);
|
||||
return msgPortData;
|
||||
});
|
||||
} else {
|
||||
// It's not an asset, or it doesn't match a ZIM URL pattern, so we should fetch it with Fetch API
|
||||
return fetch(event.request).then(function (response) {
|
||||
// If request was success, add or update it in the cache
|
||||
if (!regexpExcludedURLSchema.test(rqUrl) && !/\.zim\w{0,2}$/i.test(rqUrl)) {
|
||||
event.waitUntil(updateCache(APP_CACHE, event.request, response.clone()));
|
||||
}
|
||||
return response;
|
||||
}).catch(function (error) {
|
||||
console.debug("[SW] Network request failed and no cache.", error);
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle custom commands 'init' and 'disable' from app.js
|
||||
*/
|
||||
self.addEventListener('message', function (event) {
|
||||
if (event.data.action === 'init') {
|
||||
// On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener
|
||||
outgoingMessagePort = event.ports[0];
|
||||
@ -72,44 +331,22 @@ self.addEventListener('message', function (event) {
|
||||
// On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener
|
||||
outgoingMessagePort = null;
|
||||
fetchCaptureEnabled = false;
|
||||
self.removeEventListener('fetch', intercept);
|
||||
}
|
||||
});
|
||||
|
||||
// Pattern for ZIM file namespace - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces
|
||||
// In our case, there is also the ZIM file name, used as a prefix in the URL
|
||||
var regexpZIMUrlWithNamespace = /(?:^|\/)([^\/]+\/)([-ABIJMUVWX])\/(.+)/;
|
||||
|
||||
function fetchEventListener(event) {
|
||||
if (fetchCaptureEnabled) {
|
||||
|
||||
if (!/\.zim\//i.test(event.request.url)) {
|
||||
console.log('SW is getting file from cache: ' + event.request.url);
|
||||
return;
|
||||
}
|
||||
|
||||
if (regexpZIMUrlWithNamespace.test(event.request.url)) {
|
||||
// The ServiceWorker will handle this request
|
||||
|
||||
// If the user has disabled the display of images, and the browser wants an image, respond with empty SVG
|
||||
// A URL with "?kiwix-display" query string acts as a passthrough so that the regex will not match and
|
||||
// the image will be fetched by app.js
|
||||
// DEV: If you need to hide more image types, add them to regex below and also edit equivalent regex in app.js
|
||||
if (imageDisplay !== 'all' && /(^|\/)[IJ]\/.*\.(jpe?g|png|svg|gif)($|[?#])(?!kiwix-display)/i.test(event.request.url)) {
|
||||
var svgResponse;
|
||||
if (imageDisplay === 'manual')
|
||||
svgResponse = "<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'><rect width='1' height='1' style='fill:lightblue'/></svg>";
|
||||
else
|
||||
svgResponse = "<svg xmlns='http://www.w3.org/2000/svg'/>";
|
||||
event.respondWith(new Response(svgResponse, { headers: { 'Content-Type': 'image/svg+xml' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Let's ask app.js for that content
|
||||
event.respondWith(new Promise(function(resolve, reject) {
|
||||
/**
|
||||
* Handles fetch events that need to be extracted from the ZIM
|
||||
*
|
||||
* @param {Event} fetchEvent The fetch event to be processed
|
||||
* @returns {Promise<Response>} A Promise for the Response, or rejects with the invalid message port data
|
||||
*/
|
||||
function fetchRequestFromZIM(fetchEvent) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var nameSpace;
|
||||
var title;
|
||||
var titleWithNameSpace;
|
||||
var regexpResult = regexpZIMUrlWithNamespace.exec(event.request.url);
|
||||
var regexpResult = regexpZIMUrlWithNamespace.exec(fetchEvent.request.url);
|
||||
var prefix = regexpResult[1];
|
||||
nameSpace = regexpResult[2];
|
||||
title = regexpResult[3];
|
||||
@ -121,25 +358,27 @@ function fetchEventListener(event) {
|
||||
|
||||
// Let's instantiate a new messageChannel, to allow app.js to give us the content
|
||||
var messageChannel = new MessageChannel();
|
||||
messageChannel.port1.onmessage = function(event) {
|
||||
if (event.data.action === 'giveContent') {
|
||||
messageChannel.port1.onmessage = function (msgPortEvent) {
|
||||
if (msgPortEvent.data.action === 'giveContent') {
|
||||
// Content received from app.js
|
||||
var contentLength = event.data.content ? event.data.content.byteLength : null;
|
||||
var contentType = event.data.mimetype;
|
||||
var contentLength = msgPortEvent.data.content ? msgPortEvent.data.content.byteLength : null;
|
||||
var contentType = msgPortEvent.data.mimetype;
|
||||
// Set the imageDisplay variable if it has been sent in the event data
|
||||
imageDisplay = typeof event.data.imageDisplay !== 'undefined' ?
|
||||
event.data.imageDisplay : imageDisplay;
|
||||
var headers = new Headers ();
|
||||
imageDisplay = typeof msgPortEvent.data.imageDisplay !== 'undefined' ?
|
||||
msgPortEvent.data.imageDisplay : imageDisplay;
|
||||
var headers = new Headers();
|
||||
if (contentLength) headers.set('Content-Length', contentLength);
|
||||
// Prevent CORS issues in PWAs
|
||||
if (contentLength) headers.set('Access-Control-Allow-Origin', '*');
|
||||
if (contentType) headers.set('Content-Type', contentType);
|
||||
// Test if the content is a video or audio file
|
||||
// See kiwix-js #519 and openzim/zimwriterfs #113 for why we test for invalid types like "mp4" or "webm" (without "video/")
|
||||
// The full list of types produced by zimwriterfs is in https://github.com/openzim/zimwriterfs/blob/master/src/tools.cpp
|
||||
if (contentLength >= 1 && /^(video|audio)|(^|\/)(mp4|webm|og[gmv]|mpeg)$/i.test(contentType)) {
|
||||
// In case of a video (at least), Chrome and Edge need these HTTP headers else seeking doesn't work
|
||||
// In case of a video (at least), Chrome and Edge need these HTTP headers or else seeking doesn't work
|
||||
// (even if we always send all the video content, not the requested range, until the backend supports it)
|
||||
headers.set('Accept-Ranges', 'bytes');
|
||||
headers.set('Content-Range', 'bytes 0-' + (contentLength-1) + '/' + contentLength);
|
||||
headers.set('Content-Range', 'bytes 0-' + (contentLength - 1) + '/' + contentLength);
|
||||
}
|
||||
var responseInit = {
|
||||
status: 200,
|
||||
@ -147,23 +386,84 @@ function fetchEventListener(event) {
|
||||
headers: headers
|
||||
};
|
||||
|
||||
var httpResponse = new Response(event.data.content, responseInit);
|
||||
var httpResponse = new Response(msgPortEvent.data.content, responseInit);
|
||||
|
||||
// Let's send the content back from the ServiceWorker
|
||||
resolve(httpResponse);
|
||||
}
|
||||
else if (event.data.action === 'sendRedirect') {
|
||||
resolve(Response.redirect(prefix + event.data.redirectUrl));
|
||||
}
|
||||
else {
|
||||
console.error('Invalid message received from app.js for ' + titleWithNameSpace, event.data);
|
||||
reject(event.data);
|
||||
} else if (msgPortEvent.data.action === 'sendRedirect') {
|
||||
resolve(Response.redirect(prefix + msgPortEvent.data.redirectUrl));
|
||||
} else {
|
||||
reject(msgPortEvent.data, titleWithNameSpace);
|
||||
}
|
||||
};
|
||||
outgoingMessagePort.postMessage({'action': 'askForContent', 'title': titleWithNameSpace}, [messageChannel.port2]);
|
||||
}));
|
||||
}
|
||||
// If event.respondWith() isn't called because this wasn't a request that we want to handle,
|
||||
// then the default request/response behavior will automatically be used.
|
||||
}
|
||||
outgoingMessagePort.postMessage({
|
||||
'action': 'askForContent',
|
||||
'title': titleWithNameSpace
|
||||
}, [messageChannel.port2]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes parameters and anchors from a URL
|
||||
* @param {type} url The URL to be processed
|
||||
* @returns {String} The same URL without its parameters and anchors
|
||||
*/
|
||||
function removeUrlParameters(url) {
|
||||
return url.replace(/([^?#]+)[?#].*$/, '$1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a Request in a cache and returns a Promise for the matched Response
|
||||
* @param {String} cache The name of the cache to look in
|
||||
* @param {String} requestUrl The Request URL to fulfill from cache
|
||||
* @returns {Promise<Response>} A Promise for the cached Response, or rejects with strings 'disabled' or 'no-match'
|
||||
*/
|
||||
function fromCache(cache, requestUrl) {
|
||||
// Prevents use of Cache API if user has disabled it
|
||||
if (!useCache && cache === ASSETS_CACHE) return Promise.reject('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');
|
||||
}
|
||||
console.debug('[SW] Supplying ' + requestUrl + ' from ' + cache + '...');
|
||||
return matching;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores or updates in a cache the given Request/Response pair
|
||||
* @param {String} cache The name of the cache to open
|
||||
* @param {Request} request The original Request object
|
||||
* @param {Response} response The Response received from the server/ZIM
|
||||
* @returns {Promise} A Promise for the update action
|
||||
*/
|
||||
function updateCache(cache, request, response) {
|
||||
// Prevents use of Cache API if user has disabled it
|
||||
if (!useCache && cache === ASSETS_CACHE) return Promise.resolve();
|
||||
return caches.open(cache).then(function (cacheObj) {
|
||||
console.debug('[SW] Adding ' + request.url + ' to ' + cache + '...');
|
||||
return cacheObj.put(request, response);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the caching strategy available to this app and if it is Cache API, count the
|
||||
* number of assets in ASSETS_CACHE
|
||||
* @param {String} url A URL to test against excludedURLSchema
|
||||
* @returns {Promise<Array>} A Promise for an array of format [cacheType, cacheDescription, assetCount]
|
||||
*/
|
||||
function testCacheAndCountAssets(url) {
|
||||
if (regexpExcludedURLSchema.test(url)) return Promise.resolve(['custom', 'Custom', '-']);
|
||||
if (!useCache) return Promise.resolve(['none', 'none', 'None', 0]);
|
||||
return caches.open(ASSETS_CACHE).then(function (cache) {
|
||||
return cache.keys().then(function (keys) {
|
||||
return ['cacheAPI', ASSETS_CACHE, 'Cache API', keys.length];
|
||||
}).catch(function(err) {
|
||||
return err;
|
||||
});
|
||||
}).catch(function(err) {
|
||||
return err;
|
||||
});
|
||||
}
|
||||
|
@ -67,7 +67,7 @@
|
||||
console.log('Service Worker is already active');
|
||||
} else {
|
||||
// Register the service worker
|
||||
navigator.serviceWorker.register("../pwabuilder-sw.js", {
|
||||
navigator.serviceWorker.register("../service-worker.js", {
|
||||
scope: "../"
|
||||
}).then(function (reg) {
|
||||
console.log("Service Worker has been registered for scope: " + reg.scope);
|
||||
|
@ -837,9 +837,10 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett
|
||||
// Check for upgrade of PWA
|
||||
if (!params.upgradeNeeded && /PWA/.test(params.appType) && activeBtn === 'btnConfigure') {
|
||||
caches.keys().then(function (keyList) {
|
||||
if (keyList.length < 2) document.getElementById('alertBoxPersistent').innerHTML = '';
|
||||
if (keyList.length < 3) document.getElementById('alertBoxPersistent').innerHTML = '';
|
||||
keyList.forEach(function(key) {
|
||||
if (key === 'kiwix-precache-' + params.appVersion) return;
|
||||
if (key === cache.APPCACHE) return;
|
||||
if (key === cache.CACHEAPI) return;
|
||||
// If we get here, then there is a cache key that does not match our version, i.e. a PWA-in-waiting
|
||||
params.upgradeNeeded = true;
|
||||
document.getElementById('alertBoxPersistent').innerHTML =
|
||||
@ -848,7 +849,8 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett
|
||||
' <span id="persistentMessage"></span>\n' +
|
||||
'</div>\n';
|
||||
var loadOrInstall = params.PWAInstalled ? 'install' : 'load';
|
||||
document.getElementById('persistentMessage').innerHTML = 'Version ' + key.replace(/kiwix-precache-/, '') + ' is ready to '
|
||||
var cachePrefix = cache.APPCACHE.replace(/[\d.]+$/, '');
|
||||
document.getElementById('persistentMessage').innerHTML = 'Version ' + key.replace(cachePrefix, '') + ' is ready to '
|
||||
+ loadOrInstall + '! (Re-launch app to ' + loadOrInstall + '.)';
|
||||
});
|
||||
});
|
||||
@ -1892,7 +1894,7 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett
|
||||
// Create the MessageChannel and send 'init'
|
||||
initOrKeepAliveServiceWorker();
|
||||
} else {
|
||||
navigator.serviceWorker.register('../pwabuilder-sw.js').then(function (reg) {
|
||||
navigator.serviceWorker.register('../service-worker.js').then(function (reg) {
|
||||
// The ServiceWorker is registered
|
||||
console.log('Service worker is registered with a scope of ' + reg.scope);
|
||||
serviceWorkerRegistration = reg;
|
||||
@ -4027,27 +4029,20 @@ define(['jquery', 'zimArchiveLoader', 'uiUtil', 'util', 'cache', 'images', 'sett
|
||||
blobArray.push([title, cssBlobCache.get(title)]);
|
||||
injectCSS();
|
||||
} else {
|
||||
appstate.selectedArchive.getDirEntryByPath(title)
|
||||
.then(function (dirEntry) {
|
||||
uiUtil.poll("Resolving CSS [" + title.replace(/[^/]+\//g, '').substring(0, 18) + "]...");
|
||||
return appstate.selectedArchive.readBinaryFile(dirEntry,
|
||||
function (fileDirEntry, content) {
|
||||
var cacheKey = appstate.selectedArchive._file.name + '/' + title;
|
||||
cache.getItemFromCacheOrZIM(appstate.selectedArchive, cacheKey).then(function (content) {
|
||||
//DEV: Uncomment line below and break on next to capture cssContent for local filesystem cache
|
||||
//var cssContent = util.uintToString(content);
|
||||
var cssBlob = new Blob([content], {
|
||||
type: 'text/css'
|
||||
});
|
||||
var newURL = [fileDirEntry.namespace + "/" + fileDirEntry.url, URL.createObjectURL(cssBlob)];
|
||||
var newURL = [title, URL.createObjectURL(cssBlob)];
|
||||
blobArray.push(newURL);
|
||||
if (cssBlobCache)
|
||||
cssBlobCache.set(newURL[0], newURL[1]);
|
||||
injectCSS(); //DO NOT move this: it must run within .then function to pass correct values
|
||||
});
|
||||
}).catch(function (e) {
|
||||
console.error("could not find DirEntry for CSS : %s", title, e);
|
||||
//@TODO Change this to push an array of [title, title] afters simplified code in injectCSS()
|
||||
blobArray.push(title);
|
||||
injectCSS();
|
||||
}).catch(function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ var params = {};
|
||||
* @type Object
|
||||
*/
|
||||
var appstate = {};
|
||||
/******** UPDATE VERSION IN pwabuilder-sw.js TO MATCH VERSION AND CHECK PWASERVER BELOW!!!!!!! *******/
|
||||
/******** UPDATE VERSION IN service-worker.js TO MATCH VERSION AND CHECK PWASERVER BELOW!!!!!!! *******/
|
||||
params['appVersion'] = "1.8.5"; //DEV: Manually update this version when there is a new release: it is compared to the Settings Store "appVersion" in order to show first-time info, and the cookie is updated in app.js
|
||||
/******* UPDATE THIS ^^^^^^ IN service worker AND PWA-SERVER BELOW !! ********************/
|
||||
params['packagedFile'] = getSetting('packagedFile') || "wikipedia_en_100_nopic_2021-11.zim"; //For packaged Kiwix JS (e.g. with Wikivoyage file), set this to the filename (for split files, give the first chunk *.zimaa) and place file(s) in default storage
|
||||
@ -394,7 +394,7 @@ function getSetting(name) {
|
||||
|
||||
function setSetting(name, val) {
|
||||
if (params.storeType === 'cookie') {
|
||||
document.cookie = encodeUriComponent(name) + '=' + encodeUriComponent(val) + ';expires=Fri, 31 Dec 9999 23:59:59 GMT';
|
||||
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(val) + ';expires=Fri, 31 Dec 9999 23:59:59 GMT';
|
||||
}
|
||||
// Make Boolean value
|
||||
val = val === 'false' ? false : val === 'true' ? true : val;
|
||||
|
@ -23,9 +23,10 @@
|
||||
'use strict';
|
||||
define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) {
|
||||
|
||||
const CACHEAPI = 'kiwix-precache-' + params.appVersion; // Set the database or cache name here
|
||||
const CACHEIDB = 'kiwix-assetsCache'; // For idxDB we don't want the name to change
|
||||
const CACHEAPI = 'kiwixjs-assetsCache'; // Set the database or cache name here, and synchronize with Service Worker
|
||||
const CACHEIDB = 'kiwix-assetsCache'; // Slightly different name to disambiguate
|
||||
var objStore = 'kiwix-assets'; // Name of the object store
|
||||
const APPCACHE = 'kiwixjs-appCache-' + params.appVersion; // Ensure this is the same as in Service Worker
|
||||
|
||||
// DEV: Regex below defines the permitted key types for the cache; add further types as needed
|
||||
// NB: The key type of '.zim', or '.zimaa' (etc.) is used to store a ZIM's last-accessed article
|
||||
@ -407,14 +408,14 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) {
|
||||
*
|
||||
* @param {Object} selectedArchive The ZIM archive picked by the user
|
||||
* @param {String} key The cache key of the item to retrieve
|
||||
* @param {Function} callback A function to call with the result
|
||||
* @param {Object} dirEntry If the item's dirEntry has already been looked up, it can optionally be
|
||||
* supplied here (saves a redundant dirEntry lookup)
|
||||
*/
|
||||
function getItemFromCacheOrZIM(selectedArchive, key, callback, dirEntry) {
|
||||
function getItemFromCacheOrZIM(selectedArchive, key, dirEntry) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
// First check if the item is already in the cache
|
||||
var title = key.replace(/^[^/]+\//, '');
|
||||
getItem(key, function(result) {
|
||||
getItem(key, function (result) {
|
||||
if (result !== null && result !== false && typeof result !== 'undefined') {
|
||||
console.log("Cache supplied " + title);
|
||||
if (/\.css$/.test(title)) {
|
||||
@ -425,7 +426,7 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) {
|
||||
document.getElementById('searchingArticles').style.display = 'none';
|
||||
}
|
||||
}
|
||||
callback(result);
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
// Since there was no result, post UI messages and look up asset in ZIM
|
||||
@ -446,7 +447,7 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) {
|
||||
var getDirEntry = dirEntry ? Promise.Promise.resolve() :
|
||||
selectedArchive.getDirEntryByPath(title);
|
||||
// Read data from ZIM
|
||||
getDirEntry.then(function(resolvedDirEntry) {
|
||||
getDirEntry.then(function (resolvedDirEntry) {
|
||||
if (dirEntry) resolvedDirEntry = dirEntry;
|
||||
if (resolvedDirEntry === null) {
|
||||
console.log("Error: asset file not found: " + title);
|
||||
@ -473,8 +474,7 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) {
|
||||
document.getElementById('searchingArticles').style.display = 'none';
|
||||
}
|
||||
}
|
||||
callback(content);
|
||||
setItem(key, content, function(result) {
|
||||
setItem(key, content, function (result) {
|
||||
if (result === -1) {
|
||||
// Cache rejected item due to user settings
|
||||
} else if (result) {
|
||||
@ -483,11 +483,12 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) {
|
||||
console.error('Cache: failed to store asset ' + title);
|
||||
}
|
||||
});
|
||||
resolve(content);
|
||||
});
|
||||
}
|
||||
}).fail(function (e) {
|
||||
console.error("could not find DirEntry for asset : " + title, e);
|
||||
callback();
|
||||
}).catch(function (e) {
|
||||
reject("could not find DirEntry for asset : " + title, e);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -709,6 +710,8 @@ define(['settingsStore', 'uiUtil'], function(settingsStore, uiUtil) {
|
||||
* Functions and classes exposed by this module
|
||||
*/
|
||||
return {
|
||||
APPCACHE: APPCACHE,
|
||||
CACHEAPI: CACHEAPI,
|
||||
test: test,
|
||||
count: count,
|
||||
idxDB: idxDB,
|
||||
|
Loading…
x
Reference in New Issue
Block a user