Files
statdx/scrapers/statdx_passive_capture.user.js
T
2026-07-15 21:36:23 +01:00

376 lines
13 KiB
JavaScript

// ==UserScript==
// @name STATdx Passive Capture Link
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Captures STATdx content (JSON/HTML/images) passively and sends it to the central cache server.
// @author Antigravity
// @match https://app.statdx.com/*
// @grant GM_xmlhttpRequest
// @connect localhost
// @connect *
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// CHANGE THIS URL to the IP address or hostname of your central cache server if running on remote machines
// e.g., "http://192.168.1.50:8000"
const CACHE_SERVER_URL = "http://localhost:8000";
const capturedUrls = new Set();
function shouldCapture(url, contentType) {
try {
if (!url) return false;
const lowerUrl = url.toLowerCase();
const lowerContentType = (contentType || "").toLowerCase();
// Always capture known document content / summary endpoints
if (lowerUrl.includes('/document/content/') || lowerUrl.includes('/document/summary/')) {
return true;
}
// Check if contentType indicates target resource types
if (lowerContentType.includes('application/json') ||
lowerContentType.startsWith('text/') ||
lowerContentType.includes('javascript') ||
lowerContentType.startsWith('image/')) {
return true;
}
} catch (e) {}
return false;
}
// Helper: Compute SHA-256 hash of an ArrayBuffer using Web Crypto API
async function getSha256(buffer) {
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
// Helper: Convert ArrayBuffer to Base64 string
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
// Helper: Parse response headers string to dict
function parseHeaders(headersString) {
const headers = {};
try {
if (!headersString) return headers;
const lines = headersString.split('\r\n');
lines.forEach(line => {
const parts = line.split(':');
if (parts.length >= 2) {
const key = parts[0].trim().toLowerCase();
const value = parts.slice(1).join(':').trim();
headers[key] = value;
}
});
} catch (e) {}
return headers;
}
// Helper: Get Headers dict from fetch Headers object
function getHeadersDict(headers) {
const dict = {};
try {
if (!headers) return dict;
if (typeof headers.forEach === 'function') {
headers.forEach((val, key) => {
dict[key.toLowerCase()] = val;
});
} else {
for (const [key, val] of Object.entries(headers)) {
dict[key.toLowerCase()] = val;
}
}
} catch (e) {}
return dict;
}
// Helper: Extract buffer from XHR
async function getXhrBuffer(xhr) {
try {
if (xhr.responseType === 'arraybuffer') {
return xhr.response;
}
if (xhr.responseType === 'blob') {
return await xhr.response.arrayBuffer();
}
if (typeof xhr.response === 'string' || !xhr.responseType || xhr.responseType === 'text') {
const text = xhr.responseText || xhr.response;
if (text) {
return new TextEncoder().encode(text).buffer;
}
}
} catch (e) {}
return null;
}
// Server request: check if server already has hash
function checkServerHash(contentHash) {
return new Promise((resolve) => {
try {
GM_xmlhttpRequest({
method: "POST",
url: `${CACHE_SERVER_URL}/api/check-hash`,
headers: { "Content-Type": "application/json" },
data: JSON.stringify({ content_hash: contentHash }),
onload: function(res) {
if (res.status === 200) {
try {
const data = JSON.parse(res.responseText);
resolve(data.exists);
} catch (e) {
resolve(false);
}
} else {
resolve(false);
}
},
onerror: function() {
resolve(false);
}
});
} catch (e) {
resolve(false);
}
});
}
// Server request: upload capture payload
function sendToServer(payload) {
return new Promise((resolve, reject) => {
try {
GM_xmlhttpRequest({
method: "POST",
url: `${CACHE_SERVER_URL}/api/capture`,
headers: { "Content-Type": "application/json" },
data: JSON.stringify(payload),
onload: function(res) {
if (res.status === 200) {
resolve(res.responseText);
} else {
reject(new Error(`Server error`));
}
},
onerror: function(err) {
reject(err);
}
});
} catch (e) {
reject(e);
}
});
}
// Core processing function
async function processBuffer(url, resourceType, status, buffer, contentType, headers) {
try {
if (!shouldCapture(url, contentType)) {
return;
}
const contentHash = await getSha256(buffer);
// Deduplication check: check server cache
const exists = await checkServerHash(contentHash);
if (exists) {
return;
}
// Convert to base64 and send
const bodyBase64 = arrayBufferToBase64(buffer);
const payload = {
url: url,
resource_type: resourceType,
status: status,
timestamp: new Date().toISOString(),
headers: headers || {},
body_base64: bodyBase64,
content_type: contentType
};
await sendToServer(payload);
} catch (e) {
// Fail completely silently to prevent stack trace leaks
}
}
// Helper: Masquerade overridden functions as native code (toString spoofing)
function masqueradeAsNative(customFn, originalFn) {
try {
Object.defineProperty(customFn, 'toString', {
value: function() {
return `function ${originalFn.name || 'fetch'}() { [native code] }`;
},
writable: false,
enumerable: false,
configurable: true
});
} catch (e) {}
}
// --- INTERCEPT FETCH ---
const originalFetch = window.fetch;
const myFetch = async function(...args) {
const response = await originalFetch.apply(this, args);
try {
const url = response.url;
const contentType = response.headers.get("content-type") || "";
if (shouldCapture(url, contentType)) {
const clone = response.clone();
(async () => {
try {
const buffer = await clone.arrayBuffer();
const headers = getHeadersDict(clone.headers);
await processBuffer(url, "fetch", clone.status, buffer, contentType, headers);
} catch (e) {}
})();
}
} catch (e) {}
return response;
};
masqueradeAsNative(myFetch, originalFetch);
try {
Object.defineProperty(window, 'fetch', {
value: myFetch,
writable: true,
configurable: true,
enumerable: true
});
} catch (e) {
window.fetch = myFetch;
}
// --- INTERCEPT XHR ---
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
const myOpen = function(method, url) {
try {
this._url = url;
} catch (e) {}
return originalOpen.apply(this, arguments);
};
masqueradeAsNative(myOpen, originalOpen);
try {
Object.defineProperty(XMLHttpRequest.prototype, 'open', {
value: myOpen,
writable: true,
configurable: true,
enumerable: true
});
} catch (e) {
XMLHttpRequest.prototype.open = myOpen;
}
const mySend = function() {
try {
this.addEventListener('load', function() {
try {
const url = this._url;
const contentType = this.getResponseHeader("content-type") || "";
if (shouldCapture(url, contentType)) {
const xhr = this;
(async () => {
try {
const buffer = await getXhrBuffer(xhr);
if (buffer) {
const headers = parseHeaders(xhr.getAllResponseHeaders());
await processBuffer(url, "xhr", xhr.status, buffer, contentType, headers);
}
} catch (e) {}
})();
}
} catch (e) {}
});
} catch (e) {}
return originalSend.apply(this, arguments);
};
masqueradeAsNative(mySend, originalSend);
try {
Object.defineProperty(XMLHttpRequest.prototype, 'send', {
value: mySend,
writable: true,
configurable: true,
enumerable: true
});
} catch (e) {
XMLHttpRequest.prototype.send = mySend;
}
// --- INTERCEPT STATIC IMAGES IN THE DOM ---
function captureImageByUrl(url) {
try {
GM_xmlhttpRequest({
method: "GET",
url: url,
responseType: "arraybuffer",
onload: async function(details) {
try {
if (details.status !== 200) return;
const buffer = details.response;
if (!buffer) return;
const contentType = details.responseHeaders.match(/content-type:\s*(.*)/i)?.[1] || "image/png";
const headers = parseHeaders(details.responseHeaders);
await processBuffer(url, "image", details.status, buffer, contentType, headers);
} catch (e) {}
}
});
} catch (e) {}
}
function scanImages() {
try {
const images = document.querySelectorAll('img');
images.forEach(img => {
const src = img.src;
if (!src) return;
// Focus on STATdx / Elsevier images and skip local or non-related URLs
const isTarget = src.includes('statdx') || src.includes('elsevier') || src.startsWith('/');
if (isTarget && !capturedUrls.has(src)) {
capturedUrls.add(src);
captureImageByUrl(src);
}
});
} catch (e) {}
}
// Set up MutationObserver to dynamically capture images added by page scripts
try {
const observer = new MutationObserver((mutations) => {
scanImages();
});
// Wait for DOM to be ready to observe
if (document.body) {
scanImages();
observer.observe(document.body, { childList: true, subtree: true });
} else {
document.addEventListener('DOMContentLoaded', () => {
scanImages();
observer.observe(document.body, { childList: true, subtree: true });
});
}
} catch (e) {}
// Periodic sweep as a backup
setInterval(scanImages, 4000);
})();