您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Intercept and log URL bookmark requests in GGather
// ==UserScript== // @name GGather Bookmark Fixer // @namespace http://tampermonkey.net/ // @version 1.0 // @description Intercept and log URL bookmark requests in GGather // @author dudematthew // @match https://web.ggather.com/* // @match https://core.ggather.com/* // @grant GM_xmlhttpRequest // @connect * // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; // Styling for console logs const logStyle = { main: 'font-size: 16px; color: #4CAF50; font-weight: bold; padding: 5px; background: #f0f0f0;', url: 'font-size: 14px; color: #2196F3; padding: 3px; background: #f8f8f8;', warning: 'font-size: 14px; color: #FF5722; font-weight: bold; padding: 3px;', error: 'font-size: 14px; color: #FF0000; font-weight: bold; padding: 3px;', debug: 'font-size: 13px; color: #9C27B0; padding: 2px;' }; function debugLog(title, data = null, style = logStyle.debug) { console.log('%c[DEBUG] ' + title, style); if (data) { console.log('Data:', data); if (data instanceof Error) { console.log('Stack:', data.stack); } } } // Store original XHR methods const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; // Track request URLs, methods and metadata const requestUrls = new WeakMap(); const requestMethods = new WeakMap(); // Function to extract metadata async function extractMetadata(targetUrl) { debugLog('Fetching metadata for', targetUrl); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: targetUrl, headers: { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'User-Agent': navigator.userAgent }, onload: function (response) { try { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, 'text/html'); // 1. Find best thumbnail first let thumbnail = doc.querySelector('meta[property="og:image"]')?.content || doc.querySelector('meta[name="twitter:image"]')?.content || doc.querySelector('link[rel*="icon"][sizes="192x192"]')?.href || doc.querySelector('link[rel*="icon"][sizes="128x128"]')?.href || Array.from(doc.querySelectorAll('img')) .find(img => img.width > 100 && img.height > 100)?.src || null; // 2. Get title const title = doc.querySelector('title')?.textContent || doc.querySelector('meta[property="og:title"]')?.content || null; // 3. Get description const description = doc.querySelector('meta[name="description"]')?.content || doc.querySelector('meta[property="og:description"]')?.content || null; // Collect additional metadata const html_images = Array.from(doc.querySelectorAll('img')).map(img => ({ src: img.src, width: img.width, height: img.height, alt: img.alt })); const html_icons = Array.from(doc.querySelectorAll('link[rel*="icon"]')).map(icon => ({ href: icon.href, rel: icon.rel, sizes: icon.sizes?.value })); const html_og = Array.from(doc.querySelectorAll('meta[property^="og:"]')).map(og => ({ property: og.getAttribute('property'), content: og.content })); const html_meta = Array.from(doc.querySelectorAll('meta')).map(meta => ({ name: meta.name, content: meta.content })); const metadata = { url: targetUrl, thumbnail: thumbnail, // Thumbnail first title: title, // Title second description: description, // Description third html_images: html_images, html_icons: html_icons, html_og: html_og, html_meta: html_meta, headers: response.responseHeaders, is_webpage: true }; debugLog('Extracted metadata', metadata); resolve(metadata); } catch (e) { debugLog('Failed to extract metadata', e); reject(e); } }, onerror: function (error) { debugLog('Failed to fetch URL', error); reject(error); } }); }); } // Add this at the top with other constants let lastSuccessfulEditRequest = null; // Remove the CSRF token capture from XHR since we can't access those headers XMLHttpRequest.prototype.open = function (method, url) { requestUrls.set(this, url); requestMethods.set(this, method); debugLog('XHR Open', { method, url, isBookmark: url.includes('urlbookmark'), isAuth: url.includes('auth') || url.includes('login') }); return originalOpen.apply(this, arguments); }; // Function to find Vue instance root function findVueRoot() { // Look for Vue instance in common root element IDs const rootElements = ['#app', '#__nuxt', '#__layout', '#root']; for (const selector of rootElements) { const el = document.querySelector(selector); if (el && el.__vue__) { return el.__vue__.$root; } } // Fallback: search all elements for Vue instance const elements = document.getElementsByTagName('*'); for (const el of elements) { if (el.__vue__) { return el.__vue__.$root; } } return null; } // Modify the callOriginalUpdate function to handle thumbnails async function callOriginalUpdate(bookmarkId, metadata) { debugLog('🎯 Starting update attempt', { bookmarkId, metadata }, logStyle.warning); const saveurl = Array.from(document.querySelectorAll('*')) .find(el => el.__vue__?.$options?.name === 'saveurl')?.__vue__; if (!saveurl) { debugLog('❌ Could not find saveurl component', null, logStyle.error); return; } // Extract metadata from our fetch const urldata = await extractMetadata(metadata.url); debugLog('Extracted metadata for update', urldata); try { // First set the URL and trigger validation saveurl.url = metadata.url; await saveurl.validateBasicURL(); // Update thumbnail first if (urldata.thumbnail) { debugLog('Updating thumbnail...'); try { await saveurl.$axios({ url: saveurl.$store.state.api + "/edit-urlbookmark-thumb/", method: "post", data: { url: metadata.url, image_url: urldata.thumbnail, thumbnail_worn: 'self' } }); // Update the store after successful upload saveurl.$store.commit("eventSaveThumbChange", { thumbnail: urldata.thumbnail, thumbnail_worn: 'self' }); debugLog('✅ Thumbnail update completed'); // Add a small delay after thumbnail update await new Promise(resolve => setTimeout(resolve, 1000)); } catch (e) { debugLog('❌ Failed to update thumbnail', e, logStyle.error); } } // Then update title if (urldata.title) { debugLog('Updating title...'); // Set title in urlbookmark saveurl.urlbookmark = { pk: bookmarkId, url: metadata.url, title: urldata.title, rating: metadata.rating, owner_notes: metadata.owner_notes }; await saveurl.editURLBookmark('title'); await new Promise(resolve => setTimeout(resolve, 1000)); debugLog('Title update completed'); } // Finally update description if (urldata.description) { debugLog('Updating description...'); // Set description in urlbookmark saveurl.urlbookmark = { ...saveurl.urlbookmark, // Keep previous data description: urldata.description }; await saveurl.editURLBookmark('description'); debugLog('Description update completed'); } debugLog('✅ All updates completed', { thumbnail: urldata.thumbnail, title: urldata.title, description: urldata.description }, logStyle.success); } catch (e) { debugLog('❌ Update failed', e, logStyle.error); debugLog('Component state at failure', { url: saveurl.url, urlbookmark: saveurl.urlbookmark, methods: Object.keys(saveurl.$options.methods) }); } } // Update the bookmark creation handler to use the original update function XMLHttpRequest.prototype.send = async function (data) { const url = requestUrls.get(this); const method = requestMethods.get(this); if (url.includes('edit-urlbookmark-thumb')) { debugLog('🖼️ Thumbnail request intercepted', { url, method, data: data instanceof FormData ? Object.fromEntries(data.entries()) : data, stack: new Error().stack // This will show us the call stack }, logStyle.warning); } // Detailed logging for thumbnail requests if (url.includes('thumb')) { debugLog('🖼️ Thumbnail request intercepted', { url, method, data: data instanceof FormData ? Object.fromEntries(data.entries()) : data, headers: Array.from(arguments) }); // Store successful thumbnail requests for analysis this.addEventListener('load', function () { if (this.status === 200) { debugLog('🖼️ Successful thumbnail request', { response: this.responseText, response: this.responseText, headers: this.getAllResponseHeaders() }); } }); } // Log all requests related to thumbnails or bookmarks if (url.includes('thumb') || url.includes('bookmark')) { debugLog('Request intercepted', { url, method, data, headers: this.getAllResponseHeaders?.(), requestHeaders: Array.from(arguments) }); } try { const urlObj = new URL(url); // Handle metadata requests (keep this part) if (urlObj.pathname === '/api/get-urldata/') { const targetUrl = urlObj.searchParams.get('url'); debugLog('URL data request for', targetUrl); // Extract metadata const urldata = await extractMetadata(targetUrl); debugLog('Extracted metadata', urldata); // Create response const mockResponse = { urldata: urldata }; debugLog('Sending URL data response', mockResponse); // Set response properties Object.defineProperties(this, { 'status': { value: 200, writable: true }, 'statusText': { value: 'OK', writable: true }, 'responseText': { value: JSON.stringify(mockResponse), writable: true }, 'readyState': { value: 4, writable: true }, 'response': { value: JSON.stringify(mockResponse), writable: true } }); // Set headers this.getAllResponseHeaders = () => 'content-type: application/json'; this.getResponseHeader = (name) => name.toLowerCase() === 'content-type' ? 'application/json' : null; // Trigger events setTimeout(() => { const readyStateEvent = new Event('readystatechange'); const loadEvent = new Event('load'); this.dispatchEvent(readyStateEvent); this.dispatchEvent(loadEvent); debugLog('URL data response sent'); }, 0); return; } // Handle successful bookmark creation if (urlObj.pathname === '/api/add-urlbookmark/') { this.addEventListener('load', function () { if (this.status === 200) { try { const response = JSON.parse(this.responseText); debugLog('Bookmark saved', response); // Use original update function instead of our own callOriginalUpdate(response.pk, response); } catch (e) { debugLog('Failed to handle save response', e); } } }); } // Capture edit requests only if (url.includes('edit-urlbookmark') || url.includes('urlbookmark') && (method === 'PATCH' || method === 'PUT')) { const headers = {}; const originalSetRequestHeader = this.setRequestHeader; // Capture headers being set this.setRequestHeader = function (name, value) { headers[name] = value; return originalSetRequestHeader.apply(this, arguments); }; debugLog('Edit request detected', { url, method, headers, data }); // Add response handler this.addEventListener('load', function () { if (this.status === 200) { lastSuccessfulEditRequest = { url, method, headers, data, response: this.responseText }; debugLog('Captured successful edit request', lastSuccessfulEditRequest); } }); } return originalSend.apply(this, arguments); } catch (e) { debugLog('Error in send override', e); return originalSend.apply(this, arguments); } }; // Also intercept fetch for auth requests const originalFetch = window.fetch; window.fetch = async function (url, options) { if (typeof url === 'string' && (url.includes('auth') || url.includes('login'))) { debugLog('Fetch auth request intercepted', { url, options }); const response = await originalFetch.apply(this, arguments); const clonedResponse = response.clone(); try { const data = await clonedResponse.json(); debugLog('Fetch auth response', { url, data, headers: Array.from(response.headers.entries()) }); } catch (e) { debugLog('Failed to parse fetch auth response', e); } return response; } return originalFetch.apply(this, arguments); }; debugLog('GGather Bookmark Fixer is active', { version: GM_info.script.version, lastUpdate: new Date().toISOString() }, logStyle.main); // Add this function to find Nuxt/Axios config function findAxiosConfig() { // Try to find Nuxt instance const nuxtApp = window?.__NUXT__ || window?.$nuxt; if (nuxtApp) { debugLog('Found Nuxt app', nuxtApp); // Try to find Axios config const axiosConfig = nuxtApp?.$axios?.defaults || nuxtApp?.context?.$axios?.defaults || window?.$axios?.defaults; if (axiosConfig) { debugLog('Found Axios config', axiosConfig); return axiosConfig; } } // Try to find Vue instance and its Axios config const vueRoot = findVueRoot(); if (vueRoot) { const axiosConfig = vueRoot?.$axios?.defaults || vueRoot?.$options?.axios?.defaults; if (axiosConfig) { debugLog('Found Axios config in Vue root', axiosConfig); return axiosConfig; } } return null; } // Call this when page loads document.addEventListener('DOMContentLoaded', () => { const axiosConfig = findAxiosConfig(); if (axiosConfig) { debugLog('Will use these Axios defaults for our requests', axiosConfig); } }); })();