quoid / userscripts

An open-source userscript manager for Safari
https://quoid.github.io/userscripts/
GNU General Public License v3.0
3.02k stars 156 forks source link

Early JavaScript code cannot be intercepted even with "@run-at document-start", "Instant" injection mode needed #459

Open biergaizi opened 1 year ago

biergaizi commented 1 year ago

I'm trying to write a Userscript to change the behavior of websites by monkey-patching the built-in JavaScript functions, for example, replacing them with my custom versions to intercept their actions. This requires one to execute the Userscript before everything else. Unfortunately, I found even with @run-at document-start, early JavaScript function calls cannot be intercepted, since the Userscript is still running too late.

Demo

To replicate this problem, I selected the JavaScript alert() function as a demo. First, save the following webpage as test.html:

<head>
</head>

<body>
    <button id="test">Alert</button>
</body>

<script>
(function () {
  alert("Early call!");
}
)();

let button = document.querySelector('#test');
button.onclick = function () {
    alert("Late call!");
}
</script>

Then save the following Userscript as intercept-alert.user.js, which monkey-patches the JavaScript "alert()` function with a custom version:

// ==UserScript==
// @name     Intercept Test
// @version  1
// @run-at   document-start
// @include  http://127.0.0.1:8000/*
// @grant    none
// ==/UserScript==

function monkeyPatch() {
  let realAlert = window.alert;

  window.alert = function(text) {
    realAlert("alert has been intercepted: " + text);
  }
}

// inject the monkey patch into DOM
var script = document.createElement('script');
script.appendChild(document.createTextNode('('+ monkeyPatch +')();'));
(document.body || document.head || document.documentElement).appendChild(script);

Open the webpage, you would see that the Userscript cannot intercept the alert("Early call!");, only the alert("Late call!"); can be intercepted.

Discussion

In fact, one can also replicate this problem with TamperMonkey and ViolentMonkey on Chromium. To overcome this problem, both plugins implemented a workaround. TamperMonkey calls it "inject mode: instant", and ViolentMonkey calls it "Synchronous page mode`.

ViolentMonkey's description is:

Runs scripts at the real <document-start> reliably. Don't enable unless you do have a script that needs to run before the page starts loading and Violentmonkey is currently running it too late. This mode will be using the deprecated synchronous XHR so you'll see warnings in devtools console, although you can safely ignore them as the adverse affects it warns about are negligible in this case and you can hide the warnings for good by right-clicking one.

The workaround basically abuses document.cookie and a synchronous XMLHttpRequest call in a tricky way in order to force the browser to run the scripts in a synchronous manner, ensuring that the Userscript runs before everything else.

  1. The background script makes a Blob in the background script with the data, gets its URL via URL.createObjectURL, puts it into a Set-Cookie header via chrome.webRequest API.

  2. The content script reads the URL from document.cookie and uses it in a synchronous XMLHttpRequest to get the original data synchronously.

The source code of ViolentMonkey's implementation is available here: https://github.com/violentmonkey/violentmonkey/pull/1100

Is it possible to implement the same feature in quoid/userscripts? Or is there an alternative method to monkey-patch and intercept JavaScript function without this limitation?

ACTCD commented 1 year ago

Thanks for the feedback. Apple strictly restricts the API related to cookies, and as far as we know, it is impossible to achieve the same trick. Duplicate issue #184