1/** 2 * Service Worker fuer die Zustellung an Abwesende. 3 * 4 * =================================================================== 5 * WARUM DAS NICHT IN sw.js STEHT 6 * =================================================================== 7 * 8 * public/sw.js ist die Offline-Ablage (Workbox) und wird nur unter engen 9 * Bedingungen ueberhaupt angemeldet: Registry-Schalter, App- oder 10 * Bildschirmseite, Seite als offline verfuegbar markiert. Push braucht das 11 * Gegenteil â eine Anmeldung auf JEDER Seite, fuer jeden angemeldeten 12 * Mitarbeiter. 13 * 14 * Beides in eine Datei zu legen haette zwei Auswege gehabt, und beide sind 15 * falsch: 16 * 17 * sw.js breiter anmelden -> schaltet die Zwischenspeicherung fuer alle ein. 18 * Das aendert die Auslieferung jeder Seite, und 19 * zwar als Nebenwirkung einer Benachrichtigung. 20 * zweiten Worker auf / -> es kann nur einen Geltungsbereich / geben. Der 21 * zweite verdraengt den ersten, die Offline-Ablage 22 * waere still weg. Niemand haette es gemerkt, 23 * ausser den Leuten ohne Netz. 24 * 25 * Deshalb dieser Worker mit dem eigenen Geltungsbereich /indice-push/. Unter 26 * diesem Pfad liegt keine Seite, er kontrolliert also nichts und faengt keine 27 * Anfrage ab. Das ist kein Trick, sondern die vorgesehene Trennung: Ein 28 * push-Ereignis geht an die ANMELDUNG, nicht an die von ihr kontrollierten 29 * Seiten. Ein Worker ohne eine einzige kontrollierte Seite empfaengt es genauso. 30 * 31 * Folge fuers Weiterklicken: clients.matchAll() muss includeUncontrolled setzen, 32 * sonst findet dieser Worker die offenen Fenster nicht â sie gehoeren ihm ja 33 * nicht. 34 */ 35 36'use strict'; 37 38var URL_ABO = '/api/push/abo/setzen'; 39var URL_STAND = '/api/push/stand'; 40 41// Ohne Wartezeit uebernehmen. Ein Worker, der nur Benachrichtigungen zustellt, 42// hat keinen halben Zustand, den ein Wechsel zerreissen koennte. 43self.addEventListener('install', function () { self.skipWaiting(); }); 44self.addEventListener('activate', function (e) { e.waitUntil(self.clients.claim()); }); 45 46/** 47 * Eine Meldung ist eingetroffen. 48 * 49 * Die Nutzlast ist knapp geschlusselt, weil sie verschluesselt in eine Anfrage 50 * begrenzter Groesse passen muss â die Namen stehen in src/Push/Meldung.php und 51 * muessen dort und hier zusammen geaendert werden. 52 * 53 * t Titel b Text u Ziel g Marke z Zaehler i Bild s bleibt stehen 54 * d Daten (knappe Angaben der ausloesenden App, siehe melde()) 55 */ 56self.addEventListener('push', function (ereignis) { 57 var d = {}; 58 try { d = ereignis.data ? ereignis.data.json() : {}; } catch (e) { d = {}; } 59 60 var titel = d.t || 'Neue Benachrichtigung'; 61 var opt = { 62 body: d.b || '', 63 tag: d.g || 'indice', 64 // Ohne renotify bliebe eine ersetzte Meldung stumm â die zweite 65 // Nachricht in derselben Unterhaltung wuerde niemand bemerken. 66 renotify: !!d.g, 67 // Die Daten reisen bis zum Klick mit. Sie sind KEIN Uebertragungsweg â 68 // die Zielseite holt ihren Inhalt selbst â sondern ersparen ihr das 69 // Raten, welcher Datensatz gemeint war (siehe melde(), Parameter daten). 70 data: { url: d.u || '/', daten: d.d || null }, 71 // Ohne das verschwindet die Einblendung nach etwa fuenf Sekunden von 72 // selbst. Fuer eine Probe ist das fatal: Wer sie im Terminal ausloest 73 // und dann zum Bildschirm schaut, hat sie verpasst und haelt das 74 // Ergebnis fuer âkommt nichts an". 75 requireInteraction: !!d.s, 76 timestamp: Date.now() 77 }; 78 if (d.i) { opt.icon = d.i; opt.badge = d.i; } 79 80 ereignis.waitUntil( 81 self.registration.showNotification(titel, opt).then(function () { 82 // Zaehler am Programmsymbol. Nur sichtbar, wenn der Auftritt als App 83 // installiert ist; sonst folgenlos. 84 if (self.navigator && self.navigator.setAppBadge) { 85 var z = parseInt(d.z, 10) || 0; 86 return z > 0 ? self.navigator.setAppBadge(z) : self.navigator.clearAppBadge(); 87 } 88 }).catch(function (fehler) { 89 // NIE stillschweigend scheitern. 90 // 91 // Ein verschluckter Fehler an dieser Stelle ist der Grund, warum 92 // âes kommt nichts an" bei Web Push so schwer zu finden ist: Der 93 // Zustelldienst meldet Erfolg, der Server sieht 201, und im Browser 94 // passiert nichts â ohne eine einzige Spur. Deshalb erstens die 95 // Meldung in der Entwicklerkonsole des Workers, zweitens ein 96 // Rueckfall auf die einfachste denkbare Anzeige. 97 // 98 // Der Rueckfall ist nicht nur Hoeflichkeit: userVisibleOnly 99 // verpflichtet uns, jede Zustellung sichtbar zu machen. Wer das 100 // wiederholt verletzt, dem entzieht Chrome die Zustellung ganz. 101 console.error('[push] Meldung nicht anzeigbar:', fehler, d); 102 103 return self.registration.showNotification(titel, { body: opt.body }); 104 }) 105 ); 106}); 107 108/** 109 * Angeklickt: vorhandenes Fenster nach vorn holen statt ein neues zu oeffnen. 110 * 111 * Wer schon einen Reiter mit dem Auftritt offen hat, will genau diesen sehen â 112 * mit seinem Anmeldezustand und seinem Verlauf. Ein zweiter Reiter daneben 113 * waere jedes Mal ein kleines Aergernis. 114 * 115 * =================================================================== 116 * WARUM HIER KEIN WindowClient.navigate() STEHT 117 * =================================================================== 118 * 119 * Naheliegend waere f.navigate(ziel) â und es funktioniert hier nicht. Die Norm 120 * erlaubt navigate() ausschliesslich fuer Fenster, die DIESER Worker 121 * kontrolliert; fuer alle anderen weist der Aufruf mit einem TypeError ab. 122 *
123 * Genau das ist bei uns immer der Fall: Der Worker laeuft auf dem 124 * Geltungsbereich /indice-push/, unter dem keine Seite liegt â bewusst, damit 125 * er sw.js und dessen Offline-Ablage nicht verdraengt (siehe Kopf dieser 126 * Datei). Er kontrolliert also nie ein Fenster. Die Entscheidung, die die 127 * Offline-Funktion rettet, nimmt uns navigate() weg. 128 * 129 * postMessage kennt diese Einschraenkung nicht. Der Worker holt das Fenster 130 * nach vorn und sagt ihm, wohin es soll; das Springen macht die Seite selbst 131 * (public/js/sys/push.js). Nebenwirkung, die sogar besser ist: Innerhalb 132 * derselben Seite wird nur die Sprungmarke gewechselt, es wird nichts neu 133 * geladen. 134 */ 135self.addEventListener('notificationclick', function (ereignis) { 136 ereignis.notification.close(); 137 138 var inhalt = ereignis.notification.data || {}; 139 var ziel = inhalt.url || '/'; 140 141 ereignis.waitUntil( 142 self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (fenster) { 143 var zielUrl = new URL(ziel, self.location.origin); 144 145 // Erster Vorrang: ein Fenster, das BEREITS auf der Zielseite steht. 146 // Nur dort ist der Zuruf ein Gewinn â die Seite wechselt dann bloss 147 // die Sprungmarke und laedt nichts neu. 148 for (var i = 0; i < fenster.length; i++) { 149 var f = fenster[i]; 150 var offen = new URL(f.url); 151 152 if (offen.origin !== zielUrl.origin || offen.pathname !== zielUrl.pathname) continue; 153 154 // focus() darf hier aufgerufen werden: Ein Klick auf eine 155 // Benachrichtigung gilt als Zutun des Benutzers. 156 return f.focus().then(function (g) { 157 (g || f).postMessage({ 158 indicePush: 'ziel', 159 url: zielUrl.href, 160 daten: inhalt.daten || null 161 }); 162 }).catch(function () { 163 return self.clients.openWindow(zielUrl.href); 164 }); 165 } 166 167 // Alles andere geht ueber openWindow â auch dann, wenn ein Fenster 168 // des Auftritts offen ist, es aber eine ANDERE Seite zeigt. 169 // 170 // Der naheliegende Weg waere, dieses Fenster nach vorn zu holen und 171 // ihm das Ziel zuzurufen. Das setzt aber voraus, dass die dortige 172 // Seite den Zuruf annimmt â also dass sie push.js geladen hat und 173 // dessen Fassung aktuell ist. Diese Annahme ist falsch: Es gibt 174 // Apps ohne scripts-styles, eingebettete Ansichten und Seiten aus 175 // dem Zwischenspeicher. Schlaegt sie fehl, passiert GAR NICHTS -- 176 // der Benutzer tippt auf eine Meldung und sieht weiter seine 177 // Mail-App. Genau so ist es aufgefallen. 178 // 179 // openWindow braucht keine Mitwirkung der Zielseite. In der 180 // installierten App navigiert es das App-Fenster (die Adresse liegt 181 // im Geltungsbereich des Manifests), am Schreibtisch oeffnet es 182 // einen Reiter. Beides ist besser als nichts -- und ein fremdes 183 // Fenster, das gerade etwas anderes zeigt, wegzunavigieren waere 184 // ohnehin unhoeflich gewesen. 185 return self.clients.openWindow(zielUrl.href); 186 }) 187 ); 188}); 189 190/** 191 * Der Browser hat das Abonnement von sich aus erneuert. 192 * 193 * Passiert selten und unangekuendigt â ohne diese Behandlung waere das Geraet 194 * danach still, und niemand haette einen Anhaltspunkt, warum. Die Ereignisdaten 195 * sind je nach Browser unvollstaendig, deshalb wird der Schluessel notfalls 196 * frisch geholt und neu abonniert. 197 */ 198self.addEventListener('pushsubscriptionchange', function (ereignis) { 199 ereignis.waitUntil((function () { 200 var neu = ereignis.newSubscription; 201 if (neu) { return melden(neu); } 202 203 return fetch(URL_STAND, { 204 method: 'POST', credentials: 'same-origin', 205 headers: { 'Content-Type': 'application/json' }, body: '{}' 206 }) 207 .then(function (r) { return r.json(); }) 208 .then(function (a) { 209 var schluessel = a && a.result && a.result.schluessel; 210 if (!schluessel) { return; } 211 212 return self.registration.pushManager.subscribe({ 213 userVisibleOnly: true, 214 applicationServerKey: rohSchluessel(schluessel) 215 }).then(melden); 216 }) 217 .catch(function () {}); 218 })()); 219}); 220 221function melden(abo) { 222 var j = abo.toJSON ? abo.toJSON() : {}; 223 224 return fetch(URL_ABO, { 225 method: 'POST', 226 credentials: 'same-origin', 227 headers: { 'Content-Type': 'application/json' }, 228 body: JSON.stringify({ 229 endpunkt: abo.endpoint, 230 p256dh: (j.keys && j.keys.p256dh) || '', 231 auth: (j.keys && j.keys.auth) || '' 232 }) 233 }).catch(function () {}); 234} 235 236/** base64url -> Uint8Array; so und nicht anders will subscribe() den Schluessel. */ 237function rohSchluessel(text) { 238 var voll = (text + '='.repeat((4 - text.length % 4) % 4)).replace(/-/g, '+').replace(/_/g, '/'); 239 var roh = self.atob(voll); 240 var feld = new Uint8Array(roh.length); 241 for (var i = 0; i < roh.length;
241 i++) { feld[i] = roh.charCodeAt(i); } 242 return feld; 243}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.