From 920169655d369c9b147ec7c26474481d5e624692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Go=C5=82e=CC=A8biowski-Owczarek?= Date: Tue, 17 Sep 2024 00:56:41 +0200 Subject: [PATCH] Ajax: Fill in & warn against automatic JSON-to-JSONP promotion So far, the patch was only warning about the automatic promotion, but it wasn't filling the behavior back to jQuery 4+. This has been fixed. --- eslint.config.js | 1 + src/jquery/ajax.js | 132 +++++++++++++- test/data/ajax-jsonp-callback-name.html | 87 +++++++++ test/data/jsonpScript.js | 5 + test/data/testinit.js | 3 + test/index.html | 6 +- test/unit/jquery/ajax.js | 225 ++++++++++++++++-------- 7 files changed, 375 insertions(+), 84 deletions(-) create mode 100644 test/data/ajax-jsonp-callback-name.html create mode 100644 test/data/jsonpScript.js diff --git a/eslint.config.js b/eslint.config.js index 929e7367..00c9b5db 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -130,6 +130,7 @@ export default [ QUnit: false, url: true, compareVersions: true, + jQueryVersionSince: false, expectWarning: true, expectNoWarning: true, startIframeTest: true, diff --git a/src/jquery/ajax.js b/src/jquery/ajax.js index 2caf7929..91555749 100644 --- a/src/jquery/ajax.js +++ b/src/jquery/ajax.js @@ -5,7 +5,11 @@ import { migrateWarn, migratePatchAndWarnFunc, migratePatchFunc } from "../main. if ( jQuery.ajax ) { var oldAjax = jQuery.ajax, - rjsonp = /(=)\?(?=&|$)|\?\?/; + oldCallbacks = [], + guid = "migrate-" + Date.now(), + origJsonpCallback = jQuery.ajaxSettings.jsonpCallback, + rjsonp = /(=)\?(?=&|$)|\?\?/, + rquery = /\?/; migratePatchFunc( jQuery, "ajax", function() { var jQXHR = oldAjax.apply( this, arguments ); @@ -23,16 +27,120 @@ migratePatchFunc( jQuery, "ajax", function() { return jQXHR; }, "jqXHR-methods" ); -// Only trigger the logic in jQuery <4 as the JSON-to-JSONP auto-promotion -// behavior is gone in jQuery 4.0 and as it has security implications, we don't -// want to restore the legacy behavior. -if ( !jQueryVersionSince( "4.0.0" ) ) { +jQuery.ajaxSetup( { + jsonpCallback: function() { - // Register this prefilter before the jQuery one. Otherwise, a promoted - // request is transformed into one with the script dataType and we can't - // catch it anymore. + // Source is virtually the same as in Core, but we need to duplicate + // to maintain a proper `oldCallbacks` reference. + if ( jQuery.migrateIsPatchEnabled( "jsonp-promotion" ) ) { + var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( guid++ ) ); + this[ callback ] = true; + return callback; + } else { + return origJsonpCallback.apply( this, arguments ); + } + } +} ); + +// Register this prefilter before the jQuery one. Otherwise, a promoted +// request is transformed into one with the script dataType, and we can't +// catch it anymore. +if ( jQueryVersionSince( "4.0.0" ) ) { + + // Code mostly from: + // https://github.com/jquery/jquery/blob/fa0058af426c4e482059214c29c29f004254d9a1/src/ajax/jsonp.js#L20-L97 + jQuery.ajaxPrefilter( "+json", function( s, originalSettings, jqXHR ) { + + if ( !jQuery.migrateIsPatchEnabled( "jsonp-promotion" ) ) { + return; + } + + var callbackName, overwritten, responseContainer, + jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ? + "url" : + typeof s.data === "string" && + ( s.contentType || "" ) + .indexOf( "application/x-www-form-urlencoded" ) === 0 && + rjsonp.test( s.data ) && "data" + ); + + // Handle iff the expected data type is "jsonp" or we have a parameter to set + if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) { + migrateWarn( "jsonp-promotion", "JSON-to-JSONP auto-promotion is deprecated" ); + + // Get callback name, remembering preexisting value associated with it + callbackName = s.jsonpCallback = typeof s.jsonpCallback === "function" ? + s.jsonpCallback() : + s.jsonpCallback; + + // Insert callback into url or form data + if ( jsonProp ) { + s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName ); + } else if ( s.jsonp !== false ) { + s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; + } + + // Use data converter to retrieve json after script execution + s.converters[ "script json" ] = function() { + if ( !responseContainer ) { + jQuery.error( callbackName + " was not called" ); + } + return responseContainer[ 0 ]; + }; + + // Force json dataType + s.dataTypes[ 0 ] = "json"; + + // Install callback + overwritten = window[ callbackName ]; + window[ callbackName ] = function() { + responseContainer = arguments; + }; + + // Clean-up function (fires after converters) + jqXHR.always( function() { + + // If previous value didn't exist - remove it + if ( overwritten === undefined ) { + jQuery( window ).removeProp( callbackName ); + + // Otherwise restore preexisting value + } else { + window[ callbackName ] = overwritten; + } + + // Save back as free + if ( s[ callbackName ] ) { + + // Make sure that re-using the options doesn't screw things around + s.jsonpCallback = originalSettings.jsonpCallback; + + // Save the callback name for future use + oldCallbacks.push( callbackName ); + } + + // Call if it was a function and we have a response + if ( responseContainer && typeof overwritten === "function" ) { + overwritten( responseContainer[ 0 ] ); + } + + responseContainer = overwritten = undefined; + } ); + + // Delegate to script + return "script"; + } + } ); +} else { + + // jQuery <4 already contains this prefixer; don't duplicate the whole logic, + // but only enough to know when to warn. jQuery.ajaxPrefilter( "+json", function( s ) { + if ( !jQuery.migrateIsPatchEnabled( "jsonp-promotion" ) ) { + return; + } + // Warn if JSON-to-JSONP auto-promotion happens. if ( s.jsonp !== false && ( rjsonp.test( s.url ) || typeof s.data === "string" && @@ -45,4 +153,12 @@ if ( !jQueryVersionSince( "4.0.0" ) ) { } ); } + +// Don't trigger the above logic in jQuery >=4 by default as the JSON-to-JSONP +// auto-promotion behavior is gone in jQuery 4.0 and as it has security implications, +// we don't want to restore the legacy behavior by default. +if ( jQueryVersionSince( "4.0.0" ) ) { + jQuery.migrateDisablePatches( "jsonp-promotion" ); +} + } diff --git a/test/data/ajax-jsonp-callback-name.html b/test/data/ajax-jsonp-callback-name.html new file mode 100644 index 00000000..2d10331a --- /dev/null +++ b/test/data/ajax-jsonp-callback-name.html @@ -0,0 +1,87 @@ + + + + + jQuery Migrate test for re-using JSONP callback name with `dataType: "json"` + + + + + + + + + +

jQuery Migrate test for re-using JSONP callback name

+ + diff --git a/test/data/jsonpScript.js b/test/data/jsonpScript.js new file mode 100644 index 00000000..4623075e --- /dev/null +++ b/test/data/jsonpScript.js @@ -0,0 +1,5 @@ +/* global customJsonpCallback */ + +"use strict"; + +customJsonpCallback( { answer: 42 } ); diff --git a/test/data/testinit.js b/test/data/testinit.js index 019fffd9..95da904b 100644 --- a/test/data/testinit.js +++ b/test/data/testinit.js @@ -233,6 +233,9 @@ // Re-disable patches disabled by default jQuery.migrateDisablePatches( "self-closed-tags" ); + if ( jQueryVersionSince( "4.0.0" ) ) { + jQuery.migrateDisablePatches( "jsonp-promotion" ); + } } } ); } diff --git a/test/index.html b/test/index.html index a6aa9cfe..f6086625 100644 --- a/test/index.html +++ b/test/index.html @@ -14,6 +14,9 @@ + + + - - -