[DEL] docs(file/topic): Supprime favicon et manifest inutilisés. [FEAT] feat(file/topic): Ajoute PWA bundle et CSP pour la sécurité. ```
600 lines
18 KiB
JavaScript
600 lines
18 KiB
JavaScript
|
|
|
|
/**************************************************** WORKBOX IMPORT ****************************************************/
|
|
// The configuration is set to use Workbox
|
|
// The following code will import Workbox from CDN or public URL
|
|
// Import from public URL
|
|
|
|
importScripts('/workbox/workbox-sw.js');
|
|
importScripts('/idb/umd.js');
|
|
workbox.setConfig({modulePathPrefix: '/workbox'});
|
|
/**************************************************** END WORKBOX IMPORT ****************************************************/
|
|
|
|
|
|
|
|
function registerCacheFirst(routeMatchFn, cacheName, plugins = []) {
|
|
const strategy = new workbox.strategies.CacheFirst({ cacheName, plugins });
|
|
workbox.routing.registerRoute(routeMatchFn, strategy);
|
|
return strategy;
|
|
}
|
|
|
|
function precacheResources(strategy, resourceList, event) {
|
|
if (!(event instanceof ExtendableEvent)) {
|
|
throw new Error("precacheResources needs a valid ExtendableEvent");
|
|
}
|
|
return Promise.all(resourceList.map(path =>
|
|
strategy.handleAll({
|
|
event,
|
|
request: new Request(path),
|
|
})[1]
|
|
));
|
|
}
|
|
|
|
function createBackgroundSyncPlugin(queueName, maxRetentionTime = 2880, forceSyncFallback = false) {
|
|
return new workbox.backgroundSync.BackgroundSyncPlugin(queueName, {
|
|
maxRetentionTime,
|
|
forceSyncFallback
|
|
});
|
|
}
|
|
|
|
function createBackgroundSyncPluginWithBroadcast(queueName, channelName, maxRetentionTime = 2880, forceSyncFallback = false) {
|
|
const queue = new workbox.backgroundSync.Queue(queueName, {
|
|
maxRetentionTime,
|
|
forceSyncFallback,
|
|
});
|
|
|
|
const bc = new BroadcastChannel(channelName);
|
|
|
|
const replayQueueWithProgress = async () => {
|
|
let entry;
|
|
let successCount = 0;
|
|
let failureCount = 0;
|
|
const total = (await queue.getAll()).length;
|
|
|
|
while ((entry = await queue.shiftRequest())) {
|
|
try {
|
|
await fetch(entry.request.clone());
|
|
successCount++;
|
|
} catch (error) {
|
|
failureCount++;
|
|
await queue.unshiftRequest(entry);
|
|
throw error;
|
|
} finally {
|
|
const remaining = (await queue.getAll()).length;
|
|
bc.postMessage({ name: queueName, replaying: true, remaining });
|
|
}
|
|
}
|
|
|
|
const remaining = (await queue.getAll()).length;
|
|
bc.postMessage({
|
|
name: queueName,
|
|
replayed: true,
|
|
remaining,
|
|
successCount,
|
|
failureCount,
|
|
});
|
|
};
|
|
|
|
bc.onmessage = async (event) => {
|
|
if (event.data?.type === 'status-request') {
|
|
const entries = await queue.getAll();
|
|
bc.postMessage({ name: queueName, remaining: entries.length });
|
|
}
|
|
|
|
if (event.data?.type === 'replay-request') {
|
|
try {
|
|
await replayQueueWithProgress();
|
|
} catch (error) {
|
|
const entries = await queue.getAll();
|
|
bc.postMessage({
|
|
name: queueName,
|
|
replayed: false,
|
|
remaining: entries.length,
|
|
error: error.message,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
fetchDidFail: async ({ request }) => {
|
|
await queue.pushRequest({ request });
|
|
const entries = await queue.getAll();
|
|
bc.postMessage({ name: queueName, remaining: entries.length });
|
|
},
|
|
onSync: async () => {
|
|
await replayQueueWithProgress();
|
|
},
|
|
};
|
|
}
|
|
|
|
const messageTasks = [];
|
|
function registerMessageTask(callback) {
|
|
messageTasks.push(callback);
|
|
}
|
|
|
|
self.addEventListener('message', (event) => {
|
|
event.waitUntil(
|
|
messageTasks.reduce(
|
|
(chain, task) => chain.then(() => task(event)),
|
|
Promise.resolve()
|
|
)
|
|
);
|
|
});
|
|
|
|
const installTasks = [];
|
|
function registerInstallTask(callback, priority = 100) {
|
|
installTasks.push({
|
|
callback: (event) => {
|
|
const result = callback(event);
|
|
if (!result?.then) console.warn("Install task did not return a Promise");
|
|
return result;
|
|
},
|
|
priority,
|
|
});
|
|
}
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
installTasks
|
|
.sort((a, b) => a.priority - b.priority)
|
|
.reduce(
|
|
(chain, task) => chain.then(() => task.callback(event)),
|
|
Promise.resolve()
|
|
)
|
|
);
|
|
});
|
|
|
|
function statusGuard(min, max) {
|
|
return {
|
|
fetchDidSucceed: ({ response }) => {
|
|
if (response.status >= min && response.status <= max) {
|
|
throw new Error(`Server error: ${response.status}`);
|
|
}
|
|
return response;
|
|
}
|
|
};
|
|
}
|
|
|
|
registerMessageTask(async (event) => {
|
|
if (event.data?.type === 'SKIP_WAITING') {
|
|
await self.skipWaiting();
|
|
}
|
|
});
|
|
|
|
const usedCacheNames = new Set();
|
|
function registerCacheName(name) {
|
|
usedCacheNames.add(name);
|
|
return name;
|
|
}
|
|
|
|
async function openBackgroundFetchDatabase() {
|
|
return await self.idb.openDB('bgfetch-completed', 1, {
|
|
upgrade(db) {
|
|
if (!db.objectStoreNames.contains('files')) {
|
|
db.createObjectStore('files', { keyPath: 'id' });
|
|
}
|
|
|
|
if (!db.objectStoreNames.contains('chunks')) {
|
|
const store = db.createObjectStore('chunks', { keyPath: ['id', 'index'] });
|
|
store.createIndex('by-id', 'id');
|
|
} else {
|
|
const store = db.transaction.objectStore('chunks');
|
|
if (!Array.from(store.indexNames).includes('by-id')) {
|
|
store.createIndex('by-id', 'id');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const bgFetchMetadata = new Map();
|
|
const bgFetchChannel = new BroadcastChannel('bg-fetch');
|
|
bgFetchChannel.onmessage = async (event) => {
|
|
const { type, id, meta } = event.data || {};
|
|
|
|
switch (type) {
|
|
case 'register-meta':
|
|
bgFetchMetadata.set(id, meta);
|
|
break;
|
|
|
|
case 'get-meta':
|
|
const metadata = bgFetchMetadata.get(id) || null;
|
|
bgFetchChannel.postMessage({ type: 'meta-response', id, metadata });
|
|
break;
|
|
|
|
case 'clear-meta':
|
|
bgFetchMetadata.delete(id);
|
|
break;
|
|
|
|
case 'list-stored-files':
|
|
{
|
|
const db = await openBackgroundFetchDatabase();
|
|
const files = await db.getAll('files');
|
|
bgFetchChannel.postMessage({ type: 'stored-files', files });
|
|
break;
|
|
}
|
|
|
|
case 'delete-stored-file':
|
|
{
|
|
const name = event.data.name;
|
|
const db = await openBackgroundFetchDatabase();
|
|
const allFiles = await db.getAll('files');
|
|
const target = allFiles.find(f => f.name === name);
|
|
if (target) {
|
|
await db.delete('files', target.id);
|
|
let index = 0;
|
|
while (await db.get('chunks', [target.id, index])) {
|
|
await db.delete('chunks', [target.id, index++]);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
const backgroundFetchTasks = {
|
|
click: [],
|
|
success: [],
|
|
fail: [],
|
|
};
|
|
function registerBackgroundFetchTask(type, callback, priority = 100) {
|
|
if (!backgroundFetchTasks[type]) {
|
|
throw new Error(`Unknown background fetch event type: ${type}`);
|
|
}
|
|
|
|
backgroundFetchTasks[type].push({
|
|
callback: (event) => {
|
|
const result = callback(event);
|
|
if (!result?.then) {
|
|
console.warn(`[${type}] task did not return a Promise`);
|
|
}
|
|
return result;
|
|
},
|
|
priority,
|
|
});
|
|
}
|
|
|
|
function runBackgroundFetchTasks(type, event) {
|
|
const tasks = backgroundFetchTasks[type] ?? [];
|
|
return tasks
|
|
.sort((a, b) => a.priority - b.priority)
|
|
.reduce(
|
|
(chain, task) => chain.then(() => task.callback(event)),
|
|
Promise.resolve()
|
|
);
|
|
}
|
|
|
|
self.addEventListener('backgroundfetchclick', (event) => {
|
|
event.waitUntil(runBackgroundFetchTasks('click', event));
|
|
});
|
|
|
|
self.addEventListener('backgroundfetchsuccess', (event) => {
|
|
event.waitUntil(runBackgroundFetchTasks('success', event));
|
|
});
|
|
|
|
self.addEventListener('backgroundfetchfail', (event) => {
|
|
event.waitUntil(runBackgroundFetchTasks('fail', event));
|
|
});
|
|
|
|
|
|
const pushTasks = [];
|
|
function registerPushTask(callback) {
|
|
pushTasks.push(callback);
|
|
}
|
|
self.addEventListener('push', (event) => {
|
|
if (!(self.Notification && self.Notification.permission === 'granted')) {
|
|
return;
|
|
}
|
|
event.waitUntil(
|
|
pushTasks.reduce(
|
|
(chain, task) => chain.then(() => task(event)),
|
|
Promise.resolve()
|
|
)
|
|
);
|
|
});
|
|
|
|
const notificationActionHandlers = new Map();
|
|
function registerNotificationAction(actionName, handler) {
|
|
notificationActionHandlers.set(actionName, handler);
|
|
}
|
|
self.addEventListener('notificationclick', event => {
|
|
event.notification.close();
|
|
const action = event.action || "";
|
|
const promises = [];
|
|
|
|
const specificHandler = notificationActionHandlers.get(action);
|
|
if (typeof specificHandler === 'function') {
|
|
promises.push(Promise.resolve(specificHandler(event)));
|
|
}
|
|
|
|
const wildcardHandler = notificationActionHandlers.get('*');
|
|
if (typeof wildcardHandler === 'function') {
|
|
promises.push(Promise.resolve(wildcardHandler(event)));
|
|
}
|
|
|
|
if (promises.length > 0) {
|
|
event.waitUntil(Promise.all(promises));
|
|
}
|
|
});
|
|
const structuredPushNotificationSupport = (event) => {
|
|
const {data} = event;
|
|
const sendNotification = response => {
|
|
const {title, options} = JSON.parse(response);
|
|
return self.registration.showNotification(title, options);
|
|
};
|
|
|
|
if (data) {
|
|
const message = data.text();
|
|
event.waitUntil(sendNotification(message));
|
|
}
|
|
}
|
|
function simplePushNotificationSupport(event) {
|
|
const { data } = event;
|
|
|
|
if (!data) return;
|
|
|
|
const message = data.text();
|
|
const sendNotification = (text) => {
|
|
return self.registration.showNotification('Notification', {
|
|
body: text
|
|
});
|
|
};
|
|
|
|
event.waitUntil(sendNotification(message));
|
|
}
|
|
|
|
const periodicSyncTasks = new Map();
|
|
|
|
function registerPeriodicSyncTask(tag, callback, priority = 100) {
|
|
if (!periodicSyncTasks.has(tag)) {
|
|
periodicSyncTasks.set(tag, []);
|
|
}
|
|
periodicSyncTasks.get(tag).push({
|
|
priority,
|
|
callback: (event) => {
|
|
const result = callback(event);
|
|
if (!result?.then) console.warn(`[${tag}] periodic sync task did not return a Promise`);
|
|
return result;
|
|
}
|
|
});
|
|
}
|
|
|
|
async function runPeriodicSyncTasks(tag, event) {
|
|
const tasks = periodicSyncTasks.get(tag) ?? [];
|
|
return tasks
|
|
.sort((a, b) => a.priority - b.priority)
|
|
.reduce(
|
|
(chain, task) => chain.then(() => task.callback(event)),
|
|
Promise.resolve()
|
|
);
|
|
}
|
|
|
|
self.addEventListener('periodicsync', event => {
|
|
event.waitUntil(runPeriodicSyncTasks(event.tag, event));
|
|
});
|
|
|
|
const periodicChannel = new BroadcastChannel('periodic-sync');
|
|
|
|
function notifyPeriodicSyncClients(tag, payload = {}) {
|
|
periodicChannel.postMessage({
|
|
type: 'periodic-sync-update',
|
|
tag,
|
|
timestamp: Date.now(),
|
|
...payload
|
|
});
|
|
}
|
|
|
|
const cacheInstances = new Map();
|
|
|
|
async function openCache(name) {
|
|
if (!cacheInstances.has(name)) {
|
|
const cache = await caches.open(name);
|
|
cacheInstances.set(name, cache);
|
|
}
|
|
return cacheInstances.get(name);
|
|
}
|
|
|
|
|
|
|
|
/**************************************************** CACHE STRATEGY ****************************************************/
|
|
// Strategy: CacheFirst
|
|
// Match: ({url}) => url.pathname.startsWith('/assets')
|
|
// Cache Name: assets
|
|
// Enabled: 1
|
|
// Needs Workbox: 1
|
|
// Method:
|
|
|
|
// 1. Creation of the Workbox Cache Strategy object
|
|
// 2. Register the route with the Workbox Router
|
|
// 3. Add the assets to the cache when the service worker is installed
|
|
|
|
|
|
const cache_0_0 = new workbox.strategies.CacheFirst({
|
|
cacheName: registerCacheName('assets'),plugins: [new workbox.expiration.ExpirationPlugin({
|
|
"maxEntries": 68,
|
|
"maxAgeSeconds": 31536000
|
|
})]
|
|
});
|
|
workbox.routing.registerRoute(({url}) => url.pathname.startsWith('/assets'),cache_0_0);
|
|
registerInstallTask((event) => precacheResources(cache_0_0, [
|
|
"/assets/@spomky-labs/pwa-bundle/badge_controller-fxsMWke.js",
|
|
"/assets/@spomky-labs/pwa-bundle/connection-status_controller-a6qV4RW.js",
|
|
"/assets/@spomky-labs/pwa-bundle/abstract_controller-lHossdH.js",
|
|
"/assets/@spomky-labs/pwa-bundle/capture_controller-yP__Kii.js",
|
|
"/assets/@spomky-labs/pwa-bundle/install_controller-TRkeV12.js",
|
|
"/assets/@spomky-labs/pwa-bundle/prefetch-on-demand_controller-EGnyR3S.js",
|
|
"/assets/@spomky-labs/pwa-bundle/fullscreen_controller-cDz4gEY.js",
|
|
"/assets/@spomky-labs/pwa-bundle/barcode-detection_controller-2wM_rT7.js",
|
|
"/assets/@spomky-labs/pwa-bundle/web-share_controller-WHj4CRa.js",
|
|
"/assets/@spomky-labs/pwa-bundle/file-handling_controller-F0Y1cZK.js",
|
|
"/assets/@spomky-labs/pwa-bundle/device-orientation_controller-IHb4jim.js",
|
|
"/assets/@spomky-labs/pwa-bundle/touch_controller-mAGtlT0.js",
|
|
"/assets/@spomky-labs/pwa-bundle/battery_controller-1zvUlM8.js",
|
|
"/assets/@spomky-labs/pwa-bundle/service-worker_controller-rj3_R_-.js",
|
|
"/assets/@spomky-labs/pwa-bundle/network-information_controller-a36uKfa.js",
|
|
"/assets/@spomky-labs/pwa-bundle/backgroundsync-queue_controller-GzsTYhv.js",
|
|
"/assets/@spomky-labs/pwa-bundle/device-motion_controller-Vn1KsVn.js",
|
|
"/assets/@spomky-labs/pwa-bundle/vibration_controller-WWNgRJt.js",
|
|
"/assets/@spomky-labs/pwa-bundle/backgroundsync-form_controller-gK5yd8x.js",
|
|
"/assets/@spomky-labs/pwa-bundle/web-push_controller-czf9Gth.js",
|
|
"/assets/@spomky-labs/pwa-bundle/geolocation_controller-eLT6hhX.js",
|
|
"/assets/@spomky-labs/pwa-bundle/jwt_signer-rMDxy1b.js",
|
|
"/assets/@spomky-labs/pwa-bundle/speech-synthesis_controller-P10zBDz.js",
|
|
"/assets/@spomky-labs/pwa-bundle/presentation_controller-Ja4LWIU.js",
|
|
"/assets/@spomky-labs/pwa-bundle/receiver_controller-auggO9d.js",
|
|
"/assets/@spomky-labs/pwa-bundle/background-fetch_controller-RRspbNT.js",
|
|
"/assets/@spomky-labs/pwa-bundle/wake-lock_controller-AZuUNNo.js",
|
|
"/assets/@spomky-labs/pwa-bundle/picture-in-picture_controller-BzBogPR.js",
|
|
"/assets/@spomky-labs/pwa-bundle/helpers-jYq9oR_.js",
|
|
"/assets/@spomky-labs/pwa-bundle/index-2g1hp95.js",
|
|
"/assets/@spomky-labs/pwa-bundle/contact_controller--bz1Lhb.js",
|
|
"/assets/PaymentForm-9z-bqNC.js",
|
|
"/assets/admin-EgoTwn3.js",
|
|
"/assets/app-NCj0QLm.js"
|
|
], event));
|
|
/**************************************************** END CACHE STRATEGY ****************************************************/
|
|
|
|
|
|
|
|
|
|
|
|
/**************************************************** CACHE STRATEGY ****************************************************/
|
|
// Strategy: CacheFirst
|
|
// Match: ({request}) => request.destination === 'font'
|
|
// Cache Name: fonts
|
|
// Enabled: 1
|
|
// Needs Workbox: 1
|
|
// Method: GET
|
|
|
|
// 1. Creation of the Workbox Cache Strategy object
|
|
// 2. Register the route with the Workbox Router
|
|
// 3. Add the assets to the cache when the service worker is installed
|
|
|
|
|
|
|
|
|
|
const cache_2_0 = new workbox.strategies.CacheFirst({
|
|
cacheName: registerCacheName('fonts'),plugins: [new workbox.cacheableResponse.CacheableResponsePlugin({
|
|
"statuses": [
|
|
0,
|
|
200
|
|
]
|
|
}), new workbox.expiration.ExpirationPlugin({
|
|
"maxEntries": 60,
|
|
"maxAgeSeconds": 31536000
|
|
})]
|
|
});
|
|
workbox.routing.registerRoute(({request}) => request.destination === 'font',cache_2_0,'GET');
|
|
/**************************************************** END CACHE STRATEGY ****************************************************/
|
|
|
|
|
|
|
|
|
|
|
|
/**************************************************** CACHE STRATEGY ****************************************************/
|
|
// Strategy: StaleWhileRevalidate
|
|
// Match: ({url}) => url.origin === 'https://fonts.googleapis.com'
|
|
// Cache Name: google-fonts-stylesheets
|
|
// Enabled: 1
|
|
// Needs Workbox: 1
|
|
// Method:
|
|
|
|
// 1. Creation of the Workbox Cache Strategy object
|
|
// 2. Register the route with the Workbox Router
|
|
// 3. Add the assets to the cache when the service worker is installed
|
|
|
|
|
|
const cache_3_0 = new workbox.strategies.StaleWhileRevalidate({
|
|
cacheName: registerCacheName('google-fonts-stylesheets'),plugins: []
|
|
});
|
|
workbox.routing.registerRoute(({url}) => url.origin === 'https://fonts.googleapis.com',cache_3_0);
|
|
/**************************************************** END CACHE STRATEGY ****************************************************/
|
|
|
|
|
|
|
|
|
|
|
|
/**************************************************** CACHE STRATEGY ****************************************************/
|
|
// Strategy: CacheFirst
|
|
// Match: ({url}) => url.origin === 'https://fonts.gstatic.com'
|
|
// Cache Name: google-fonts-webfonts
|
|
// Enabled: 1
|
|
// Needs Workbox: 1
|
|
// Method:
|
|
|
|
// 1. Creation of the Workbox Cache Strategy object
|
|
// 2. Register the route with the Workbox Router
|
|
// 3. Add the assets to the cache when the service worker is installed
|
|
|
|
|
|
|
|
|
|
const cache_3_1 = new workbox.strategies.CacheFirst({
|
|
cacheName: registerCacheName('google-fonts-webfonts'),plugins: [new workbox.cacheableResponse.CacheableResponsePlugin({
|
|
"statuses": [
|
|
0,
|
|
200
|
|
]
|
|
}), new workbox.expiration.ExpirationPlugin({
|
|
"maxEntries": 30,
|
|
"maxAgeSeconds": 31536000
|
|
})]
|
|
});
|
|
workbox.routing.registerRoute(({url}) => url.origin === 'https://fonts.gstatic.com',cache_3_1);
|
|
/**************************************************** END CACHE STRATEGY ****************************************************/
|
|
|
|
|
|
|
|
|
|
|
|
/**************************************************** CACHE STRATEGY ****************************************************/
|
|
// Strategy: CacheFirst
|
|
// Match: ({request, url}) => (request.destination === 'image' && !url.pathname.startsWith('/assets'))
|
|
// Cache Name: images
|
|
// Enabled: 1
|
|
// Needs Workbox: 1
|
|
// Method:
|
|
|
|
// 1. Creation of the Workbox Cache Strategy object
|
|
// 2. Register the route with the Workbox Router
|
|
// 3. Add the assets to the cache when the service worker is installed
|
|
|
|
|
|
const cache_4_0 = new workbox.strategies.CacheFirst({
|
|
cacheName: registerCacheName('images'),plugins: []
|
|
});
|
|
workbox.routing.registerRoute(({request, url}) => (request.destination === 'image' && !url.pathname.startsWith('/assets')),cache_4_0);
|
|
/**************************************************** END CACHE STRATEGY ****************************************************/
|
|
|
|
|
|
|
|
|
|
/**************************************************** CACHE CLEAR ****************************************************/
|
|
// The configuration is set to clear the cache on each install event
|
|
// The following code will remove all the caches
|
|
registerInstallTask(() =>
|
|
caches.keys().then(keys =>
|
|
Promise.all(
|
|
keys
|
|
.filter(k => usedCacheNames.has(k))
|
|
.map(k => caches.delete(k))
|
|
)
|
|
)
|
|
, 0);
|
|
/**************************************************** END CACHE CLEAR ****************************************************/
|
|
|
|
|
|
|
|
|
|
|
|
/**************************************************** SKIP WAITING ****************************************************/
|
|
// The configuration is set to skip waiting on each install event
|
|
registerInstallTask(() => self.skipWaiting(), 5);
|
|
self.addEventListener("activate", function (event) {
|
|
event.waitUntil(self.clients.claim());
|
|
});
|
|
/**************************************************** END SKIP WAITING ****************************************************/
|
|
|
|
|
|
|