mirror of
https://github.com/kiwix/kiwix-js-pwa.git
synced 2025-09-08 11:48:26 -04:00

Former-commit-id: 4cd056e5d578816a38508ac6d96320e13a259287 [formerly 2fb24c2b8cd694f7afaab9f0e0a93682d0bea9e8] Former-commit-id: 162c4cfddc0f3d89d9d92800de262bed5c450336
233 lines
8.7 KiB
JavaScript
233 lines
8.7 KiB
JavaScript
// Service Worker with Cache-first network, with some code from pwabuilder.com
|
|
'use strict';
|
|
|
|
const CACHE = "sw-precache";
|
|
const precacheFiles = [
|
|
"manifest.json",
|
|
"www/I/s/Icon_External_Link.png",
|
|
"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-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/map_marker-18px.png",
|
|
"www/img/icons/wikimed-blue-32.png",
|
|
"www/img/spinner.gif",
|
|
"www/index.html",
|
|
"www/js/app.js",
|
|
"www/js/init.js",
|
|
"www/js/lib/bootstrap.js",
|
|
"www/js/lib/bootstrap.min.js",
|
|
"www/js/lib/cookies.js",
|
|
"www/js/lib/images.js",
|
|
"www/js/lib/jquery-3.2.1.slim.js",
|
|
"www/js/lib/kiwixServe.js",
|
|
"www/js/lib/q.js",
|
|
"www/js/lib/require.js",
|
|
"www/js/lib/transformStyles.js",
|
|
"www/js/lib/uiUtil.js",
|
|
"www/js/lib/utf8.js",
|
|
"www/js/lib/util.js",
|
|
"www/js/lib/xzdec.js",
|
|
"www/js/lib/xzdec_wrapper.js",
|
|
"www/js/lib/zimArchive.js",
|
|
"www/js/lib/zimArchiveLoader.js",
|
|
"www/js/lib/zimDirEntry.js",
|
|
"www/js/lib/zimfile.js"
|
|
];
|
|
|
|
self.addEventListener("install", function (event) {
|
|
console.log("[SW] Install Event processing");
|
|
|
|
console.log("[SW] Skip waiting on install");
|
|
self.skipWaiting();
|
|
|
|
event.waitUntil(
|
|
caches.open(CACHE).then(function (cache) {
|
|
console.log("[SW] Caching pages during install");
|
|
return cache.addAll(precacheFiles);
|
|
})
|
|
);
|
|
});
|
|
|
|
// Allow sw to control current page
|
|
self.addEventListener("activate", function (event) {
|
|
console.log("[SW] Claiming clients for current page");
|
|
event.waitUntil(self.clients.claim());
|
|
});
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
});
|
|
|
|
// 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 = /(?:^|\/)([^\/]+\/)([-ABIJMUVWX])\/(.+)/;
|
|
|
|
|
|
// If any fetch fails, it will look for the request in the cache and serve it from there first
|
|
self.addEventListener("fetch", function (event) {
|
|
console.log('[SW] Service Worker ' + (event.request.method === "GET" ? 'intercepted ' : 'noted ') + event.request.url, event.request.method);
|
|
if (event.request.method !== "GET") return;
|
|
if (/\.zim\w{0,2}\//i.test(event.request.url) && regexpZIMUrlWithNamespace.test(event.request.url)) {
|
|
// 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) {
|
|
var nameSpace;
|
|
var title;
|
|
var titleWithNameSpace;
|
|
var regexpResult = regexpZIMUrlWithNamespace.exec(event.request.url);
|
|
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 (event) {
|
|
if (event.data.action === 'giveContent') {
|
|
// Content received from app.js
|
|
var contentLength = event.data.content ? event.data.content.byteLength : null;
|
|
var contentType = event.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();
|
|
if (contentLength) headers.set('Content-Length', contentLength);
|
|
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(event.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);
|
|
}
|
|
};
|
|
outgoingMessagePort.postMessage({
|
|
'action': 'askForContent',
|
|
'title': titleWithNameSpace
|
|
}, [messageChannel.port2]);
|
|
}));
|
|
} else {
|
|
event.respondWith(
|
|
fromCache(event.request).then(
|
|
function (response) {
|
|
// The response was found in the cache so we responde with it and update the entry
|
|
|
|
// This is where we call the server to get the newest version of the
|
|
// file to use the next time we show view
|
|
event.waitUntil(
|
|
fetch(event.request).then(function (response) {
|
|
console.log('[SW] Refreshing CACHE from server...');
|
|
return updateCache(event.request, response);
|
|
})
|
|
);
|
|
console.log('[SW] Supplying ' + event.request.url + ' from CACHE...');
|
|
return response;
|
|
},
|
|
function () {
|
|
// The response was not found in the cache so we look for it on the server
|
|
return fetch(event.request)
|
|
.then(function (response) {
|
|
// If request was success, add or update it in the cache
|
|
event.waitUntil(updateCache(event.request, response.clone()));
|
|
|
|
return response;
|
|
})
|
|
.catch(function (error) {
|
|
console.log("[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;
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateCache(request, response) {
|
|
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");
|
|
} |