From 3a249f395cc44272d36757829c51a6d6fb863fc0 Mon Sep 17 00:00:00 2001 From: Raymond Hill Date: Mon, 26 Aug 2024 14:28:16 -0400 Subject: [PATCH] Improve `prevent-xhr` scriptlet As per filter list maintainers feedback. --- assets/resources/scriptlets.js | 81 ++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/assets/resources/scriptlets.js b/assets/resources/scriptlets.js index 8acbb9894..0272ecce3 100644 --- a/assets/resources/scriptlets.js +++ b/assets/resources/scriptlets.js @@ -2695,6 +2695,13 @@ function noXhrIf( 'content-type': '', 'content-length': '', }; + const safeDispatchEvent = (xhr, type) => { + try { + xhr.dispatchEvent(new Event(type)); + } catch(_) { + } + }; + const XHRBefore = XMLHttpRequest.prototype; self.XMLHttpRequest = class extends self.XMLHttpRequest { open(method, url, ...args) { xhrInstances.delete(this); @@ -2721,27 +2728,26 @@ function noXhrIf( let promise = Promise.resolve({ xhr: this, directive, - props: { - readyState: { value: 4 }, + response: { response: { value: '' }, responseText: { value: '' }, responseXML: { value: null }, responseURL: { value: haystack.url }, - status: { value: 200 }, - statusText: { value: 'OK' }, - }, + } }); switch ( this.responseType ) { case 'arraybuffer': promise = promise.then(details => { - details.props.response.value = new ArrayBuffer(0); + const response = details.response; + response.response.value = new ArrayBuffer(0); return details; }); haystack.headers['content-type'] = 'application/octet-stream'; break; case 'blob': promise = promise.then(details => { - details.props.response.value = new Blob([]); + const response = details.response; + response.response.value = new Blob([]); return details; }); haystack.headers['content-type'] = 'application/octet-stream'; @@ -2750,8 +2756,9 @@ function noXhrIf( promise = promise.then(details => { const parser = new DOMParser(); const doc = parser.parseFromString('', 'text/html'); - details.props.response.value = doc; - details.props.responseXML.value = doc; + const response = details.response; + response.response.value = doc; + response.responseXML.value = doc; return details; }); haystack.headers['content-type'] = 'text/html'; @@ -2759,8 +2766,9 @@ function noXhrIf( } case 'json': promise = promise.then(details => { - details.props.response.value = {}; - details.props.responseText.value = '{}'; + const response = details.response; + response.response.value = {}; + response.responseText.value = '{}'; return details; }); haystack.headers['content-type'] = 'application/json'; @@ -2769,8 +2777,9 @@ function noXhrIf( if ( directive === '' ) { break; } promise = promise.then(details => { return generateContentFn(details.directive).then(text => { - details.props.response.value = text; - details.props.responseText.value = text; + const response = details.response; + response.response.value = text; + response.responseText.value = text; return details; }); }); @@ -2778,11 +2787,35 @@ function noXhrIf( break; } promise.then(details => { - haystack.headers['content-length'] = `${details.props.response.value}`.length; - Object.defineProperties(details.xhr, details.props); - details.xhr.dispatchEvent(new Event('readystatechange')); - details.xhr.dispatchEvent(new Event('load')); - details.xhr.dispatchEvent(new Event('loadend')); + Object.defineProperties(details.xhr, { + readyState: { value: 1, configurable: true }, + }); + safeDispatchEvent(details.xhr, 'readystatechange'); + return details; + }).then(details => { + const response = details.response; + haystack.headers['content-length'] = `${response.response.value}`.length; + Object.defineProperties(details.xhr, { + readyState: { value: 2, configurable: true }, + status: { value: 200 }, + statusText: { value: 'OK' }, + }); + safeDispatchEvent(details.xhr, 'readystatechange'); + return details; + }).then(details => { + Object.defineProperties(details.xhr, { + readyState: { value: 3, configurable: true }, + }); + Object.defineProperties(details.xhr, details.response); + safeDispatchEvent(details.xhr, 'readystatechange'); + return details; + }).then(details => { + Object.defineProperties(details.xhr, { + readyState: { value: 4 }, + }); + safeDispatchEvent(details.xhr, 'readystatechange'); + safeDispatchEvent(details.xhr, 'load'); + safeDispatchEvent(details.xhr, 'loadend'); safe.uboLog(logPrefix, `Prevented with response:\n${details.xhr.response}`); }); } @@ -2809,6 +2842,18 @@ function noXhrIf( return out.join('\r\n'); } }; + self.XMLHttpRequest.prototype.open.toString = function() { + return XHRBefore.open.toString(); + }; + self.XMLHttpRequest.prototype.send.toString = function() { + return XHRBefore.send.toString(); + }; + self.XMLHttpRequest.prototype.getResponseHeader.toString = function() { + return XHRBefore.getResponseHeader.toString(); + }; + self.XMLHttpRequest.prototype.getAllResponseHeaders.toString = function() { + return XHRBefore.getAllResponseHeaders.toString(); + }; } /******************************************************************************/