PageSourceSearch

https://www.habselstree.org.uk/boys/wp-content/plugins/image-protection/protection.js?ver=1.2.2

js habselstree.org.uk collected 2026-10-01 02:07:03 UTC 17,305 bytes, 591 lines download raw bytes

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.