PageSourceSearch

https://www.wrocklage.de/push-sw.js

js wrocklage.de collected 2026-10-05 18:44:11 UTC 11,127 bytes, 243 lines download raw bytes

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.