/** * Anti-Cheat Script for oTree Experiments * Prevents common cheating methods during experimental tasks */ (function() { 'use strict'; // Configuration const config = { blockRightClick: true, blockContextMenu: true, blockTextSelection: true, blockCopyPaste: true, blockDevTools: true, blockKeyboardShortcuts: true, trackWindowFocus: true, showWarnings: true, logAttempts: false }; let cheatingAttempts = 0; let focusLossCount = 0; let devToolsDetected = false; // ==================== Right-Click Prevention ==================== if (config.blockRightClick) { document.addEventListener('contextmenu', function(e) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Right-click attempt'); showWarning('Rechtsklick ist während der Aufgabe nicht erlaubt.'); return false; }, false); } // ==================== Text Selection Prevention ==================== if (config.blockTextSelection) { document.addEventListener('selectstart', function(e) { // Allow selection in input fields and textareas if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return true; } e.preventDefault(); return false; }, false); // Disable text selection via CSS const style = document.createElement('style'); style.textContent = ` body { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } input, textarea { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; } `; document.head.appendChild(style); } // ==================== Copy/Paste Prevention ==================== if (config.blockCopyPaste) { // Block copy document.addEventListener('copy', function(e) { // Allow copy from input fields if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return true; } e.preventDefault(); e.clipboardData.setData('text/plain', ''); logCheatingAttempt('Copy attempt'); showWarning('Kopieren ist nicht erlaubt.'); return false; }, false); // Block cut document.addEventListener('cut', function(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { return true; } e.preventDefault(); logCheatingAttempt('Cut attempt'); return false; }, false); // Block paste (optional - might want to allow in input fields) document.addEventListener('paste', function(e) { // You can allow paste in input fields if needed // if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') { // return true; // } e.preventDefault(); logCheatingAttempt('Paste attempt'); showWarning('Einfügen ist nicht erlaubt.'); return false; }, false); } // ==================== Keyboard Shortcuts Prevention ==================== if (config.blockKeyboardShortcuts) { document.addEventListener('keydown', function(e) { const key = e.key ? e.key.toUpperCase() : ''; // Debug logging for Cmd+F if (e.metaKey && e.keyCode === 70) { console.log('[DEBUG] ENTERING Cmd+F block, metaKey:', e.metaKey, 'keyCode:', e.keyCode); } // ===== F12 (Dev Tools - Both OS) ===== if (key === 'F12' || e.keyCode === 123) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); logCheatingAttempt('F12 key press'); // showWarning('Die entwicklertools sind nicht erlaubt.'); return false; } // ===== Mac Screenshot Shortcuts (Check FIRST - highest priority) ===== // Cmd+Shift+3 (Full screenshot) if (e.metaKey && e.shiftKey && !e.ctrlKey && !e.altKey && (key === '#' || key === '3' || e.keyCode === 51)) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); logCheatingAttempt('Cmd+Shift+3 (Mac Screenshot)'); showWarning('Screenshots sind nicht erlaubt.'); return false; } // Cmd+Shift+4 (Area screenshot) if (e.metaKey && e.shiftKey && !e.ctrlKey && !e.altKey && (key === '$' || key === '4' || e.keyCode === 52)) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); logCheatingAttempt('Cmd+Shift+4 (Mac Screenshot)'); showWarning('Screenshots sind nicht erlaubt.'); return false; } // Cmd+Shift+5 (Screenshot controls) if (e.metaKey && e.shiftKey && !e.ctrlKey && !e.altKey && (key === '%' || key === '5' || e.keyCode === 53)) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); logCheatingAttempt('Cmd+Shift+5 (Mac Screenshot)'); showWarning('Screenshots sind nicht erlaubt.'); return false; } // ===== Developer Tools ===== // Windows/Linux: Ctrl+Shift+I if (e.ctrlKey && e.shiftKey && !e.metaKey && (key === 'I' || e.keyCode === 73)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Ctrl+Shift+I (Dev Tools)'); // showWarning('Die entwicklertools sind nicht erlaubt.'); return false; } // Mac: Cmd+Option+I if (e.metaKey && e.altKey && (key === 'I' || e.keyCode === 73)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Cmd+Option+I (Mac Dev Tools)'); // showWarning('Die entwicklertools sind nicht erlaubt.'); return false; } // ===== Console ===== // Windows/Linux: Ctrl+Shift+J if (e.ctrlKey && e.shiftKey && !e.metaKey && (key === 'J' || e.keyCode === 74)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Ctrl+Shift+J (Console)'); // showWarning('Die entwicklertools sind nicht erlaubt.'); return false; } // Mac: Cmd+Option+J if (e.metaKey && e.altKey && (key === 'J' || e.keyCode === 74)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Cmd+Option+J (Mac Console)'); // showWarning('Die entwicklertools sind nicht erlaubt.'); return false; } // ===== Inspect Element ===== // Windows/Linux: Ctrl+Shift+C if (e.ctrlKey && e.shiftKey && !e.metaKey && (key === 'C' || e.keyCode === 67)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Ctrl+Shift+C (Inspect Element)'); // showWarning('Die entwicklertools sind nicht erlaubt.'); return false; } // Mac: Cmd+Option+C if (e.metaKey && e.altKey && (key === 'C' || e.keyCode === 67)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Cmd+Option+C (Mac Inspect Element)'); // showWarning('Die entwicklertools sind nicht erlaubt.'); return false; } // ===== View Source ===== // Windows/Linux: Ctrl+U if (e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && (key === 'U' || e.keyCode === 85)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Ctrl+U (View Source)'); showWarning('Quellcode anzeigen ist nicht erlaubt.'); return false; } // Mac: Cmd+Option+U if (e.metaKey && e.altKey && !e.shiftKey && (key === 'U' || e.keyCode === 85)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Cmd+Option+U (Mac View Source)'); showWarning('Quellcode anzeigen ist nicht erlaubt.'); return false; } // ===== Save Page ===== // Windows/Linux: Ctrl+S if (e.ctrlKey && !e.shiftKey && !e.metaKey && (key === 'S' || e.keyCode === 83)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Ctrl+S (Save Page)'); showWarning('Seite speichern ist nicht erlaubt.'); return false; } // Mac: Cmd+S (without Shift) if (e.metaKey && !e.shiftKey && !e.ctrlKey && !e.altKey && (key === 'S' || e.keyCode === 83)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Cmd+S (Mac Save Page)'); showWarning('Seite speichern ist nicht erlaubt.'); return false; } // ===== Find in Page ===== // Windows/Linux: Ctrl+F if (e.ctrlKey && !e.shiftKey && !e.metaKey && (key === 'F' || e.keyCode === 70)) { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); logCheatingAttempt('Ctrl+F (Find)'); showWarning('Suchen ist nicht erlaubt.'); return false; } // Mac: Cmd+F - MUST block before browser processes it if (e.metaKey && e.keyCode === 70) { console.log('[Anti-Cheat] Detected Cmd+F - BLOCKING'); e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); logCheatingAttempt('Cmd+F (Mac Find)'); showWarning('Suchen ist nicht erlaubt.'); return false; } // ===== Copy (for non-input elements) ===== // Windows/Linux: Ctrl+C if (e.ctrlKey && !e.shiftKey && !e.metaKey && (key === 'C' || e.keyCode === 67)) { if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Ctrl+C (Copy)'); return false; } } // Mac: Cmd+C if (e.metaKey && !e.shiftKey && !e.altKey && !e.ctrlKey && (key === 'C' || e.keyCode === 67)) { if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Cmd+C (Mac Copy)'); return false; } } // ===== Windows Screenshot Shortcuts ===== // Windows+Shift+S (Snipping tool - Windows 10+) if (e.metaKey && e.shiftKey && e.ctrlKey && (key === 'S' || e.keyCode === 83)) { e.preventDefault(); e.stopPropagation(); logCheatingAttempt('Win+Shift+S (Windows Screenshot)'); showWarning('Screenshots sind nicht erlaubt.'); return false; } }, {capture: true, passive: false}); } // ==================== DevTools Detection ==================== if (config.blockDevTools) { // Method 1: Console detection const devToolsCheck = function() { const threshold = 160; if (window.outerWidth - window.innerWidth > threshold || window.outerHeight - window.innerHeight > threshold) { if (!devToolsDetected) { devToolsDetected = true; logCheatingAttempt('DevTools opened (size detection)'); // showWarning('Bitte schließen Sie die Entwicklertools.'); } } else { devToolsDetected = false; } }; // Check periodically setInterval(devToolsCheck, 10000); // Method 2: Debugger statement (more aggressive) // Uncomment if you want to force-close devtools /* setInterval(function() { debugger; }, 100); */ // Method 3: Console.log detection const originalLog = console.log; console.log = function() { logCheatingAttempt('Console usage detected'); originalLog.apply(console, arguments); }; } // ==================== Window Focus Tracking ==================== if (config.trackWindowFocus) { let lastBlurTime = 0; let findDialogSuspected = false; window.addEventListener('blur', function() { lastBlurTime = Date.now(); // Check if this is a brief blur (likely Find dialog opening) setTimeout(function() { const blurDuration = Date.now() - lastBlurTime; // If blur lasted less than 200ms and page still has focus, likely Find dialog if (blurDuration < 200 && !document.hidden && document.hasFocus()) { findDialogSuspected = true; logCheatingAttempt('Possible Find dialog opened (brief focus loss)'); showWarning('Die Suchfunktion ist nicht erlaubt.'); } }, 250); focusLossCount++; logCheatingAttempt('Window focus lost (tab/window switch)'); // Store focus loss in hidden field if it exists const focusInput = document.getElementById('focus_loss_count'); if (focusInput) { focusInput.value = focusLossCount; } if (config.showWarnings && !findDialogSuspected) { // Show warning when they return setTimeout(function() { if (document.hasFocus()) { showWarning('Sie haben das Fenster verlassen. Bitte bleiben Sie während der Aufgabe im Fenster.'); } }, 100); } }); window.addEventListener('focus', function() { findDialogSuspected = false; // Window regained focus }); // Detect visibility change (different tab) document.addEventListener('visibilitychange', function() { if (document.hidden) { focusLossCount++; logCheatingAttempt('Page visibility lost (tab change)'); } }); } // ==================== Find Dialog Detection ==================== // Detect browser's Find-in-Page dialog (regardless of how it was opened) let searchAttempts = 0; // Method 1: Detect selection changes that might indicate find-in-page let lastSelectionText = ''; document.addEventListener('selectionchange', function() { const selection = window.getSelection(); const selectedText = selection.toString(); // If text is being selected programmatically (not by user), it might be Find if (selectedText && selectedText !== lastSelectionText) { // Check if selection appears to be from find (usually highlights matches) const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; if (range && range.startContainer === range.endContainer) { // This could be Find highlighting a match searchAttempts++; if (searchAttempts > 2) { // Avoid false positives logCheatingAttempt('Suspected Find-in-Page usage (text selection pattern)'); } } } lastSelectionText = selectedText; }); // Method 2: Monitor for browser search event (limited support) if ('onsearch' in document) { document.addEventListener('search', function() { logCheatingAttempt('Browser search event detected'); showWarning('Die Suchfunktion ist nicht erlaubt.'); }); } // ==================== Additional Screenshot Prevention ==================== // Note: Complete screenshot prevention is not possible, but we can detect and log attempts // Some shortcuts like Windows+PrintScreen may not be fully blockable due to OS-level handling document.addEventListener('keyup', function(e) { // Print Screen key (Windows) if (e.key === 'PrintScreen' || e.keyCode === 44) { logCheatingAttempt('Print Screen key'); showWarning('Screenshots sind nicht erlaubt.'); } }, false); document.addEventListener('keydown', function(e) { // Alt+Print Screen (Windows - captures active window) if (e.altKey && (e.key === 'PrintScreen' || e.keyCode === 44)) { e.preventDefault(); logCheatingAttempt('Alt+Print Screen (Windows)'); showWarning('Screenshots sind nicht erlaubt.'); return false; } }, false); // Detect if page is being captured (experimental) // Some browsers don't support this if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { // Screen capture API detection would require user permission // This is mainly for detection, not prevention } // ==================== Drag and Drop Prevention ==================== document.addEventListener('dragstart', function(e) { e.preventDefault(); return false; }, false); // ==================== Helper Functions ==================== function logCheatingAttempt(type) { cheatingAttempts++; if (config.logAttempts) { console.warn('[Anti-Cheat]', type, '- Total attempts:', cheatingAttempts); // Send to server if liveSend is available (oTree live pages) if (typeof liveSend !== 'undefined') { try { liveSend({ type: 'cheat_attempt', attempt_type: type, total_attempts: cheatingAttempts, focus_losses: focusLossCount, timestamp: Date.now() }); } catch (e) { console.error('Failed to send cheat attempt to server:', e); } } // Store in hidden field if it exists const cheatingInput = document.getElementById('cheating_attempts'); if (cheatingInput) { cheatingInput.value = cheatingAttempts; } } } function showWarning(message) { if (!config.showWarnings) return; // Check if warning element exists let warningDiv = document.getElementById('anti-cheat-warning'); if (!warningDiv) { // Create warning element warningDiv = document.createElement('div'); warningDiv.id = 'anti-cheat-warning'; warningDiv.style.cssText = ` position: fixed; top: 30px; left: 50%; transform: translateX(-50%); background-color: #dc143c; color: white; padding: 15px 30px; border-radius: 5px; box-shadow: 0 4px 6px rgba(0,0,0,0.3); z-index: 999999; font-size: 1rem; font-weight: normal; font-style: italic; text-align: center; display: none; max-width: 80%; opacity: 0.88; `; document.body.appendChild(warningDiv); } warningDiv.textContent = message; warningDiv.style.display = 'block'; // Hide after 3 seconds setTimeout(function() { warningDiv.style.display = 'none'; }, 3000); } // ==================== Initialize ==================== function init() { console.log('[Anti-Cheat] Protection enabled'); // Add hidden fields to form if they don't exist document.addEventListener('DOMContentLoaded', function() { const form = document.getElementById('form'); if (form) { // Add cheating attempts field if (!document.getElementById('cheating_attempts')) { const cheatingInput = document.createElement('input'); cheatingInput.type = 'hidden'; cheatingInput.id = 'cheating_attempts'; cheatingInput.name = 'cheating_attempts'; cheatingInput.value = '0'; form.appendChild(cheatingInput); } // Add focus loss count field if (!document.getElementById('focus_loss_count')) { const focusInput = document.createElement('input'); focusInput.type = 'hidden'; focusInput.id = 'focus_loss_count'; focusInput.name = 'focus_loss_count'; focusInput.value = '0'; form.appendChild(focusInput); } } }); } // Start protection init(); // Export API for custom configuration window.AntiCheat = { getAttempts: function() { return cheatingAttempts; }, getFocusLosses: function() { return focusLossCount; }, configure: function(options) { Object.assign(config, options); }, showWarning: showWarning }; })();