Transport policy enforcer

Enforce request policies for fetch() or GM_xmlhttpRequest methods with fallback routing

Ce script ne doit pas être installé directement. C'est une librairie destinée à être incluse dans d'autres scripts avec la méta-directive // @require https://update.greasyfork.org/scripts/591898/1906160/Transport%20policy%20enforcer.js

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           Transport policy enforcer
// @namespace      861ddd094884eac5bea7a3b12e074f34
// @author         Anonymous, Codex
// @version        1.0.0
// @license        BSD-0
// @grant          none
// ==/UserScript==

(function (global) {
    'use strict';

    const TRANSPORTS = ['gm-first', 'fetch-first', 'gm-only', 'fetch-only'];
    const config = {
        transport: 'gm-first',
        fallbackOn: {
            transportError: true,
            statuses: [403, 429, [500, 599]],
            predicate: null,
        },
        gmProvider: () =>
            (typeof GM !== 'undefined' && GM.xmlHttpRequest) ||
            (typeof GM_xmlhttpRequest === 'function' && GM_xmlhttpRequest) ||
            null,
        fetchProvider: () =>
            (typeof window !== 'undefined' && window.fetch)
                ? window.fetch.bind(window)
                : (typeof fetch === 'function' ? fetch : null),
        timeoutMs: 30000,
        onTransportEvent: null,
    };

    function configure(options) {
        return Object.assign(config, options || {});
    }

    function matchesStatus(status, list) {
        if (!Array.isArray(list)) return false;
        const value = Number(status);
        return list.some((entry) => {
            if (Array.isArray(entry)) {
                return entry.length === 2 && value >= Number(entry[0]) && value <= Number(entry[1]);
            }
            return value === Number(entry);
        });
    }

    function emit(event) {
        if (typeof config.onTransportEvent !== 'function') return;
        try { config.onTransportEvent(event); } catch (e) { /* host hook is best-effort */ }
    }

    function asError(value, message) {
        if (value instanceof Error) return value;
        const error = new Error(value && value.message ? value.message : message);
        if (value !== undefined) error.cause = value;
        return error;
    }

    function transportError(value, transport, details, kind) {
        const error = asError(value, `TransportPolicy: ${kind} using ${transport} for ${details.url}`);
        try { error.transport = transport; } catch (e) { /* a frozen provider error is replaced below */ }
        if (error.transport !== transport) {
            const replacement = new Error(error.message);
            replacement.cause = error;
            replacement.transport = transport;
            if (kind === 'timeout') replacement.timedOut = true;
            return replacement;
        }
        if (kind === 'timeout') error.timedOut = true;
        return error;
    }

    function normaliseGmResponse(raw, details) {
        raw = raw || {};
        const responseText = typeof raw.responseText === 'string' ? raw.responseText : '';
        return {
            status: Number(raw.status) || 0,
            statusText: raw.statusText || '',
            responseText,
            response: raw.response !== undefined ? raw.response : responseText,
            responseHeaders: raw.responseHeaders || '',
            finalUrl: raw.finalUrl || details.url,
            transport: 'gm',
            fellBack: false,
        };
    }

    function gmRequest(details, provider) {
        return new Promise((resolve, reject) => {
            const options = Object.assign({}, details, {
                timeout: details.timeout ?? config.timeoutMs,
                onload: (response) => resolve(normaliseGmResponse(response, details)),
                onerror: (error) => reject(transportError(error, 'gm', details, 'network error')),
                ontimeout: (error) => reject(transportError(error, 'gm', details, 'timeout')),
                onabort: (error) => reject(transportError(error, 'gm', details, 'abort')),
            });
            try {
                provider(options);
            } catch (error) {
                reject(transportError(error, 'gm', details, 'provider error'));
            }
        });
    }

    function headerString(headers) {
        let lines = '';
        if (headers && typeof headers.forEach === 'function') {
            headers.forEach((value, name) => { lines += `${name}: ${value}\r\n`; });
        }
        return lines;
    }

    async function fetchRequest(details, provider) {
        const timeout = details.timeout ?? config.timeoutMs;
        const init = {
            method: details.method || 'GET',
            credentials: 'include',
            cache: 'no-store',
            redirect: 'follow',
        };
        if (details.headers) init.headers = Object.assign({}, details.headers);
        if (details.data != null) init.body = details.data;

        let controller;
        let timer;
        if (timeout > 0 && typeof AbortController === 'function') {
            controller = new AbortController();
            init.signal = controller.signal;
            timer = setTimeout(() => controller.abort(), timeout);
        }

        try {
            const raw = await provider(details.url, init);
            const responseType = details.responseType || 'text';
            let response;
            let responseText = '';
            if (responseType === 'json') {
                responseText = await raw.text();
                response = JSON.parse(responseText);
            } else if (responseType === 'arraybuffer') {
                response = await raw.arrayBuffer();
            } else if (responseType === 'blob') {
                response = await raw.blob();
            } else {
                responseText = await raw.text();
                response = responseText;
            }
            return {
                status: Number(raw.status) || 0,
                statusText: raw.statusText || '',
                responseText,
                response,
                responseHeaders: headerString(raw.headers),
                finalUrl: raw.url || details.url,
                transport: 'fetch',
                fellBack: false,
            };
        } catch (error) {
            const timedOut = !!(controller && controller.signal.aborted);
            throw transportError(error, 'fetch', details, timedOut ? 'timeout' : 'transport error');
        } finally {
            if (timer !== undefined) clearTimeout(timer);
        }
    }

    function providerFor(transport) {
        const factory = transport === 'gm' ? config.gmProvider : config.fetchProvider;
        if (typeof factory !== 'function') return null;
        return factory();
    }

    async function attempt(transport, details) {
        emit({ type: 'attempt', transport, url: details.url, method: details.method || 'GET' });
        let provider;
        try {
            provider = providerFor(transport);
        } catch (error) {
            throw transportError(error, transport, details, 'provider error');
        }
        if (typeof provider !== 'function') {
            const error = transportError(undefined, transport, details, 'unavailable');
            error.unavailable = true;
            throw error;
        }
        return transport === 'gm'
            ? gmRequest(details, provider)
            : fetchRequest(details, provider);
    }

    function ordering(policy) {
        switch (policy) {
        case 'gm-first': return ['gm', 'fetch'];
        case 'fetch-first': return ['fetch', 'gm'];
        case 'gm-only': return ['gm'];
        case 'fetch-only': return ['fetch'];
        default: throw new Error(`TransportPolicy: unknown transport policy ${policy}`);
        }
    }

    function fallbackDecision(outcome, rules, transport) {
        if (typeof rules.predicate === 'function') {
            const verdict = rules.predicate(outcome.response, transport);
            if (typeof verdict === 'boolean') {
                return verdict ? { fallback: true, reason: 'predicate' } : { fallback: false };
            }
        }
        if (outcome.error) {
            return rules.transportError
                ? { fallback: true, reason: 'transport-error' }
                : { fallback: false };
        }
        return matchesStatus(outcome.response.status, rules.statuses)
            ? { fallback: true, reason: 'status' }
            : { fallback: false };
    }

    function primarySummary(transport, outcome) {
        if (outcome.error) return { transport, error: outcome.error };
        return { transport, status: outcome.response.status };
    }

    function finalEvent(type, transport, details, value, reason) {
        const event = { type, transport, url: details.url, method: details.method || 'GET' };
        if (value && typeof value.status === 'number') event.status = value.status;
        if (value instanceof Error) event.error = value;
        if (reason) event.reason = reason;
        emit(event);
    }

    async function request(input) {
        const details = input || {};
        const policy = details.transport || config.transport;
        const order = ordering(policy);
        const rules = Object.assign({}, config.fallbackOn || {}, details.fallbackOn || {});
        const { transport: ignoredTransport, fallbackOn: ignoredFallback, ...networkDetails } = details;
        const primaryTransport = order[0];
        let primary;

        try {
            primary = { response: await attempt(primaryTransport, networkDetails) };
        } catch (error) {
            primary = { error };
        }

        if (order.length === 1) {
            if (primary.error) {
                finalEvent('rejected', primaryTransport, networkDetails, primary.error,
                    primary.error.unavailable ? 'unavailable' : 'transport-error');
                throw primary.error;
            }
            finalEvent('resolved', primaryTransport, networkDetails, primary.response);
            return primary.response;
        }

        let decision;
        if (primary.error && primary.error.unavailable) {
            decision = { fallback: true, reason: 'unavailable' };
        } else {
            try {
                decision = fallbackDecision(primary, rules, primaryTransport);
            } catch (error) {
                const rejection = transportError(error, primaryTransport, networkDetails, 'predicate error');
                finalEvent('rejected', primaryTransport, networkDetails, rejection);
                throw rejection;
            }
        }

        if (!decision.fallback) {
            if (primary.error) {
                finalEvent('rejected', primaryTransport, networkDetails, primary.error, 'transport-error');
                throw primary.error;
            }
            finalEvent('resolved', primaryTransport, networkDetails, primary.response);
            return primary.response;
        }

        const fallbackEvent = {
            type: 'fallback',
            transport: primaryTransport,
            url: networkDetails.url,
            method: networkDetails.method || 'GET',
            reason: decision.reason,
        };
        if (primary.response) fallbackEvent.status = primary.response.status;
        if (primary.error) fallbackEvent.error = primary.error;
        emit(fallbackEvent);

        const secondaryTransport = order[1];
        try {
            const response = await attempt(secondaryTransport, networkDetails);
            response.fellBack = true;
            response.primary = primarySummary(primaryTransport, primary);
            finalEvent('resolved', secondaryTransport, networkDetails, response);
            return response;
        } catch (error) {
            error.transport = secondaryTransport;
            if (primary.error) error.primaryError = primary.error;
            else error.primaryResponse = primary.response;
            finalEvent('rejected', secondaryTransport, networkDetails, error,
                error.unavailable ? 'unavailable' : 'transport-error');
            throw error;
        }
    }

    function xmlHttpRequest(details) {
        const { onload, onerror, ontimeout, ...requestDetails } = details;
        request(requestDetails).then(
            (response) => { if (typeof onload === 'function') onload(response); },
            (error) => {
                if (error.timedOut && typeof ontimeout === 'function') ontimeout(error);
                else if (typeof onerror === 'function') onerror(error);
            }
        );
        return { abort() {} };
    }

    global.TransportPolicy = {
        configure,
        config,
        request,
        xmlHttpRequest,
        TRANSPORTS,
        matchesStatus,
    };
}(typeof globalThis !== 'undefined' ? globalThis : this));