1const normalizeNotificationUrl = (value) => { 2 try { 3 const url = new URL(String(value || '/'), self.location.origin); 4 return url.origin === self.location.origin 5 ? url.href 6 : self.location.origin; 7 } catch { 8 return self.location.origin; 9 } 10}; 11 12const getNotificationId = (value) => { 13 const notificationId = Number(value); 14 return Number.isSafeInteger(notificationId) && notificationId > 0 15 ? notificationId 16 : null; 17}; 18 19const withNotificationId = (value, notificationId) => { 20 const url = new URL(value, self.location.origin); 21 url.searchParams.set('notificationId', String(notificationId)); 22 return url.href; 23}; 24 25const BROWSER_PUSH_ACTION_PARAM = 'browserPushAction'; 26const MAX_BROWSER_PUSH_ACTIONS = 2; 27 28const getSupportedNotificationActionCount = () => { 29 if (typeof Notification === 'undefined') return 0; 30 const maxActions = Number(Notification.maxActions); 31 return Number.isInteger(maxActions) && maxActions >= 0 32 ? Math.min(maxActions, MAX_BROWSER_PUSH_ACTIONS) 33 : 0; 34}; 35 36const normalizeNotificationTargetUrl = (value) => { 37 try { 38 const raw = String(value || '').trim(); 39 if (!raw) return null; 40 const url = new URL(raw, self.location.origin); 41 return url.protocol === 'http:' || url.protocol === 'https:' ? url : null; 42 } catch { 43 return null; 44 } 45}; 46 47const normalizeNotificationActions = (value) => { 48 if (!Array.isArray(value)) return []; 49 50 return value 51 .slice(0, MAX_BROWSER_PUSH_ACTIONS) 52 .flatMap((candidate, index) => { 53 if (!candidate || typeof candidate !== 'object') return []; 54 const title = String(candidate.title || '').trim(); 55 const url = normalizeNotificationTargetUrl(candidate.url); 56 if (!title || !url) return []; 57 58 const promoCode = String(candidate.promoCode || '').trim(); 59 const rawPromoTimeout = Number(candidate.promoCodeTimeOut); 60 const promoCodeTimeOut = 61 Number.isInteger(rawPromoTimeout) && 62 rawPromoTimeout >= 1 && 63 rawPromoTimeout <= 43_200 64 ? rawPromoTimeout 65 : null; 66 return [ 67 { 68 action: `cta-${index}`, 69 promoCode, 70 promoCodeTimeOut, 71 title, 72 url: url.href, 73 }, 74 ]; 75 }); 76}; 77 78const withNotificationAction = (value, action, notificationId) => { 79 const clickUrl = new URL(value, self.location.origin); 80 clickUrl.searchParams.set(BROWSER_PUSH_ACTION_PARAM, action); 81 if (notificationId) { 82 clickUrl.searchParams.set('notificationId', String(notificationId)); 83 } 84 return clickUrl.href; 85}; 86 87const isSameOriginWindow = (client) => { 88 try { 89 return new URL(client.url).origin === self.location.origin; 90 } catch { 91 return false; 92 } 93}; 94 95const IMAGE_PRELOAD_TIMEOUT_MS = 8_000; 96const normalizeNotificationImageUrl = (value) => { 97 if (!value) return undefined; 98 99 try { 100 const url = new URL(String(value), self.location.origin); 101 return url.protocol === 'http:' || url.protocol === 'https:' 102 ? url.href 103 : undefined; 104 } catch { 105 return undefined; 106 } 107}; 108 109const getBrowserPushBrand = () => { 110 const hostname = new URL(self.location.origin).hostname.toLowerCase(); 111 const isSkala = 112 hostname === 'skalaskins.com' || hostname.endsWith('.skalaskins.com'); 113 114 return { 115 icon: new URL( 116 isSkala 117 ? '/images/skalaskins.com/favicon.svg' 118 : '/images/cscase.com/favicon.svg', 119 self.location.origin, 120 ).href, 121 title: isSkala ? 'SkalaSkins' : 'CSCASE', 122 }; 123}; 124 125const preloadNotificationImage = async (url) => { 126 const controller = new AbortController(); 127 const timeout = setTimeout( 128 () => controller.abort(), 129 IMAGE_PRELOAD_TIMEOUT_MS, 130 ); 131 132 try { 133 const isCrossOrigin = 134 new URL(url, self.location.origin).origin !== self.location.origin; 135 const response = await fetch(url, { 136 cache: 'force-cache', 137 mode: isCrossOrigin ? 'no-cors' : 'same-origin', 138 signal: controller.signal, 139 }); 140 const isOpaqueResponse = response.type === 'opaque'; 141 if (!response.ok && !isOpaqueResponse) { 142 throw new Error('Notification image request failed'); 143 } 144 145 if (!isOpaqueResponse && typeof createImageBitmap === 'function') { 146 const blob = await response.blob(); 147 const bitmap = await createImageBitmap(blob); 148 bitmap.close?.(); 149 } 150 } finally { 151 clearTimeout(timeout); 152 } 153}; 154 155const BROWSER_PUSH_WORKER_VERSION = 6; 156const VERIFICATION_ATTEMPT_PATTERN = /^[A-Za-z0-9_-]{16,128}$/; 157const VERIFICATION_CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{32,128}$/; 158const MAX_VERIFICATION_MESSAGE_LENGTH = 160; 159const hasControlCharacter = (value) => 160 Array.from(value).some((character) => { 161 const codePoint = character.codePointAt(0) || 0; 162 return codePoint <= 31 || codePoint === 127; 163 }); 164 165self.addEventListener('install', (event) => {
166 event.waitUntil(self.skipWaiting()); 167}); 168 169self.addEventListener('activate', (event) => { 170 event.waitUntil(self.clients.claim()); 171}); 172 173self.addEventListener('message', (event) => { 174 if (event.data?.type !== 'browserPushCapabilityRequest') return; 175 176 const response = { 177 type: 'browserPushCapability', 178 version: BROWSER_PUSH_WORKER_VERSION, 179 }; 180 const responsePort = event.ports?.[0]; 181 if (responsePort) responsePort.postMessage(response); 182 else event.source?.postMessage(response); 183}); 184 185self.addEventListener('push', (event) => { 186 if (!event.data) return; 187 188 let payload; 189 try { 190 payload = event.data.json(); 191 } catch { 192 payload = { body: event.data.text() }; 193 } 194 195 const verificationAttemptId = String( 196 payload.verificationAttemptId || payload.attemptId || '', 197 ); 198 const verificationChallenge = String(payload.challenge || ''); 199 const verificationMessage = String(payload.verificationMessage || '').trim(); 200 if ( 201 payload.type === 'browserPushVerification' && 202 VERIFICATION_ATTEMPT_PATTERN.test(verificationAttemptId) && 203 VERIFICATION_CHALLENGE_PATTERN.test(verificationChallenge) && 204 verificationMessage && 205 verificationMessage.length <= MAX_VERIFICATION_MESSAGE_LENGTH && 206 !hasControlCharacter(verificationMessage) 207 ) { 208 const brand = getBrowserPushBrand(); 209 event.waitUntil( 210 Promise.all([ 211 self.registration.showNotification(brand.title, { 212 body: verificationMessage, 213 data: { 214 type: 'browserPushVerification', 215 attemptId: verificationAttemptId, 216 url: self.location.origin, 217 }, 218 icon: brand.icon, 219 tag: 'browser-push-verification', 220 renotify: false, 221 }), 222 self.clients 223 .matchAll({ type: 'window', includeUncontrolled: true }) 224 .then((windows) => { 225 windows.forEach((client) => { 226 client.postMessage({ 227 type: 'browserPushVerification', 228 attemptId: verificationAttemptId, 229 challenge: verificationChallenge, 230 }); 231 }); 232 }), 233 ]), 234 ); 235 return; 236 } 237 238 event.waitUntil( 239 (async () => { 240 const brand = getBrowserPushBrand(); 241 const image = normalizeNotificationImageUrl(payload.image); 242 const icon = normalizeNotificationImageUrl(brand.icon); 243 const badge = 244 normalizeNotificationImageUrl(payload.badge) ?? 245 normalizeNotificationImageUrl(brand.icon); 246 const notificationTitle = 247 String(payload.title || '').trim() || brand.title; 248 const actions = normalizeNotificationActions(payload.actions); 249 const supportedActionCount = getSupportedNotificationActionCount(); 250 const visibleActions = actions.slice(0, supportedActionCount); 251 const defaultAction = 252 supportedActionCount === 0 ? (actions[0]?.action ?? null) : null; 253 const body = String(payload.body || '').trim(); 254 const visibleBody = defaultAction 255 ? [body, `â ${actions[0].title}`].filter(Boolean).join('\n') 256 : body; 257 const mediaUrls = [...new Set([image, icon, badge].filter(Boolean))]; 258 const loadedMediaUrls = new Set(); 259 260 await Promise.all( 261 mediaUrls.map(async (url) => { 262 try { 263 await preloadNotificationImage(url); 264 loadedMediaUrls.add(url); 265 } catch { 266 // A broken image must not prevent delivery of the notification. 267 } 268 }), 269 ); 270 271 await self.registration.showNotification(notificationTitle, { 272 actions: visibleActions.map(({ action, title }) => ({ action, title })), 273 badge: loadedMediaUrls.has(badge) 274 ? badge 275 : loadedMediaUrls.has(icon) 276 ? icon 277 : undefined, 278 body: visibleBody, 279 data: { 280 actions, 281 defaultAction, 282 notificationId: payload.notificationId ?? null, 283 url: normalizeNotificationUrl(payload.url), 284 }, 285 icon: loadedMediaUrls.has(icon) ? icon : undefined, 286 image: loadedMediaUrls.has(image) ? image : undefined, 287 tag: payload.tag ? String(payload.tag) : undefined, 288 }); 289 })(), 290 ); 291}); 292 293self.addEventListener('notificationclick', (event) => { 294 event.notification.close(); 295 const notificationId = getNotificationId( 296 event.notification.data?.notificationId, 297 );
298 const selectedAction = Array.isArray(event.notification.data?.actions) 299 ? event.notification.data.actions.find( 300 (action) => 301 action?.action === 302 (event.action || event.notification.data?.defaultAction), 303 ) 304 : null; 305 306 event.waitUntil( 307 self.clients 308 .matchAll({ type: 'window', includeUncontrolled: true }) 309 .then((windows) => { 310 if (selectedAction) { 311 const actionUrl = withNotificationAction( 312 new URL('/', self.location.origin).href, 313 selectedAction.action, 314 notificationId, 315 ); 316 const sameOriginWindows = windows.filter(isSameOriginWindow); 317 const target = 318 sameOriginWindows.find( 319 (client) => client.visibilityState === 'visible', 320 ) ?? sameOriginWindows[0]; 321 const openActionWindow = () => 322 self.clients 323 .openWindow(actionUrl) 324 .then((openedWindow) => openedWindow?.focus()); 325 326 if (!target || typeof target.navigate !== 'function') { 327 return openActionWindow(); 328 } 329 return target 330 .navigate(actionUrl) 331 .then((navigatedWindow) => (navigatedWindow ?? target).focus()) 332 .catch(openActionWindow); 333 } 334 335 if (notificationId) { 336 const sameOriginWindows = windows.filter(isSameOriginWindow); 337 const target = 338 sameOriginWindows.find( 339 (client) => client.visibilityState === 'visible', 340 ) ?? sameOriginWindows[0]; 341 342 if (target) { 343 const targetUrl = withNotificationId(target.url, notificationId); 344 if (new URL(target.url).href === targetUrl) { 345 return target.focus(); 346 } 347 348 const openTargetWindow = () => 349 self.clients 350 .openWindow(targetUrl) 351 .then((openedWindow) => openedWindow?.focus()); 352 if (typeof target.navigate !== 'function') { 353 return openTargetWindow(); 354 } 355 return target 356 .navigate(targetUrl) 357 .then((navigatedWindow) => (navigatedWindow ?? target).focus()) 358 .catch(openTargetWindow); 359 } 360 361 const rootUrl = withNotificationId( 362 new URL('/', self.location.origin).href, 363 notificationId, 364 ); 365 return self.clients.openWindow(rootUrl); 366 } 367 368 const fallbackUrl = normalizeNotificationUrl( 369 event.notification.data?.url, 370 ); 371 const target = windows.find( 372 (client) => normalizeNotificationUrl(client.url) === fallbackUrl, 373 ); 374 if (target) return target.focus(); 375 return self.clients.openWindow(fallbackUrl); 376 }), 377 ); 378});
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.