Website Changes Monitor

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.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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);
    }
  });
})();