/** * service-worker.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 * * Copyright 2015 Mossroy and contributors * License GPL v3: * * This file is part of Kiwix. * * Kiwix is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Kiwix is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Kiwix (file LICENSE-GPLv3.txt). If not, see */ 'use strict'; /** * A global Boolean that governs whether images are displayed * app.js can alter this variable via messaging */ 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 /** * Removes parameters and anchors from a URL * @param {type} url * @returns {String} same URL without its parameters and anchors */ function removeUrlParameters(url) { return url.replace(regexpRemoveUrlParameters, "$1"); } var outgoingMessagePort = null; var fetchCaptureEnabled = false; self.addEventListener('fetch', fetchEventListener); 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 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 = ""; else svgResponse = ""; 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]); })); } // 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. } }