1// indexHash: 2484548a7aa5a0126585a2668b208ef1fdf41869 2// use our own cache for /api/config and /api/config/theme 3// since ngsw doesn't allow dynamic cache updates 4const CACHE_NAME = 'ihub-cache'; 5const CACHE_ORIGINS = ['https://fonts.googleapis.com', 'https://fonts.gstatic.com', 'https://ih-cdn.ihub.app']; 6const CACHE_ICONS_REGEX = 7 /https:\/\/(az720171\.vo\.msecnd\.net|az601583\.vo\.msecnd\.net|cainspirehub\.azureedge\.net)\/app-content\/icon\/.*/i; 8// add the fetch handler before ngsw so our code can intercept the fetch event before ngsw can 9self.addEventListener('fetch', (event) => { 10 const { request } = event; 11 12 // check if this is a get request to /api/config or /api/config/theme 13 // The (\?.*)? suffix allows versioned URLs like /api/config/theme?v=hash 14 // The version param acts as the cache key discriminator â 15 // a new hash means a cache miss, forcing a fresh network fetch. 16 if ( 17 request.method === 'GET' && 18 request.url.startsWith(self.location.origin) && 19 request.url.match(/\/api\/(config|config\/theme)(\?.*)?$/i) 20 ) { 21 // handle the request with our own cache 22 event.respondWith( 23 (async function () { 24 // if the request cache directive is set to reload then skip cache and go to network 25 // store the response in cache if it's ok 26 switch (request.cache) { 27 case 'reload': 28 // get the response from the network 29 // add cache busting to the request so we can bypass the browser and cdn cache 30 const newUrl = new URL(request.url); 31 newUrl.searchParams.set('d', Date.now()); 32 33 var networkRequest = new Request(newUrl, request); 34 // get the network response 35 const networkResponse = await fetch(networkRequest); 36 37 // open the cache 38 const cache = await caches.open(CACHE_NAME).catch((err) => { 39 console.error('Failed to open cache', err); 40 return null; 41 }); 42 // if the cache failed to open then just return the network response 43 if (!cache) { 44 return networkResponse; 45 } 46 47 // modify the request to remove the cache busting (d param) from request.url 48 const cacheRequest = new Request(request.url, request); 49 50 if (networkResponse.ok) { 51 // store the response in cache and use a clone in case cache is empty and the network response was used 52 await cache 53 .put(cacheRequest, networkResponse.clone()) 54 .catch((err) => console.error('Failed to put cache', err)); 55 56 return networkResponse; 57 } 58 // fallback to cache if network fails 59 const cachedResponse = await cache.match(cacheRequest).catch((err) => { 60 console.error('Failed to match cache', err); 61 return null; 62 }); 63 // return the cached value if available otherwise wait for the network response; 64 return cachedResponse || networkResponse; 65 66 case 'no-store': 67 return fetch(request); 68 } 69 70 // Default: stale-while-revalidate. Returns cached response instantly 71 // (zero latency) while revalidating in the background. This is the 72 // hot path for page-load theme fetches â the versioned URL (?v=hash) 73 // ensures a cache miss when the theme actually changes. 74 // open the cache for writing 75 const cache = await caches.open(CACHE_NAME).catch((err) => { 76 console.error('Failed to open cache', err); 77 return null; 78 }); 79 // if the cache failed to open then just return the network response 80 if (!cache) { 81 return fetch(request); 82 } 83 84 // try to load the request from cache 85 const cachedResponse = await cache.match(request).catch((err) => { 86 console.error('Failed to match cache', err); 87 return null; 88 }); 89 // prep the network response 90 const networkResponsePromise = fetch(request); 91 92 // tell the fetch event to stay open 93 // until the network request is also done since we can respond immediately with the cached value 94 event.waitUntil( 95 (async function () { 96 // get the response from the network 97 const networkResponse = await networkResponsePromise; 98 99 if (networkResponse.ok) { 100 // store the response in cache and use a clone in case cache is empty and the network response was used 101 await cache 102 .put(request, networkResponse.clone()) 103 .catch((err) => console.error('Failed to put cache', err)); 104 } 105 })() 106 ); 107 108 // return the cached value if available otherwise wait for the network response; 109 return cachedResponse || networkResponsePromise; 110 })() 111 ); 112 113 return; 114 } 115 116 // check if ngsw should intercept the request 117 if ( 118 // if this is a navigation request 119 request.mode === 'navigate' || 120 // or this is a get request 121 (request.method === 'GET' && 122 // and the request is not local 123 ((request.url.startsWith(self.location.origin) && 124 // and the request is for something in the cache 125 request.url.match( 126 /\/(manifest.webmanifest|favicon.ico|home|index.html|[a-z0-9]+\.(css|js|woff|eot|ttf|svg|woff2)|)$/i 127 )) ||
128 // and the origin is enabled for caching 129 CACHE_ORIGINS.indexOf(request.url) === 0)) || 130 CACHE_ICONS_REGEX.test(request.url) 131 ) { 132 // fetch event will be processed by ngsw as normal 133 return; 134 } 135 136 // if the url is for: 137 // "https://ih-cdn.ihub.app/**/*.js", 138 // "https://ih-cdn.ihub.app/**/*.css", 139 // "https://ih-cdn.ihub.app/**/*.woff", 140 // "https://ih-cdn.ihub.app/**/*.eot", 141 // "https://ih-cdn.ihub.app/**/*.ttf", 142 // "https://ih-cdn.ihub.app/**/*.svg", 143 // "https://ih-cdn.ihub.app/**/*.woff2" 144 // then allow it to be processed by ngsw so it can be cached 145 if ( 146 request.url.match(/https:\/\/ih-cdn\.ihub\.app\/.*\.(js|css|woff|eot|ttf|svg|woff2)$/i) || 147 // or the request is the local origin 148 request.url.startsWith(self.location.origin) 149 ) { 150 return; 151 } 152 153 // don't allow the ngsw to intercept this fetch event 154 event.stopImmediatePropagation(); 155}); 156 157// set global to serviceworker so emoji-js won't choke on init 158global = self; 159 160// Self-hosted (same-origin) rather than cdnjs.cloudflare.com (IHUB-17205). 161// importScripts() is synchronous and re-runs on every worker (re)start â a 162// routine event, since an idle worker is terminated by the browser after 163// ~30s and respawns on the next fetch. Per spec, if any one script here 164// fails to load, the ENTIRE worker fails to start, not just the 165// firebase/he/emoji-js-dependent push-notification code below â ngsw and 166// all its offline/caching support goes down with it. The four 167// cdnjs.cloudflare.com URLs this used to import were never same-origin and 168// never listed in ngsw-config.json's asset groups, so they depen
168ded 169// entirely on ordinary HTTP cache surviving disk-pressure eviction, 170// private/incognito sessions, and Chrome's per-site cache partitioning â 171// none of which self-hosting has to worry about. Vendored copies live in 172// App/assets/vendor/, copied from node_modules at build time (see 173// App.csproj's CopyServiceWorkerVendorAssets* targets) so bumping a 174// package.json version keeps them current instead of drifting from a 175// hand-pinned CDN version â and are additionally listed in 176// ngsw-config.json's service-worker-vendor lazy asset group so they 177// persist in Cache Storage, not just ordinary HTTP cache, once fetched. 178importScripts( 179 'ngsw-worker.js', 180 '/assets/vendor/firebase-app-compat.js', 181 '/assets/vendor/firebase-messaging-compat.js', 182 '/assets/vendor/he.js', 183 '/assets/vendor/emoji.min.js' 184); 185 186let emoji; 187self.addEventListener('message', function (event) { 188 switch (event.data.action) { 189 case 'reset_cache': 190 return caches 191 .keys() 192 .then(function (keyList) { 193 const precacheKey = 'ihubapp-precache-' + self.registration.scope; 194 return Promise.all( 195 keyList.map(function (key) { 196 if (key === precacheKey || key === 'ihubapp-static-resources') { 197 return caches.delete(key); 198 } 199 200 return Promise.resolve(); 201 }) 202 ); 203 }) 204 .then(function () { 205 console.log('Serviceworker cache wiped'); 206 event.ports[0].postMessage('complete'); 207 208 return self.registration 209 .unregister() 210 .then(() => self.clients.matchAll()) 211 .then((clients) => { 212 clients.forEach((client) => client.navigate(client.url)); 213 }); 214 }) 215 .catch(function (err) { 216 console.error('Reset cache failed', err); 217 event.ports[0].postMessage('error', err); 218 }); 219 } 220}); 221 222if ('PushManager' in this) { 223 // initialize firebase 224 firebase.initializeApp({ 225 apiKey: 'AIzaSyCHhrP8cbemEhpGMKE2L8Uigl6PDVde4oE', 226 authDomain: 'quantum-gearbox-557.firebaseapp.com', 227 databaseURL: 'https://quantum-gearbox-557.firebaseio.com', 228 projectId: 'quantum-gearbox-557', 229 storageBucket: 'quantum-gearbox-557.appspot.com', 230 messagingSenderId: '261205718171', 231 appId: '1:261205718171:web:3a57afd49efd33e2e8423e', 232 vapid: 'BI5DtqI3h8Db7NQGHawXFQ1TEpKN8GPizj_QN0U2b_WzhNY39ctC4L_Q4z1u6k6sC709W2hYcDZpt5j1UnB37-g' 233 }); 234 235 const messaging = firebase.messaging(); 236 237 messaging.onBackgroundMessage(function (payload) { 238 if (!emoji) { 239 emoji = new EmojiConvertor(); 240 emoji.replace_mode = 'unified'; 241 } 242 243 console.log('Received push notification', payload); 244 245 // replace emoticons but skip URLs by splitting the text into segments and only replacing emoticons in non-URL segments 246 const replaceEmoticonsSkipURLs = (text) => { 247 if (!text) return text; 248 249 // Regex pattern to match URLs 250 const urlPattern = /https?:\/\/[^\s]+/g; 251 // array to store segments 252 const segments = []; 253 254 let lastIndex = 0; 255 256 // Iterate through the text and identify URLs 257 text.replace(urlPattern, (match, index) => { 258 // Add non-URL text from lastIndex to the start of the URL 259 segments.push(text.substring(lastIndex, index)); 260 261 // Add URL 262 segments.push(match); 263 264 // Update lastIndex 265 lastIndex = index + match.length; 266 }); 267 268 // Add any remaining non-URL text to segments 269 if (lastIndex < text.length) { 270 segments.push(text.substring(lastIndex)); 271 } 272 273 // Replace emoticons only in non-URL segments 274 for (let i = 0; i < segments.length; i++) { 275 if (!segments[i].match(urlPattern)) { 276 segments[i] = emoji.replace_emoticons(segments[i]); 277 } 278 } 279 280 // Rejoin segments to get final text 281 return segments.join(''); 282 }; 283 284 // Replace emoji in body and title but skip URLs 285 payload.data.title = he.decode(replaceEmoticonsSkipURLs(payload.data?.title)); 286 payload.data.body = payload.data?.body ? he.decode(replaceEmoticonsSkipURLs(payload.data?.body)) : undefined; 287 if (payload.notification) { 288 payload.notification.title = he.decode(replaceEmoticonsSkipURLs(payload.notification?.title)); 289 payload.notification.body = he.decode(replaceEmoticonsSkipURLs(payload.notification?.body)); 290 291 payload.data = { 292 ...payload.data, 293 ...payload.notification 294 }; 295 } 296 297 const options = { 298 body: payload.data.body, 299 icon: payload.data.icon, 300 image: payload.data.image, 301 tag: payload.data.tag, 302 requireInteraction: true, 303 data: payload.data 304 }; 305 306 return self.registration.showNotification(payload.data.title, options); 307 }); 308 309 self.addEventListener( 310 'notificationclick', 311 function (event) { 312 const data = event.notification.data; 313 314 event.notification.close(); 315 316 if (event.action) { 317 console.error('User attempted an unsupported action', event.action); 318 } else { 319 event.waitUntil( 320 clients 321 .matchAll({ 322 includeUncontrolled: true, 323 type: 'window' 324 }) 325 .then(function (activeClients) { 326 // Validate click_action to prevent open redirect attacks 327 let appUrl = '/home'; 328 const clickAction = data?.click_action?.trim(); 329 if (clickAction) { 330 // Allow relative URLs or same-origin absolute URLs only 331 // Explicitly reject protocol-relative URLs (//evil.com/path) 332 if (clickAction.startsWith('/') && !clickAction.startsWith('//')) { 333 appUrl = clickAction; 334 } else { 335 try { 336 const url = new URL(clickAction); 337 if (url.origin === self.location.origin) { 338 appUrl = clickAction; 339 } 340 } catch (e) { 341 // Invalid URL, use default 342 } 343 } 344 } 345 346 if (activeClients.length > 0) { 347 activeClients[0].navigate(appUrl); 348 activeClients[0].focus(); 349 } else { 350 clients.openWindow(appUrl); 351 } 352 }) 353 ); 354 } 355 }, 356 false 357 ); 358 359 self.addEventListener('notificationclose', (e) => console.log('notification closed', e)); 360}
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.