1(function() { 2 'use strict'; 3 4 var CONFIG = typeof imageProtectionVars !== 'undefined' ? imageProtectionVars : {}; 5 6 var LEVELS = { 7 relaxed: { 8 preemptiveModifier: false, 9 blurOnWindowBlur: false, 10 focusLossHeuristic: false, 11 periodicClipboardClear: false 12 }, 13 balanced: { 14 preemptiveModifier: true, 15 blurOnWindowBlur: true, 16 focusLossHeuristic: false, 17 periodicClipboardClear: false 18 }, 19 strict: { 20 preemptiveModifier: true, 21 blurOnWindowBlur: true, 22 focusLossHeuristic: true, 23 periodicClipboardClear: true 24 } 25 }; 26 27 class ImageProtection { 28 constructor() { 29 this.filteredImages = []; 30 this.filteredVideos = []; 31 this.bgImages = []; 32 this.observer = null; 33 34 this.message = CONFIG.message || ''; 35 this.showCredit = !!CONFIG.showCredit; 36 this.blurAmount = CONFIG.blurAmount ? parseInt(CONFIG.blurAmount, 10) : 10; 37 this.blurImages = !!CONFIG.blurImages; 38 this.blurVideos = !!CONFIG.blurVideos; 39 this.disableRightClick = !!CONFIG.disableRightClick; 40 41 var levelName = (CONFIG.protectionLevel || 'balanced').toLowerCase(); 42 this.levelName = LEVELS[levelName] ? levelName : 'balanced'; 43 this.level = LEVELS[this.levelName]; 44 45 this.neutralizeClipboard = !!CONFIG.neutralizeClipboard; 46 this.interceptScreenRecording = !!CONFIG.interceptScreenRecording; 47 this.printProtection = !!CONFIG.printProtection; 48 49 // Active blur "locks". The content is blurred while any lock is held. 50 this.blurLocks = new Set(); 51 this.modifierTimer = null; 52 this.lastModifierAt = 0; 53 this.clipboardInterval = null; 54 55 this.init(); 56 } 57 58 init() { 59 this.injectStyles(); 60 this.getImages(); 61 this.getVideos(); 62 this.setupEventListeners(); 63 this.setupImageProtection(); 64 this.setupVideoProtection(); 65 this.setupImageLinkProtection(); 66 this.setupMutationObserver(); 67 this.setupScreenRecordingGuard(); 68 this.setupPeriodicClipboardClear(); 69 } 70 71 injectStyles() { 72 if (document.getElementById('image-protection-styles')) { 73 return; 74 } 75 76 var css = ''; 77 78 if (this.printProtection) { 79 // Blank protected media when printing / saving to PDF. 80 css += '@media print {' + 81 '.image-protection-on, .image-protection-on * {' + 82 'visibility: hidden !important;' + 83 '}' + 84 'body::after {' + 85 "content: 'Protected content is not available in print.';" + 86 'display: block; text-align: center; padding: 40px; font-size: 16px;' + 87 '}' + 88 '}'; 89 } 90 91 if (!css) { 92 return; 93 } 94 95 var style = document.createElement('style'); 96 style.id = 'image-protection-styles'; 97 style.textContent = css; 98 (document.head || document.documentElement).appendChild(style); 99 } 100 101 setupMutationObserver() { 102 this.observer = new MutationObserver((mutations) => { 103 var shouldUpdate = false; 104 105 for (var i = 0; i < mutations.length; i++) { 106 var mutation = mutations[i]; 107 108 if (mutation.type === 'attributes' && mutation.target) { 109 if (mutation.target.nodeName === 'IMG' || mutation.target.nodeName === 'VIDEO') { 110 shouldUpdate = true; 111 break; 112 } 113 continue; 114 } 115 116 if (mutation.addedNodes && mutation.addedNodes.length) { 117 mutation.addedNodes.forEach((node) => { 118 if (node.nodeType !== Node.ELEMENT_NODE) { 119 return; 120 } 121 if (node.nodeName === 'IMG' || 122 node.nodeName === 'VIDEO' || 123 (node.getElementsByTagName && node.getElementsByTagName('img').length) || 124 (node.getElementsByTagName && node.getElementsByTagName('video').length) || 125 this.hasBackgroundImage(node)) { 126 shouldUpdate = true; 127 } 128 }); 129 } 130 } 131 132 if (shouldUpdate) { 133 this.getImages(); 134 this.getVideos(); 135 this.setupImageProtection(); 136 this.setupVideoProtection(); 137 this.setupImageLinkProtection(); 138 this.refreshBlur(); 139 } 140 }); 141 142 this.observer.observe(document.body, { 143 childList: true, 144 subtree: true, 145 attributes: true, 146 attributeFilter: ['src', 'style', 'srcset'] 147 }); 148 } 149
150 hasBackgroundImage(element) { 151 try { 152 var style = window.getComputedStyle(element); 153 return style.backgroundImage !== 'none' && style.backgroundImage.includes('url('); 154 } catch (e) { 155 return false; 156 } 157 } 158 159 isProtectionExcluded(element) { 160 if (!element || element.nodeType !== Node.ELEMENT_NODE) { 161 return false; 162 } 163 164 if (element.matches('[data-image-protection="true"], .force-image-protection')) { 165 return false; 166 } 167 168 return element.closest('.no-image-protection, [data-image-protection="false"]') !== null; 169 } 170 171 isRecaptchaElement(element) { 172 if (!element || element.nodeType !== Node.ELEMENT_NODE) { 173 return false; 174 } 175 176 if (element.tagName === 'IFRAME') { 177 var src = (element.getAttribute('src') || '').toLowerCase(); 178 if (src.indexOf('recaptcha') !== -1) { 179 return true; 180 } 181 } 182 183 var node = element; 184 while (node && node.nodeType === Node.ELEMENT_NODE) { 185 var id = (node.id || '').toLowerCase(); 186 var className = (typeof node.className === 'string' ? node.className : (node.getAttribute('class') || '')).toLowerCase(); 187 188 if (id.indexOf('recaptcha') !== -1 || className.indexOf('recaptcha') !== -1) { 189 return true; 190 } 191 192 if (node.getAttribute('data-sitekey')) { 193 return true; 194 } 195 196 node = node.parentElement; 197 } 198 199 return false; 200 } 201 202 getImages() { 203 this.filteredImages = []; 204 this.bgImages = []; 205 206 if (this.blurImages) { 207 var images = document.querySelectorAll("img:not([src$='.svg'])"); 208 209 this.bgImages = Array.from(document.querySelectorAll('*')).filter((element) => { 210 var style = window.getComputedStyle(element); 211 var hasBackgroundImage = style.backgroundImage !== 'none' && style.backgroundImage.includes('url('); 212 213 var hasDataSrc = element.hasAttribute('data-src'); 214 var isLikelyImage = hasDataSrc && ( 215 element.tagName === 'IMG' || 216 element.classList.contains('lazy') || 217 element.classList.contains('lazyload') || 218 /\.(jpg|jpeg|png|gif|webp)$/i.test(element.getAttribute('data-src') || '') 219 ); 220 221 return hasBackgroundImage || isLikelyImage; 222 }); 223 224 var allImages = [...images, ...this.bgImages]; 225 this.filteredImages = allImages.filter((img) => !this.isProtectionExcluded(img)); 226 } 227 228 var forcedProtectionElements = document.querySelectorAll('[data-image-protection="true"], .force-image-protection'); 229 forcedProtectionElements.forEach((el) => { 230 if (!this.filteredImages.includes(el)) { 231 this.filteredImages.push(el); 232 } 233 }); 234 } 235 236 getVideos() { 237 this.filteredVideos = []; 238 239 if (this.blurVideos) { 240 var videos = document.querySelectorAll('video'); 241 this.filteredVideos = Array.from(videos).filter((video) => !this.isProtectionExcluded(video)); 242 } 243 244 var forcedProtectionVideos = document.querySelectorAll('video[data-image-protection="true"], video.force-image-protection'); 245 forcedProtectionVideos.forEach((video) => { 246 if (!this.filteredVideos.includes(video)) { 247 this.filteredVideos.push(video); 248 } 249 }); 250 } 251 252 setupEventListeners() { 253 if (this.disableRightClick) { 254 document.addEventListener('contextmenu', (e) => e.preventDefault(), true); 255 } 256 257 document.addEventListener('keydown', this.handleKeyDown.bind(this), true); 258 document.addEventListener('keyup', this.handleKeyUp.bind(this), true); 259 260 document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); 261 window.addEventListener('blur', this.handleWindowBlur.bind(this)); 262 window.addEventListener('focus', this.handleWindowFocus.bind(this)); 263 window.addEventListener('pagehide', () => this.addBlur('hidden')); 264 265 document.addEventListener('copy', this.handleCopy.bind(this)); 266 document.addEventListener('selectstart', this.handleSelectStart.bind(this)); 267 268 if (this.printProtection) { 269 window.addEventListener('beforeprint', () => this.addBlur('print')); 270 window.addEventListener('afterprint', () => this.removeBlur('print')); 271 } 272 } 273 274 // Layout-independent screenshot combo detection using e.code. 275 isScreenshotCombo(e) { 276 var code = e.code || ''; 277 278 // PrintScreen (Windows/Linux), and Shift+PrintScreen (Linux area capture). 279 if (code === 'PrintScreen' || e.key === 'PrintScreen' || e.keyCode === 44) { 280 return true; 281 } 282
283 // Windows Snipping Tool: Win+Shift+S (Meta maps to the Windows key). 284 if (e.shiftKey && e.metaKey && code === 'KeyS') { 285 return true; 286 } 287 288 // macOS: Cmd+Shift+3 / 4 / 5. 289 if (e.metaKey && e.shiftKey && (code === 'Digit3' || code === 'Digit4' || code === 'Digit5')) { 290 return true; 291 } 292 293 return false; 294 } 295 296 handleKeyDown(e) { 297 if (this.isScreenshotCombo(e)) { 298 this.addBlur('keycombo'); 299 // The capture is instantaneous; release this lock shortly after. 300 window.setTimeout(() => this.removeBlur('keycombo'), 1200); 301 e.preventDefault(); 302 return false; 303 } 304 305 // Pre-emptive shield: macOS swallows the digit keydown for screenshots, 306 // so blur as soon as the Cmd/Win+Shift modifier pair is held. 307 if (this.level.preemptiveModifier && e.shiftKey && (e.metaKey || e.ctrlKey)) { 308 this.lastModifierAt = Date.now(); 309 this.addBlur('modifier'); 310 } 311 312 // Block copy/save shortcuts. 313 if ((e.ctrlKey || e.metaKey) && (e.code === 'KeyC' || e.code === 'KeyS')) { 314 if (this.selectionTouchesProtectedImage() || e.code === 'KeyS') { 315 e.preventDefault(); 316 return false; 317 } 318 } 319 320 // Block print shortcut when print protection is on. 321 if (this.printProtection && (e.ctrlKey || e.metaKey) && e.code === 'KeyP') { 322 this.addBlur('print'); 323 e.preventDefault(); 324 return false; 325 } 326 } 327 328 handleKeyUp(e) { 329 if (this.neutralizeClipboard && (e.code === 'PrintScreen' || e.key === 'PrintScreen' || e.keyCode === 44)) { 330 this.neutralizeClipboardNow(); 331 } 332 333 // Release the pre-emptive modifier shield once modifiers are gone. 334 if (!e.shiftKey || (!e.metaKey && !e.ctrlKey)) { 335 this.removeBlur('modifier'); 336 } 337 338 if (this.printProtection && e.code === 'KeyP') { 339 window.setTimeout(() => this.removeBlur('print'), 500); 340 } 341 } 342 343 handleWindowBlur() { 344 // reCAPTCHA is rendered inside a cross-origin iframe. Clicking the 345 // checkbox or challenge moves focus into that iframe, which fires a 346 // window.blur on the parent page. Do not treat that as a screenshot. 347 if (document.activeElement && this.isRecaptchaElement(document.activeElement)) { 348 return; 349 } 350 351 // Snipping Tool / screenshot overlays / capture pickers steal window focus. 352 if (this.level.blurOnWindowBlur) { 353 this.addBlur('focus'); 354 return; 355 } 356 357 // Strict-only heuristic: focus lost right after a modifier press is very 358 // likely a screenshot tool, even in lower aggressiveness configs. 359 if (this.level.focusLossHeuristic && Date.now() - this.lastModifierAt < 400) { 360 this.addBlur('focus'); 361 } 362 } 363 364 handleWindowFocus() { 365 this.removeBlur('focus'); 366 } 367 368 handleVisibilityChange() { 369 if (document.hidden) { 370 this.addBlur('hidden'); 371 } else { 372 this.removeBlur('hidden'); 373 } 374 } 375 376 handleSelectStart(e) { 377 var target = e.target; 378 if (target && target.nodeType === Node.ELEMENT_NODE && target.closest && target.closest('.image-protection-on')) { 379 e.preventDefault(); 380 return false; 381 } 382 } 383 384 selectionTouchesProtectedImage() { 385 var selection = window.getSelection(); 386 if (!selection || selection.rangeCount === 0) { 387 return false; 388 } 389 390 for (var i = 0; i < this.filteredImages.length; i++) { 391 var img = this.filteredImages[i]; 392 if (img && selection.containsNode(img, true)) { 393 return true; 394 } 395 } 396 return false; 397 } 398 399 handleCopy(e) { 400 if (this.selectionTouchesProtectedImage()) { 401 e.preventDefault(); 402 return false; 403 } 404 } 405 406 neutralizeClipboardNow() { 407 try { 408 if (navigator.clipboard && navigator.clipboard.writeText) { 409 navigator.clipboard.writeText(' ').catch(() => {}); 410 } 411 } catch (err) { 412 // Best-effort only; ignore unsupported environments. 413 } 414 } 415 416 setupPeriodicClipboardClear() { 417 if (!this.neutralizeClipboard || !this.level.periodicClipboardClear) { 418 return; 419 } 420 421 this.clipboardInterval = window.setInterval(() => { 422 if (document.hasFocus()) { 423 this.neutralizeClipboardNow(); 424 } 425 }, 3000); 426 } 427 428 setupScreenRecordingGuard() { 429 if (!this.interceptScreenRecording) { 430 return; 431 } 432 if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) { 433 return; 434 } 435 436 var self = this; 437 var original = navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices); 438 439 navigator.mediaDevices.getDisplayMedia = function() { 440 self.addBlur('recording'); 441
442 return original.apply(this, arguments).then((stream) => { 443 var tracks = stream.getTracks(); 444 var remaining = tracks.length; 445 446 if (!remaining) { 447 self.removeBlur('recording'); 448 } 449 450 tracks.forEach((track) => { 451 track.addEventListener('ended', () => { 452 remaining -= 1; 453 if (remaining <= 0) { 454 self.removeBlur('recording'); 455 } 456 }); 457 }); 458 459 return stream; 460 }).catch((err) => { 461 self.removeBlur('recording'); 462 throw err; 463 }); 464 }; 465 } 466 467 setupImageLinkProtection() { 468 if (!this.blurImages) { 469 return; 470 } 471 472 document.querySelectorAll("a[href$='.jpg'], a[href$='.png'], a[href$='.jpeg'], a[href$='.webp'], a[href$='.gif']").forEach((link) => { 473 link.addEventListener('contextmenu', (e) => e.preventDefault()); 474 link.querySelectorAll('*').forEach((child) => { 475 child.addEventListener('contextmenu', (e) => e.preventDefault()); 476 }); 477 }); 478 } 479 480 setupImageProtection() { 481 this.filteredImages.forEach((img) => { 482 if (!this.bgImages.includes(img)) { 483 this.addElementProtection(img); 484 } 485 }); 486 } 487 488 setupVideoProtection() { 489 this.filteredVideos.forEach((video) => { 490 this.addElementProtection(video); 491 }); 492 } 493 494 addElementProtection(el) { 495 if (el.dataset && el.dataset.imageProtectionBound === '1') { 496 return; 497 } 498 if (el.dataset) { 499 el.dataset.imageProtectionBound = '1'; 500 } 501 502 el.classList.add('image-protection-on'); 503 el.style.cssText += 'user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;-webkit-user-drag:none;-khtml-user-drag:none;-moz-user-drag:none;-o-user-drag:none;user-drag:none;'; 504 el.setAttribute('draggable', 'false'); 505 el.addEventListener('contextmenu', (e) => e.preventDefault()); 506 el.addEventListener('dragstart', (e) => e.preventDefault()); 507 } 508 509 // --- Blur state machine -------------------------------------------- 510 511 addBlur(reason) { 512 this.blurLocks.add(reason); 513 this.refreshBlur(); 514 } 515 516 removeBlur(reason) { 517 this.blurLocks.delete(reason); 518 this.refreshBlur(); 519 } 520 521 refreshBlur() { 522 if (this.blurLocks.size > 0) { 523 this.applyBlur(); 524 } else { 525 this.clearBlur(); 526 } 527 } 528 529 applyBlur() { 530 var elements = [...this.filteredImages, ...this.filteredVideos]; 531 elements.forEach((element) => { 532 element.style.transition = 'none'; 533 element.style.filter = 'blur(' + this.blurAmount + 'px)'; 534 }); 535 this.showMessage(); 536 } 537 538 clearBlur() { 539 var elements = [...this.filteredImages, ...this.filteredVideos]; 540 elements.forEach((element) => { 541 element.style.transition = ''; 542 element.style.filter = 'none'; 543 }); 544 this.hideMessage(); 545 } 546 547 showMessage() { 548 if (!this.message || document.querySelector('.image-protection-message')) { 549 return; 550 } 551 552 if (this.filteredImages.length === 0 && this.filteredVideos.length === 0) { 553 return; 554 } 555 556 var overlay = document.createElement('div'); 557 overlay.classList.add('image-protection-message'); 558 overlay.style.cssText = 'position:fixed;bottom:20px;left:20px;right:20px;max-width:500px;background-color:rgba(0,0,0,0.8);color:#fff;padding:10px;border-radius:5px;z-index:2147483647;'; 559 overlay.textContent = this.message; 560 561 if (this.showCredit) { 562 overlay.appendChild(document.createElement('br')); 563 overlay.appendChild(document.createElement('br')); 564 overlay.appendChild(document.createTextNode('Protected by ')); 565 566 var creditLink = document.createElement('a'); 567 creditLink.href = 'https://thewebkitchen.co.uk/'; 568 creditLink.target = '_blank'; 569 creditLink.rel = 'noopener noreferrer'; 570 creditLink.style.color = 'white'; 571 creditLink.textContent = 'The Web Kitchen'; 572 overlay.appendChild(creditLink); 573 } 574 575 (document.body || document.documentElement).appendChild(overlay); 576 } 577 578 hideMessage() { 579 var overlay = document.querySelector('.image-protection-message'); 580 if (overlay) { 581 overlay.remove(); 582 } 583 } 584 } 585 586 if (document.readyState === 'loading') { 587 document.addEventListener('DOMContentLoaded', () => new ImageProtection()); 588 } else { 589 new ImageProtection(); 590 } 591})();
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.