您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A powerful and flexible monitor that automatically detects changes on any website. Including support for POST requests and even complex pages that require dynamic security tokens (nonces/CSRF) to view content.
// ==UserScript== // @name Website Changes Monitor // @namespace https://greasyfork.org/en/users/670188-hacker09 // @version 1 // @description A powerful and flexible monitor that automatically detects changes on any website. Including support for POST requests and even complex pages that require dynamic security tokens (nonces/CSRF) to view content. // @author hacker09 // @match *://*/* // @icon https://i.imgur.com/0kx5i9q.png // @grant GM_getValue // @grant GM_setValue // @grant GM_openInTab // @grant GM_listValues // @grant GM_deleteValue // @grant GM.xmlHttpRequest // @grant GM_registerMenuCommand // @connect * // ==/UserScript== (function() { 'use strict'; const now = Date.now(); //Capture a single timestamp for this run to prevent race conditions between tabs GM_registerMenuCommand('Add/Remove Website', () => { GM_openInTab('https://cyber-sec0.github.io/monitor/', { active: true }); }); unsafeWindow.gm_storage = { setValue: GM_setValue, getValue: GM_getValue, deleteValue: GM_deleteValue, listValues: GM_listValues }; //Expose the TM Storage const performCheck = (key, dataForRequest, originalData) => { //key: The storage key; dataForRequest: Data for the HTTP call (may include a token); originalData: The original stored data to modify upon completion GM.xmlHttpRequest({ method: dataForRequest.body ? 'POST' : 'GET', url: key.replace(/Counter\d+/, ''), headers: dataForRequest.header || {}, data: dataForRequest.body || null, onload: (response) => { if (response.status < 200 || response.status > 299) { return; } //Ignore failed requests const { responseText } = response; const oldContent = originalData.content || originalData.HTML || ''; const urlToOpen = originalData.website || key.replace(/Counter\d+/, ''); let contentForStorage = responseText, triggerAlert = false; const parser = new DOMParser(), doc = parser.parseFromString(responseText, 'text/html'); if (originalData.comparisonMethod === 'new_items' && originalData.selector && originalData.idAttribute) { //Comparison Method 1: Check for new items in a list based on a unique ID const oldIdSet = new Set(oldContent ? oldContent.split(',') : []); //FIX: Support using 'innerText' as a special case for the ID, otherwise use getAttribute. Trim the result for consistency. const newIds = Array.from(doc.querySelectorAll(originalData.selector)).map(el => (originalData.idAttribute.toLowerCase() === 'innertext' ? el.innerText : el.getAttribute(originalData.idAttribute))?.trim()).filter(Boolean); if (oldContent && newIds.some(id => !oldIdSet.has(id))) { triggerAlert = true; } //Trigger if any new item's ID is not found in the old set of IDs contentForStorage = newIds.join(','); } else if (originalData.comparisonMethod === 'order' && originalData.selector && originalData.idAttribute) { //Comparison Method 2: Check if the order of items in a list has changed const newIdString = Array.from(doc.querySelectorAll(originalData.selector)).map(el => (originalData.idAttribute.toLowerCase() === 'innertext' ? el.innerText : el.getAttribute(originalData.idAttribute))?.trim()).filter(Boolean).join(','); if (oldContent && oldContent !== newIdString) { triggerAlert = true; } contentForStorage = newIdString; } else { //Default Comparison Method: Check for any change in the selected element's HTML or the full page if (originalData.selector) { if (doc.querySelector(originalData.selector)) { contentForStorage = doc.querySelector(originalData.selector).innerHTML; } //If specified, only use the user's chosen selector to compare } const sanitize = (html) => { if (!html) return ''; const t = document.createElement('div'); t.innerHTML = html; return (t.textContent || t.innerText || "").replace(/\s+/g, ' ').trim();}; //Helper function to strip HTML tags and normalize whitespace for a more reliable text comparison if (oldContent && sanitize(oldContent) !== sanitize(contentForStorage)) { triggerAlert = true; } } if (triggerAlert) { if (Date.now() - GM_getValue(`lock_open_${urlToOpen}`, 0) > 15000) { //Prevent re-opening the same URL for 15s GM_setValue(`lock_open_${urlToOpen}`, Date.now()); //Set the lock immediately to prevent other tabs from opening the URL GM_openInTab(urlToOpen, { active: false, insert: false }); } } GM_setValue(key, { ...originalData, content: contentForStorage, lastChecked: now }); //Save the new content, preserving the timestamp that was set before the check }, }); }; GM_listValues().forEach(key => { if (/^check_interval_ms$|^lock_/.test(key)) { return; } //Skip looping through special config/lock keys const storedData = GM_getValue(key, {}); if (!storedData || storedData.isPaused) { return; } if (now - (storedData.lastChecked || 0) < GM_getValue('check_interval_ms', 60000)) { return; } GM_setValue(key, { ...storedData, lastChecked: now }); //Immediately update the timestamp in storage to prevent other tabs from running the same check if (storedData.tokenEnabled && storedData.tokenUrl && storedData.tokenSelector && storedData.tokenPlaceholder) { //Handle token-based requests GM.xmlHttpRequest({ method: 'GET', url: storedData.tokenUrl, onload: (response) => { if (response.status < 200 || response.status > 299) { return; } //Ignore failed requests const { responseText } = response, parser = new DOMParser(), doc = parser.parseFromString(responseText, 'text/html'), elements = doc.querySelectorAll(storedData.tokenSelector); let nonce = null; for (const element of elements) { //Search for the token using the specified method (RegEx, attribute, or text content) if (storedData.tokenRegEx) { const match = element.textContent.match(new RegExp(storedData.tokenRegEx)); if (match && match[1]) { nonce = match[1]; break; } } else { nonce = storedData.tokenAttribute ? element.getAttribute(storedData.tokenAttribute) : element.textContent.trim(); if(nonce) break; } } if (nonce) { const dataWithToken = JSON.parse(JSON.stringify(storedData)); //Deep clone the data object so the original isn't modified by token injection const placeholder = new RegExp(storedData.tokenPlaceholder.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'); //Create a RegExp from the placeholder, escaping regex chars if (dataWithToken.body) { dataWithToken.body = dataWithToken.body.replace(placeholder, nonce); } if (dataWithToken.header) { for (const hKey in dataWithToken.header) { if (typeof dataWithToken.header[hKey] === 'string') { dataWithToken.header[hKey] = dataWithToken.header[hKey].replace(placeholder, nonce); } } } performCheck(key, dataWithToken, storedData); //Pass both the temp data for the request and the original data for saving } }, }); } else { //Handle non-token-based requests performCheck(key, storedData, storedData); } }); })();