Input Guardian

Enhanced input preservation with security checks, performance optimizations, and user controls.

Versión del día 24/03/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Input Guardian
// @namespace    https://spin.rip/
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @version      1.0
// @author       Spinfal
// @description  Enhanced input preservation with security checks, performance optimizations, and user controls.
// @license      AGPL-3.0 License
// ==/UserScript==

(function () {
    'use strict';

    const dbName = "InputGuardianDB";
    const storeName = "inputs";
    const dbVersion = 2;
    const DEBOUNCE_TIME = 500; // ms
    const CLEANUP_DAYS = 30;
    const SENSITIVE_NAMES = /(ccnum|creditcard|cvv|ssn|sin|securitycode)/i;

    // Security: Additional sensitive field check
    const isSensitiveField = (input) => {
        return SENSITIVE_NAMES.test(input.id) ||
               SENSITIVE_NAMES.test(input.name) ||
               input.closest('[data-sensitive="true"]');
    };

    // Performance: Debounce function
    const debounce = (func, wait) => {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    };

    // Enhanced database with timestamp
    const openDatabase = () => {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(dbName, dbVersion);

            request.onupgradeneeded = (event) => {
                const db = event.target.result;

                // Create or upgrade object store
                let store;
                if (!db.objectStoreNames.contains(storeName)) {
                    store = db.createObjectStore(storeName, { keyPath: "id" });
                } else {
                    store = request.transaction.objectStore(storeName);
                }

                // Ensure the "timestamp" index exists
                if (!store.indexNames.contains("timestamp")) {
                    store.createIndex("timestamp", "timestamp", { unique: false });
                }
            };

            request.onsuccess = (event) => resolve(event.target.result);
            request.onerror = (event) => reject(event.target.error);
        });
    };

    // Cleanup old entries
    const cleanupOldEntries = async () => {
        try {
            const db = await openDatabase();
            const transaction = db.transaction(storeName, "readwrite");
            const store = transaction.objectStore(storeName);
            const index = store.index("timestamp");
            const threshold = Date.now() - (CLEANUP_DAYS * 86400000);

            let cursor = await new Promise((resolve, reject) => {
                const request = index.openCursor(IDBKeyRange.upperBound(threshold));
                request.onsuccess = (event) => resolve(event.target.result);
                request.onerror = (event) => reject(event.target.error);
            });

            while (cursor) {
                store.delete(cursor.primaryKey);
                cursor = await new Promise((resolve, reject) => {
                    cursor.continue();
                    cursor.onsuccess = (event) => resolve(event.target.result);
                    cursor.onerror = (event) => reject(event.target.error);
                });
            }
        } catch (error) {
            console.error("Cleanup failed:", error);
        }
    };

    const saveInput = debounce(async (id, value) => {
        try {
            const db = await openDatabase();
            const transaction = db.transaction(storeName, "readwrite");
            const store = transaction.objectStore(storeName);
            store.put({
                id,
                value,
                timestamp: Date.now()
            });
        } catch (error) {
            console.error("Error saving input:", error);
        }
    }, DEBOUNCE_TIME);

    // Handle contenteditable elements
    const handleRichText = (element) => {
        const id = element.id || element.dataset.guardianId ||
                 Math.random().toString(36).substr(2, 9);

        element.dataset.guardianId = id;
        return id;
    };

    // User controls
    const addControls = () => {
        // Add menu command
        if (typeof GM_registerMenuCommand === 'function') {
            GM_registerMenuCommand("Clear Saved Inputs", async () => {
                try {
                    const db = await openDatabase();
                    const transaction = db.transaction(storeName, "readwrite");
                    const store = transaction.objectStore(storeName);
                    await store.clear();
                    showFeedback('Cleared all saved inputs!');
                } catch (error) {
                    console.error("Clear failed:", error);
                    showFeedback('Failed to clear inputs');
                }
            });
        }

        // Add hotkey (Ctrl+Shift+Z)
        document.addEventListener('keydown', (e) => {
            if (e.ctrlKey && e.shiftKey && e.key === 'Z') {
                indexedDB.deleteDatabase(dbName);
                showFeedback('Database reset! Please reload the page.');
            }
        });
    };

    // Visual feedback
    const showFeedback = (message) => {
        const existing = document.getElementById('guardian-feedback');
        if (existing) existing.remove();

        const div = document.createElement('div');
        div.id = 'guardian-feedback';
        div.style = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            padding: 10px 20px;
            background: #4CAF50;
            color: white;
            border-radius: 5px;
            z-index: 9999;
            opacity: 1;
            transition: opacity 0.5s ease-in-out;
        `;
        div.textContent = message;
        document.body.appendChild(div);

        // Fade out and remove after 2 seconds
        setTimeout(() => {
            div.style.opacity = '0';
            setTimeout(() => div.remove(), 500);
        }, 2000);
    };

    // Enhanced input detection
    const isValidInput = (input) => {
        const isHidden = input.offsetParent === null ||
                       window.getComputedStyle(input).visibility === 'hidden';

        return !isHidden &&
               input.type !== 'password' &&
               input.autocomplete !== 'off' &&
               !isSensitiveField(input);
    };

    // Modified event listener with contenteditable support
    document.addEventListener('input', (event) => {
        const target = event.target;
        let id, value;

        if (target.isContentEditable) {
            id = handleRichText(target);
            value = target.innerHTML;
        } else if (['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) {
            id = target.id || target.name;
            value = target.value;
        }

        if (id && isValidInput(target)) {
            saveInput(id, value);
        }
    });

    // Initialize
    window.addEventListener('load', () => {
        restoreInputs();
        addControls();
        cleanupOldEntries();
    });

    // Modified restore function with timestamp
    const restoreInputs = async () => {
        try {
            const db = await openDatabase();
            const transaction = db.transaction(storeName, "readonly");
            const store = transaction.objectStore(storeName);
            const request = store.getAll();

            request.onsuccess = () => {
                let restoredCount = 0;
                request.result.forEach(({ id, value }) => {
                    const input = document.querySelector(`[id="${id}"], [name="${id}"]`);
                    if (input && isValidInput(input)) {
                        if (input.isContentEditable) {
                            input.innerHTML = value;
                        } else {
                            input.value = value;
                        }
                        restoredCount++;
                    }
                });
                if (restoredCount > 0) {
                    showFeedback(`Restored ${restoredCount} input${restoredCount !== 1 ? 's' : ''}!`);
                }
            };
        } catch (error) {
            console.error("Restore failed:", error);
        }
    };

})();