Make GM xhr more parallel again

Workaround for Tampermonkey 5.3.2 parallel GM_xmlhttpRequest issue

Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greatest.deepsurf.us/scripts/516445/1480246/Make%20GM%20xhr%20more%20parallel%20again.js

  1. /*
  2. * https://github.com/Tampermonkey/tampermonkey/issues/2215
  3. *
  4. * This script provides a workaround for a Chrome MV3 issue (https://github.com/w3c/webextensions/issues/694)
  5. * where extensions can't set/delete headers that are preserved over redirects.
  6. *
  7. * By setting `redirect: 'manual'` and following redirects manually, this script ensures request redirects work
  8. * as intended and requests to different URLs are made in parallel (again).
  9. *
  10. * Userscript authors should include this as a `@require` when they need to make parallel requests with GM_xmlhttpRequest,
  11. * especially if requests might take a long time to complete.
  12. *
  13. * Including this script will modify the behavior of GM_xmlhttpRequest and GM.xmlHttpRequest in Tampermonkey only.
  14. *
  15. * Usage:
  16. *
  17. * Add this to the metadata block of your userscript:
  18. *
  19. * // @grant GM_xmlhttpRequest
  20. * // @require https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js
  21. *
  22. **/
  23.  
  24. /* global GM_info, GM_xmlhttpRequest, GM */
  25.  
  26. const HAS_GM = typeof GM !== 'undefined';
  27. const NEW_GM = ((scope, GM) => {
  28. // Check if running in Tampermonkey and if version supports redirect control
  29. if (GM_info.scriptHandler !== "Tampermonkey" || compareVersions(GM_info.version, "5.3.2") < 0) return;
  30.  
  31. // Backup original functions
  32. const GM_xmlhttpRequestOrig = GM_xmlhttpRequest;
  33. const GM_xmlHttpRequestOrig = GM.xmlHttpRequest;
  34.  
  35. function compareVersions(v1, v2) {
  36. const parts1 = v1.split('.').map(Number);
  37. const parts2 = v2.split('.').map(Number);
  38. const length = Math.max(parts1.length, parts2.length);
  39.  
  40. for (let i = 0; i < length; i++) {
  41. const num1 = parts1[i] || 0;
  42. const num2 = parts2[i] || 0;
  43.  
  44. if (num1 > num2) return 1;
  45. if (num1 < num2) return -1;
  46. }
  47. return 0;
  48. }
  49.  
  50. // Wrapper for GM_xmlhttpRequest
  51. function GM_xmlhttpRequestWrapper(odetails) {
  52. // If redirect is manually set, simply pass odetails to the original function
  53. if (odetails.redirect !== undefined) {
  54. return GM_xmlhttpRequestOrig(odetails);
  55. }
  56.  
  57. // Warn if onprogress is used with settings incompatible with fetch mode used in background
  58. if (odetails.onprogress || odetails.fetch === false) {
  59. console.warn("Fetch mode does not support onprogress in the background.");
  60. }
  61.  
  62. const {
  63. onload,
  64. onloadend,
  65. onerror,
  66. onabort,
  67. ontimeout,
  68. ...details
  69. } = odetails;
  70.  
  71. // Set redirect to manual and handle redirects
  72. const handleRedirects = (initialDetails) => {
  73. const request = GM_xmlhttpRequestOrig({
  74. ...initialDetails,
  75. redirect: 'manual',
  76. onload: function(response) {
  77. if (response.status >= 300 && response.status < 400) {
  78. const m = response.responseHeaders.match(/Location:\s*(\S+)/i);
  79. // Follow redirect manually
  80. const redirectUrl = m && m[1];
  81. if (redirectUrl) {
  82. const absoluteUrl = new URL(redirectUrl, initialDetails.url).href;
  83. handleRedirects({ ...initialDetails, url: absoluteUrl });
  84. return;
  85. }
  86. }
  87.  
  88. if (onload) onload.call(this, response);
  89. if (onloadend) onloadend.call(this, response);
  90. },
  91. onerror: function(response) {
  92. if (onerror) onerror.call(this, response);
  93. if (onloadend) onloadend.call(this, response);
  94. },
  95. onabort: function(response) {
  96. if (onabort) onabort.call(this, response);
  97. if (onloadend) onloadend.call(this, response);
  98. },
  99. ontimeout: function(response) {
  100. if (ontimeout) ontimeout.call(this, response);
  101. if (onloadend) onloadend.call(this, response);
  102. }
  103. });
  104. return request;
  105. };
  106.  
  107. return handleRedirects(details);
  108. }
  109.  
  110. // Wrapper for GM.xmlHttpRequest
  111. function GM_xmlHttpRequestWrapper(odetails) {
  112. let abort;
  113.  
  114. const p = new Promise((resolve, reject) => {
  115. const { onload, ontimeout, onerror, ...send } = odetails;
  116.  
  117. send.onerror = function(r) {
  118. if (onerror) {
  119. resolve(r);
  120. onerror.call(this, r);
  121. } else {
  122. reject(r);
  123. }
  124. };
  125. send.ontimeout = function(r) {
  126. if (ontimeout) {
  127. // See comment above
  128. resolve(r);
  129. ontimeout.call(this, r);
  130. } else {
  131. reject(r);
  132. }
  133. };
  134. send.onload = function(r) {
  135. resolve(r);
  136. if (onload) onload.call(this, r);
  137. };
  138.  
  139. const a = GM_xmlhttpRequestWrapper(send).abort;
  140. if (abort === true) {
  141. a();
  142. } else {
  143. abort = a;
  144. }
  145. });
  146.  
  147. p.abort = () => {
  148. if (typeof abort === 'function') {
  149. abort();
  150. } else {
  151. abort = true;
  152. }
  153. };
  154.  
  155. return p;
  156. }
  157.  
  158. // Export wrappers
  159. GM_xmlhttpRequest = GM_xmlhttpRequestWrapper;
  160. scope.GM_xmlhttpRequestOrig = GM_xmlhttpRequestOrig;
  161.  
  162. const gopd = Object.getOwnPropertyDescriptor(GM, 'xmlHttpRequest');
  163. if (gopd && gopd.configurable === false) {
  164. return {
  165. __proto__: GM,
  166. xmlHttpRequest: GM_xmlHttpRequestWrapper,
  167. xmlHttpRequestOrig: GM_xmlHttpRequestOrig
  168. };
  169. } else {
  170. GM.xmlHttpRequest = GM_xmlHttpRequestWrapper;
  171. GM.xmlHttpRequestOrig = GM_xmlHttpRequestOrig;
  172. }
  173. })(this, HAS_GM ? GM : {});
  174.  
  175. if (HAS_GM && NEW_GM) GM = NEW_GM;