Input Guardian

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

  1. // ==UserScript==
  2. // @name Input Guardian
  3. // @namespace https://spin.rip/
  4. // @match *://*/*
  5. // @grant GM_registerMenuCommand
  6. // @grant GM_notification
  7. // @version 2.0
  8. // @author Spinfal
  9. // @description Enhanced input preservation with security checks, performance optimizations, and user controls.
  10. // @license AGPL-3.0 License
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const dbName = "InputGuardianDB";
  17. const storeName = "inputs";
  18. const dbVersion = 2;
  19. const DEBOUNCE_TIME = 500; // ms
  20. const CLEANUP_DAYS = 30;
  21. const SENSITIVE_NAMES = /(ccnum|creditcard|cvv|ssn|sin|securitycode)/i;
  22.  
  23. const cache = new Map();
  24.  
  25. const isSensitiveField = (input) => {
  26. return SENSITIVE_NAMES.test(input.id) ||
  27. SENSITIVE_NAMES.test(input.name) ||
  28. input.closest('[data-sensitive="true"]');
  29. };
  30.  
  31. const debounce = (func, wait) => {
  32. let timeout;
  33. return (...args) => {
  34. clearTimeout(timeout);
  35. timeout = setTimeout(() => func.apply(this, args), wait);
  36. };
  37. };
  38.  
  39. const openDatabase = () => {
  40. return new Promise((resolve, reject) => {
  41. const request = indexedDB.open(dbName, dbVersion);
  42.  
  43. request.onupgradeneeded = (event) => {
  44. const db = event.target.result;
  45. let store;
  46. if (!db.objectStoreNames.contains(storeName)) {
  47. store = db.createObjectStore(storeName, { keyPath: "id" });
  48. } else {
  49. store = request.transaction.objectStore(storeName);
  50. }
  51. if (!store.indexNames.contains("timestamp")) {
  52. store.createIndex("timestamp", "timestamp", { unique: false });
  53. }
  54. };
  55.  
  56. request.onsuccess = (event) => resolve(event.target.result);
  57. request.onerror = (event) => reject(event.target.error);
  58. });
  59. };
  60.  
  61. const cleanupOldEntries = async () => {
  62. try {
  63. const db = await openDatabase();
  64. const transaction = db.transaction(storeName, "readwrite");
  65. const store = transaction.objectStore(storeName);
  66. const index = store.index("timestamp");
  67. const threshold = Date.now() - (CLEANUP_DAYS * 86400000);
  68.  
  69. return new Promise((resolve, reject) => {
  70. const request = index.openCursor(IDBKeyRange.upperBound(threshold));
  71. request.onsuccess = (event) => {
  72. const cursor = event.target.result;
  73. if (cursor) {
  74. cursor.delete();
  75. cursor.continue();
  76. } else {
  77. resolve();
  78. }
  79. };
  80. request.onerror = reject;
  81. });
  82. } catch (error) {
  83. console.error("Cleanup failed:", error);
  84. }
  85. };
  86.  
  87. const saveInput = debounce(async (id, value) => {
  88. cache.set(id, value);
  89. try {
  90. const db = await openDatabase();
  91. const transaction = db.transaction(storeName, "readwrite");
  92. const store = transaction.objectStore(storeName);
  93. store.put({
  94. id,
  95. value,
  96. timestamp: Date.now()
  97. });
  98. } catch (error) {
  99. console.error("Error saving input:", error);
  100. }
  101. }, DEBOUNCE_TIME);
  102.  
  103. const handleRichText = (element) => {
  104. const id = element.id || element.dataset.guardianId ||
  105. Math.random().toString(36).substr(2, 9);
  106. element.dataset.guardianId = id;
  107. return id;
  108. };
  109.  
  110. const addControls = () => {
  111. if (typeof GM_registerMenuCommand === 'function') {
  112. GM_registerMenuCommand("Clear Saved Inputs", async () => {
  113. try {
  114. const db = await openDatabase();
  115. const transaction = db.transaction(storeName, "readwrite");
  116. const store = transaction.objectStore(storeName);
  117. await store.clear();
  118. cache.clear();
  119. showFeedback('Cleared all saved inputs!');
  120. } catch (error) {
  121. console.error("Clear failed:", error);
  122. showFeedback('Failed to clear inputs');
  123. }
  124. });
  125. }
  126.  
  127. document.addEventListener('keydown', (e) => {
  128. if (e.ctrlKey && e.shiftKey && e.key === 'Z') {
  129. indexedDB.deleteDatabase(dbName);
  130. cache.clear();
  131. showFeedback('Database reset! Please reload the page.');
  132. }
  133. });
  134. };
  135.  
  136. const showFeedback = (message) => {
  137. const existing = document.getElementById('guardian-feedback');
  138. if (existing) existing.remove();
  139.  
  140. const div = document.createElement('div');
  141. div.id = 'guardian-feedback';
  142. div.style = `
  143. position: fixed;
  144. bottom: 20px;
  145. right: 20px;
  146. padding: 10px 20px;
  147. background: #4CAF50;
  148. color: white;
  149. border-radius: 5px;
  150. z-index: 9999;
  151. opacity: 1;
  152. transition: opacity 0.5s ease-in-out;
  153. `;
  154. div.textContent = message;
  155. document.body.appendChild(div);
  156.  
  157. setTimeout(() => {
  158. div.style.opacity = '0';
  159. setTimeout(() => div.remove(), 500);
  160. }, 2000);
  161. };
  162.  
  163. const isValidInput = (input) => {
  164. const isHidden = input.offsetParent === null ||
  165. window.getComputedStyle(input).visibility === 'hidden';
  166.  
  167. return !isHidden &&
  168. input.type !== 'password' &&
  169. input.autocomplete !== 'off' &&
  170. !isSensitiveField(input);
  171. };
  172.  
  173. document.addEventListener('input', (event) => {
  174. if (['INPUT', 'TEXTAREA', 'SELECT'].includes(event.target.tagName) || event.target.isContentEditable) {
  175. const target = event.target;
  176. let id, value;
  177.  
  178. if (target.isContentEditable) {
  179. id = handleRichText(target);
  180. value = target.innerHTML;
  181. } else {
  182. id = target.id || target.name;
  183. value = target.value;
  184. }
  185.  
  186. if (id && isValidInput(target)) {
  187. saveInput(id, value);
  188. }
  189. }
  190. });
  191.  
  192. window.addEventListener('load', () => {
  193. restoreInputs();
  194. addControls();
  195. setTimeout(cleanupOldEntries, 5000); // Defer cleanup
  196. });
  197.  
  198. const restoreInputs = async () => {
  199. try {
  200. const inputs = document.querySelectorAll('input, textarea, select, [contenteditable]');
  201. const inputMap = new Map(Array.from(inputs).map(input => [input.id || input.name, input]));
  202.  
  203. // First restore from cache
  204. for (const [id, value] of cache.entries()) {
  205. const input = inputMap.get(id);
  206. if (input && isValidInput(input)) {
  207. if (input.isContentEditable) {
  208. input.innerHTML = value;
  209. } else {
  210. input.value = value;
  211. }
  212. }
  213. }
  214.  
  215. // Then restore from IndexedDB
  216. const db = await openDatabase();
  217. const transaction = db.transaction(storeName, "readonly");
  218. const store = transaction.objectStore(storeName);
  219. const request = store.getAll();
  220.  
  221. request.onsuccess = () => {
  222. let restoredCount = 0;
  223. request.result.forEach(({ id, value }) => {
  224. if (!cache.has(id)) {
  225. const input = inputMap.get(id);
  226. if (input && isValidInput(input)) {
  227. if (input.isContentEditable) {
  228. input.innerHTML = value;
  229. } else {
  230. input.value = value;
  231. }
  232. cache.set(id, value);
  233. restoredCount++;
  234. }
  235. }
  236. });
  237. if (restoredCount > 0) {
  238. showFeedback(`Restored ${restoredCount} input${restoredCount !== 1 ? 's' : ''}!`);
  239. }
  240. };
  241. } catch (error) {
  242. console.error("Restore failed:", error);
  243. }
  244. };
  245.  
  246. })();