JSON Response Capture

Capture and save JSON responses from web requests with URL filtering

Από την 22/12/2024. Δείτε την τελευταία έκδοση.

// ==UserScript==
// @name         JSON Response Capture
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Capture and save JSON responses from web requests with URL filtering
// @author       nickm8
// @license      MIT
// @match        *://*/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js
// ==/UserScript==

(function() {
    'use strict';

    // Additional styles for configuration UI
    const style = document.createElement('style');
    style.textContent = `
        /* Previous styles remain the same */
        .json-capture-panel {
            position: fixed;
            bottom: 1rem;
            right: 1rem;
            background: white;
            border-radius: 0.5rem;
            box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
            transition: all 200ms;
            z-index: 10000;
        }
        .json-capture-panel.minimized {
            width: 3rem;
        }
        .json-capture-panel.expanded {
            width: 24rem;
        }
        .json-capture-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0.5rem;
            border-bottom: 1px solid #e5e7eb;
            background: #f9fafb;
            border-top-left-radius: 0.5rem;
            border-top-right-radius: 0.5rem;
        }
        .json-capture-content {
            max-height: 24rem;
            overflow: auto;
            padding: 1rem;
        }
        .json-capture-item {
            margin-bottom: 1rem;
            padding: 0.5rem;
            border: 1px solid #e5e7eb;
            border-radius: 0.375rem;
        }
        .json-capture-url {
            font-size: 0.875rem;
            color: #4b5563;
            margin-bottom: 0.25rem;
            word-break: break-all;
        }
        .json-capture-timestamp {
            font-size: 0.75rem;
            color: #6b7280;
            margin-bottom: 0.5rem;
        }
        .json-capture-json {
            font-size: 0.75rem;
            background: #f9fafb;
            padding: 0.5rem;
            border-radius: 0.375rem;
            overflow: auto;
            white-space: pre-wrap;
            max-height: 12rem;
        }
        .json-capture-button {
            padding: 0.25rem 0.5rem;
            background: none;
            border: none;
            cursor: pointer;
            color: #4b5563;
        }
        .json-capture-button:hover {
            color: #2563eb;
        }
        .json-capture-button.delete:hover {
            color: #dc2626;
        }
        
        /* New styles for configuration */
        .config-section {
            margin-bottom: 1rem;
            padding: 0.5rem;
            border: 1px solid #e5e7eb;
            border-radius: 0.375rem;
        }
        .config-title {
            font-weight: 600;
            margin-bottom: 0.5rem;
        }
        .config-input {
            display: flex;
            gap: 0.5rem;
            margin-bottom: 0.5rem;
        }
        .config-input input {
            flex: 1;
            padding: 0.25rem 0.5rem;
            border: 1px solid #e5e7eb;
            border-radius: 0.25rem;
        }
        .config-list {
            display: flex;
            flex-wrap: wrap;
            gap: 0.25rem;
        }
        .config-tag {
            background: #f3f4f6;
            padding: 0.25rem 0.5rem;
            border-radius: 0.25rem;
            display: flex;
            align-items: center;
            gap: 0.25rem;
        }
        .config-tag button {
            padding: 0;
            background: none;
            border: none;
            cursor: pointer;
            color: #6b7280;
        }
        .tabs {
            display: flex;
            border-bottom: 1px solid #e5e7eb;
            margin-bottom: 1rem;
        }
        .tab {
            padding: 0.5rem 1rem;
            cursor: pointer;
            border-bottom: 2px solid transparent;
        }
        .tab.active {
            border-bottom-color: #2563eb;
            color: #2563eb;
        }
    `;
    document.head.appendChild(style);

    const { useState, useEffect } = React;

    // Configuration component
    function ConfigSection({ matches, ignores, onUpdateMatches, onUpdateIgnores }) {
        const [newMatch, setNewMatch] = useState('');
        const [newIgnore, setNewIgnore] = useState('');

        const addMatch = () => {
            if (newMatch && !matches.includes(newMatch)) {
                onUpdateMatches([...matches, newMatch]);
                setNewMatch('');
            }
        };

        const addIgnore = () => {
            if (newIgnore && !ignores.includes(newIgnore)) {
                onUpdateIgnores([...ignores, newIgnore]);
                setNewIgnore('');
            }
        };

        const removeMatch = (match) => {
            onUpdateMatches(matches.filter(m => m !== match));
        };

        const removeIgnore = (ignore) => {
            onUpdateIgnores(ignores.filter(i => i !== ignore));
        };

        return React.createElement('div', { className: 'config-content' }, [
            React.createElement('div', { key: 'matches', className: 'config-section' }, [
                React.createElement('div', { className: 'config-title' }, 'URL Matches'),
                React.createElement('div', { className: 'config-input' }, [
                    React.createElement('input', {
                        value: newMatch,
                        onChange: (e) => setNewMatch(e.target.value),
                        placeholder: 'Enter URL keyword to match',
                        onKeyPress: (e) => e.key === 'Enter' && addMatch()
                    }),
                    React.createElement('button', {
                        className: 'json-capture-button',
                        onClick: addMatch
                    }, '+')
                ]),
                React.createElement('div', { className: 'config-list' },
                    matches.map(match => 
                        React.createElement('span', { key: match, className: 'config-tag' }, [
                            match,
                            React.createElement('button', {
                                onClick: () => removeMatch(match)
                            }, '×')
                        ])
                    )
                )
            ]),
            React.createElement('div', { key: 'ignores', className: 'config-section' }, [
                React.createElement('div', { className: 'config-title' }, 'URL Ignores'),
                React.createElement('div', { className: 'config-input' }, [
                    React.createElement('input', {
                        value: newIgnore,
                        onChange: (e) => setNewIgnore(e.target.value),
                        placeholder: 'Enter URL keyword to ignore',
                        onKeyPress: (e) => e.key === 'Enter' && addIgnore()
                    }),
                    React.createElement('button', {
                        className: 'json-capture-button',
                        onClick: addIgnore
                    }, '+')
                ]),
                React.createElement('div', { className: 'config-list' },
                    ignores.map(ignore => 
                        React.createElement('span', { key: ignore, className: 'config-tag' }, [
                            ignore,
                            React.createElement('button', {
                                onClick: () => removeIgnore(ignore)
                            }, '×')
                        ])
                    )
                )
            ])
        ]);
    }

    function JsonCapturePanel() {
        const [captures, setCaptures] = useState([]);
        const [isMinimized, setIsMinimized] = useState(true);
        const [activeTab, setActiveTab] = useState('captures');
        const [matches, setMatches] = useState([]);
        const [ignores, setIgnores] = useState([]);
        const domain = window.location.hostname;

        // Load configuration from localStorage
        useEffect(() => {
            const config = JSON.parse(localStorage.getItem(`jsonCapture_${domain}`) || '{"matches":[],"ignores":[]}');
            setMatches(config.matches);
            setIgnores(config.ignores);
        }, []);

        // Save configuration to localStorage
        useEffect(() => {
            localStorage.setItem(`jsonCapture_${domain}`, JSON.stringify({ matches, ignores }));
        }, [matches, ignores]);

        const shouldCaptureUrl = (url) => {
            if (matches.length === 0) return false;
            return matches.some(match => url.includes(match)) && 
                   !ignores.some(ignore => url.includes(ignore));
        };

        useEffect(() => {
            // Intercept fetch
            const originalFetch = window.fetch;
            window.fetch = async function(...args) {
                const response = await originalFetch.apply(this, args);
                const clone = response.clone();

                try {
                    const contentType = clone.headers.get('content-type');
                    if (contentType && contentType.includes('application/json')) {
                        const url = typeof args[0] === 'string' ? args[0] : args[0].url;
                        if (shouldCaptureUrl(url)) {
                            const json = await clone.json();
                            setCaptures(prev => [...prev, {
                                timestamp: new Date().toISOString(),
                                url,
                                data: json
                            }]);
                        }
                    }
                } catch (err) {
                    console.error('Error processing response:', err);
                }

                return response;
            };

            // Intercept XHR
            const originalXHROpen = XMLHttpRequest.prototype.open;
            const originalXHRSend = XMLHttpRequest.prototype.send;

            XMLHttpRequest.prototype.open = function(...args) {
                this._url = args[1];
                return originalXHROpen.apply(this, args);
            };

            XMLHttpRequest.prototype.send = function(...args) {
                this.addEventListener('load', function() {
                    try {
                        const contentType = this.getResponseHeader('content-type');
                        if (contentType && contentType.includes('application/json')) {
                            if (shouldCaptureUrl(this._url)) {
                                const json = JSON.parse(this.responseText);
                                setCaptures(prev => [...prev, {
                                    timestamp: new Date().toISOString(),
                                    url: this._url,
                                    data: json
                                }]);
                            }
                        }
                    } catch (err) {
                        console.error('Error processing XHR response:', err);
                    }
                });

                return originalXHRSend.apply(this, args);
            };
        }, [matches, ignores]);

        const handleSave = () => {
            const blob = new Blob([JSON.stringify(captures, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `json-captures-${domain}-${new Date().toISOString()}.json`;
            a.click();
            URL.revokeObjectURL(url);
        };

        const handleClear = () => {
            setCaptures([]);
        };

        return React.createElement('div', {
            className: `json-capture-panel ${isMinimized ? 'minimized' : 'expanded'}`
        }, [
            // Header
            React.createElement('div', {
                key: 'header',
                className: 'json-capture-header'
            }, [
                !isMinimized && React.createElement('div', {
                    key: 'title'
                }, `JSON Captures (${captures.length})`),
                React.createElement('div', {
                    key: 'controls',
                    style: { display: 'flex', gap: '0.5rem' }
                }, [
                    !isMinimized && React.createElement('button', {
                        key: 'save',
                        className: 'json-capture-button',
                        onClick: handleSave,
                        title: 'Save captures'
                    }, '💾'),
                    React.createElement('button', {
                        key: 'toggle',
                        className: 'json-capture-button',
                        onClick: () => setIsMinimized(!isMinimized),
                        title: isMinimized ? 'Expand' : 'Minimize'
                    }, isMinimized ? '⤢' : '⤡'),
                    !isMinimized && React.createElement('button', {
                        key: 'clear',
                        className: 'json-capture-button delete',
                        onClick: handleClear,
                        title: 'Clear captures'
                    }, '✕')
                ])
            ]),
            // Content
            !isMinimized && React.createElement('div', { key: 'content' }, [
                // Tabs
                React.createElement('div', { key: 'tabs', className: 'tabs' }, [
                    React.createElement('div', {
                        className: `tab ${activeTab === 'captures' ? 'active' : ''}`,
                        onClick: () => setActiveTab('captures')
                    }, 'Captures'),
                    React.createElement('div', {
                        className: `tab ${activeTab === 'config' ? 'active' : ''}`,
                        onClick: () => setActiveTab('config')
                    }, 'Configuration')
                ]),
                // Tab content
                activeTab === 'captures' ? 
                    React.createElement('div', {
                        key: 'captures',
                        className: 'json-capture-content'
                    },
                        captures.length === 0
                            ? React.createElement('div', {
                                style: { textAlign: 'center', color: '#6b7280' }
                            }, matches.length === 0 ? 'Configure URL matches to start capturing' : 'No JSON responses captured yet')
                            : captures.map((capture, index) =>
                                React.createElement('div', {
                                    key: index,
                                    className: 'json-capture-item'
                                }, [
                                    React.createElement('div', {
                                        key: 'url',
                                        className: 'json-capture-url'
                                    }, capture.url),
                                    React.createElement('div', {
                                        key: 'timestamp',
                                        className: 'json-capture-timestamp'
                                    }, capture.timestamp),
                                    React.createElement('pre', {
                                        key: 'json',
                                        className: 'json-capture-json'
                                    }, JSON.stringify(capture.data, null, 2))
                                ])
                            )
                    )
                    : React.createElement(ConfigSection, {
                        key: 'config',
                        matches,
                        ignores,
                        onUpdateMatches: setMatches,
                        onUpdateIgnores: setIgnores
                    })
            ])
]);
}

const container = document.createElement('div');
document.body.appendChild(container);
ReactDOM.render(React.createElement(JsonCapturePanel), container);
})();