Greasy Fork

水源解码

可在水源论坛 base64 解码选中内容

当前为 2021-10-16 提交的版本,查看 最新版本

// ==UserScript==
// @name         水源解码
// @namespace    CCCC_David
// @version      0.1.0
// @description  可在水源论坛 base64 解码选中内容
// @author       CCCC_David
// @match        https://shuiyuan.sjtu.edu.cn/*
// @grant        none
// ==/UserScript==

(() => {
    'use strict';

    // From Font Awesome Free v5.15 by @fontawesome - https://fontawesome.com
    // License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
    // Modified class attribute to fit in.
    const DECODE_ICON = '<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="key" class="d-icon svg-icon" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M512 176.001C512 273.203 433.202 352 336 352c-11.22 0-22.19-1.062-32.827-3.069l-24.012 27.014A23.999 23.999 0 0 1 261.223 384H224v40c0 13.255-10.745 24-24 24h-40v40c0 13.255-10.745 24-24 24H24c-13.255 0-24-10.745-24-24v-78.059c0-6.365 2.529-12.47 7.029-16.971l161.802-161.802C163.108 213.814 160 195.271 160 176 160 78.798 238.797.001 335.999 0 433.488-.001 512 78.511 512 176.001zM336 128c0 26.51 21.49 48 48 48s48-21.49 48-48-21.49-48-48-48-48 21.49-48 48z"></path></svg>';

    // Parameters.
    const APPEND_DECODE_BUTTON_TARGET_CLASS = 'buttons';

    // Utility functions.
    const escapeRegExpOutsideCharacterClass = (s) => s.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');

    const allowedPolicy = window.trustedTypes?.createPolicy?.('allowedPolicy', {createHTML: (x) => x});
    const createTrustedHTML = (html) => (allowedPolicy ? allowedPolicy.createHTML(html) : html);

    const utf8Decoder = new TextDecoder('utf-8', {fatal: true});
    const htmlParser = new DOMParser();

    const isBinaryString = (s) => s.split('').every((c) => c.charCodeAt(0) < 256);

    const decodeUTF8BinaryString = (s) => {
        // Assuming input is binary string.
        const byteArray = new Uint8Array(s.split('').map((c) => c.charCodeAt(0)));
        try {
            return utf8Decoder.decode(byteArray);
        } catch {
            return null;
        }
    };

    const decodeBase64AndURI = (data) => {
        let result = data, prevResult = data;
        // eslint-disable-next-line no-constant-condition
        while (true) {
            const tempResult = result;
            try {
                result = atob(result);
            } catch {
                break;
            }
            prevResult = tempResult;
            if (result === prevResult) {
                break;
            }
        }
        if (isBinaryString(result)) {
            result = decodeUTF8BinaryString(result) ?? prevResult;
        }
        try {
            result = decodeURIComponent(result);
        } catch {
        }
        return result;
    };

    const lookupShortURLs = async (shortURLs) => {
        if (!shortURLs) {
            return new Map();
        }
        try {
            const response = await fetch('/uploads/lookup-urls', {
                method: 'POST',
                body: shortURLs.map((url) => `short_urls%5B%5D=${encodeURIComponent(url)}`).join('&'),
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                    'Discourse-Present': 'true',
                    'Discourse-Logged-In': 'true',
                    'X-Requested-With': 'XMLHttpRequest',
                    'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content,
                },
                mode: 'same-origin',
                credentials: 'include',
            });
            if (!response.ok) {
                // eslint-disable-next-line no-console
                console.error(`lookupShortURLs fetch failure: ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`);
                return new Map();
            }
            const result = await response.json();
            return new Map(result.map((item) => [item.short_url, item.url]));
        } catch (e) {
            // eslint-disable-next-line no-console
            console.error(e);
            return new Map();
        }
    };

    const renderContent = async (content) => {
        // First cook the content.
        // eslint-disable-next-line no-undef
        const cookedContent = await require('discourse/lib/text').cookAsync(content);
        let tree;
        try {
            tree = htmlParser.parseFromString(cookedContent, 'text/html');
        } catch (e) {
            // eslint-disable-next-line no-console
            console.error(e);
            return '<font color="red">(Parse error)</font>';
        }

        // Extract all short URLs and look up in batch.
        const shortURLs = [];
        for (const el of tree.querySelectorAll('img[data-orig-src], source[data-orig-src]')) {
            shortURLs.push(el.getAttribute('data-orig-src'));
        }
        for (const el of tree.querySelectorAll('a[data-orig-href]')) {
            shortURLs.push(el.getAttribute('data-orig-href'));
        }
        const shortURLMapping = await lookupShortURLs(shortURLs);

        // Replace short URLs with real URLs.
        for (const el of tree.querySelectorAll('img[data-orig-src], source[data-orig-src]')) {
            const src = el.getAttribute('data-orig-src');
            if (shortURLMapping.has(src)) {
                el.src = shortURLMapping.get(src);
                el.removeAttribute('data-orig-src');
            }
        }
        for (const el of tree.querySelectorAll('a[data-orig-href]')) {
            const href = el.getAttribute('data-orig-href');
            if (shortURLMapping.has(href)) {
                el.href = shortURLMapping.get(href);
                el.removeAttribute('data-orig-href');
            }
        }
        return tree.body.querySelector(':scope > p')?.innerHTML ?? tree.body.innerHTML;
    };

    const convertSelection = async () => {
        const selection = window.getSelection();
        const selectionString = selection.toString();
        const {anchorNode, focusNode} = selection;
        if (!selectionString || !anchorNode || !focusNode) {
            return;
        }
        let targetNode;
        if (anchorNode === focusNode) {
            targetNode = anchorNode;
        } else if (anchorNode.contains(focusNode)) {
            targetNode = focusNode;
        } else if (focusNode.contains(anchorNode)) {
            targetNode = anchorNode;
        } else {
            targetNode = focusNode;
        }
        if (targetNode.innerHTML === undefined) {
            targetNode = targetNode.parentNode;
        }
        targetNode.innerHTML = createTrustedHTML(await renderContent(decodeBase64AndURI(selectionString)));
        selection.removeAllRanges();
    };

    const addDecodeButton = (quoteButtonContainer) => {
        if (quoteButtonContainer?.nodeName.toLowerCase() !== 'div' || document.getElementById('decode-selection-button')) {
            return;
        }
        const decodeButtonContainer = document.createElement('span');
        decodeButtonContainer.innerHTML = createTrustedHTML(`
            <button aria-label="解码" id="decode-selection-button" class="btn-flat btn btn-icon-text" type="button">
                ${DECODE_ICON}
                <span class="d-button-label">解码</span>
            </button>
        `);
        quoteButtonContainer.appendChild(decodeButtonContainer);
        document.getElementById('decode-selection-button').addEventListener('click', convertSelection);
    };

    const observer = new MutationObserver((mutationsList) => {
        for (const mutation of mutationsList) {
            if (mutation.type === 'childList') {
                for (const el of mutation.addedNodes) {
                    if (el.classList?.contains(APPEND_DECODE_BUTTON_TARGET_CLASS)) {
                        addDecodeButton(el);
                    }
                }
            } else if (mutation.type === 'attributes') {
                if (!mutation.oldValue?.match(new RegExp(`(?:^|\\s)${escapeRegExpOutsideCharacterClass(APPEND_DECODE_BUTTON_TARGET_CLASS)}(?:\\s|$)`, 'u')) &&
                    mutation.target.classList?.contains(APPEND_DECODE_BUTTON_TARGET_CLASS)) {
                    addDecodeButton(mutation.target);
                }
            }
        }
    });

    observer.observe(document.documentElement, {
        subtree: true,
        childList: true,
        attributeFilter: ['class'],
        attributeOldValue: true,
    });

    for (const el of document.getElementsByClassName(APPEND_DECODE_BUTTON_TARGET_CLASS)) {
        addDecodeButton(el);
    }
})();