Skip to content

Commit

Permalink
Ajax: Fill in & warn against automatic JSON-to-JSONP promotion
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mgol committed Sep 16, 2024
1 parent 95d05ce commit 87d27c7
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 85 deletions.
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export default [
QUnit: false,
url: true,
compareVersions: true,
jQueryVersionSince: false,
expectWarning: true,
expectNoWarning: true,
startIframeTest: true,
Expand Down
115 changes: 107 additions & 8 deletions src/jquery/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { migrateWarn, migratePatchAndWarnFunc, migratePatchFunc } from "../main.
if ( jQuery.ajax ) {

var oldAjax = jQuery.ajax,
rjsonp = /(=)\?(?=&|$)|\?\?/;
oldCallbacks = [],
rjsonp = /(=)\?(?=&|$)|\?\?/,
rquery = /\?/;

migratePatchFunc( jQuery, "ajax", function() {
var jQXHR = oldAjax.apply( this, arguments );
Expand All @@ -23,16 +25,105 @@ 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" ) ) {
// 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" ) ) {

// 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.
// 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" &&
Expand All @@ -45,4 +136,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" );
}

}
5 changes: 5 additions & 0 deletions test/data/jsonpScript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* global customJsonpCallback */

"use strict";

customJsonpCallback( { answer: 42 } );
3 changes: 3 additions & 0 deletions test/data/testinit.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@

// Re-disable patches disabled by default
jQuery.migrateDisablePatches( "self-closed-tags" );
if ( jQueryVersionSince( "4.0.0" ) ) {
jQuery.migrateDisablePatches( "jsonp-promotion" );
}
}
} );
}
Expand Down
6 changes: 3 additions & 3 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<!-- A promise polyfill -->
<script src="../external/npo/npo.js"></script>

<!-- Version comparisons -->
<script src="data/compareVersions.js"></script>

<!-- Load a jQuery and jquery-migrate plugin file based on URL -->
<script src="data/testinit.js"></script>
<script>
Expand All @@ -25,9 +28,6 @@
TestManager.loadProject( "jquery-migrate", "min", true );
</script>

<!-- Version comparisons -->
<script src="data/compareVersions.js"></script>

<!-- Unit test files -->
<script>
TestManager.loadTests();
Expand Down
190 changes: 116 additions & 74 deletions test/unit/jquery/ajax.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,95 +27,137 @@ QUnit.test( "jQuery.ajax() deprecations on jqXHR", function( assert ) {
} );

[ " - Same Domain", " - Cross Domain" ].forEach( function( label, crossDomain ) {
function runTests( options ) {
var forceEnablePatch = ( options || {} ).forceEnablePatch || false;

// 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.
QUnit[ jQueryVersionSince( "4.0.0" ) ? "skip" : "test" ](
"jQuery.ajax() JSON-to-JSONP auto-promotion" + label, function( assert ) {
QUnit.test( "jQuery.ajax() JSON-to-JSONP auto-promotion" + label + (
forceEnablePatch ? ", patch force-enabled" : ""
), function( assert ) {

assert.expect( 5 );
assert.expect( 10 );

var done = assert.async(),
tests = [
function() {
return expectNoWarning( assert, "dataType: \"json\"",
function() {
if ( forceEnablePatch ) {
jQuery.migrateEnablePatches( "jsonp-promotion" );
}

var done = assert.async(),
patchEnabled = forceEnablePatch || !jQueryVersionSince( "4.0.0" ),
tests = [
function() {
var testName = "dataType: \"json\"";
return expectNoWarning( assert, testName, function() {
return jQuery.ajax( {
url: url( "null.json" ),
context: { testName: testName },
crossDomain: crossDomain,
dataType: "json"
} ).catch( jQuery.noop );
}
);
},

function() {
return expectWarning( assert, "dataType: \"json\", URL callback", 1,
function() {
dataType: "json",
jsonpCallback: "customJsonpCallback"
} ).then( function() {
assert.ok( true, this.testName + " (success)" );
} ).catch( function() {
assert.ok( false, this.testName + " (failure)" );
} );
} );
},

function() {
var testName = "dataType: \"json\", URL callback";
return expectWarning( assert, testName, patchEnabled ? 1 : 0, function() {
return jQuery.ajax( {
url: url( "null.json?callback=?" ),
url: url( "jsonpScript.js?callback=?" ),
context: { testName: testName },
crossDomain: crossDomain,
dataType: "json"
} ).catch( jQuery.noop );
}
);
},

function() {
return expectWarning( assert, "dataType: \"json\", data callback", 1,
function() {
dataType: "json",
jsonpCallback: "customJsonpCallback"
} ).then( function() {
assert.ok( patchEnabled, this.testName + " (success)" );
} ).catch( function() {
assert.ok( !patchEnabled, this.testName + " (failure)" );
} );
} );
},

function() {
var testName = "dataType: \"json\", data callback";
return expectWarning( assert, testName, patchEnabled ? 1 : 0, function() {
return jQuery.ajax( {
url: url( "null.json" ),
url: url( "jsonpScript.js" ),
context: { testName: testName },
crossDomain: crossDomain,
data: "callback=?",
dataType: "json"
} ).catch( jQuery.noop );
}
);
},

function() {
return expectNoWarning( assert, "dataType: \"jsonp\", URL callback",
function() {
dataType: "json",
jsonpCallback: "customJsonpCallback"
} ).then( function() {
assert.ok( patchEnabled, this.testName + " (success)" );
} ).catch( function() {
assert.ok( !patchEnabled, this.testName + " (failure)" );
} );
} );
},

function() {
var testName = "dataType: \"jsonp\", URL callback";
return expectNoWarning( assert, testName, function() {
return jQuery.ajax( {
url: url( "null.json?callback=?" ),
url: url( "jsonpScript.js?callback=?" ),
context: { testName: testName },
crossDomain: crossDomain,
dataType: "jsonp"
} ).catch( jQuery.noop );
}
);
},

function() {
return expectNoWarning( assert, "dataType: \"jsonp\", data callback",
function() {
return jQuery.ajax( {
url: url( "null.json" ),
dataType: "jsonp",
jsonpCallback: "customJsonpCallback"
} ).then( function() {
assert.ok( true, this.testName + " (success)" );
} ).catch( function() {
assert.ok( false, this.testName + " (failure)" );
} );
} );
},

function() {
var testName = "dataType: \"jsonp\", data callback";
return expectNoWarning( assert, testName, function() {
return jQuery.ajax( {
url: url( "jsonpScript.js" ),
context: { testName: testName },
crossDomain: crossDomain,
data: "callback=?",
dataType: "jsonp"
} ).catch( jQuery.noop );
}
);
}
];

// Invoke tests sequentially as they're async and early tests could get warnings
// from later ones.
function run( tests ) {
var test = tests[ 0 ];
return test().then( function() {
if ( tests.length > 1 ) {
return run( tests.slice( 1 ) );
}
} );
}

run( tests )
.then( function() {
done();
} );
} );
dataType: "jsonp",
jsonpCallback: "customJsonpCallback"
} ).then( function() {
assert.ok( true, this.testName + " (success)" );
} ).catch( function() {
assert.ok( false, this.testName + " (failure)" );
} );
} );
}
];

// Invoke tests sequentially as they're async and early tests could get warnings
// from later ones.
function run( tests ) {
var test = tests[ 0 ];
return test().then( function() {
if ( tests.length > 1 ) {
return run( tests.slice( 1 ) );
}
} );
}

run( tests )
.then( function() {
done();
} );
} );
}

if ( jQueryVersionSince( "4.0.0" ) ) {

// In jQuery 4+, this behavior is disabled by default for security
// reasons, re-enable for this test, but test default behavior as well.
runTests( { forceEnablePatch: true } );
runTests( { forceEnablePatch: false } );
} else {
runTests();
}
} );

}

0 comments on commit 87d27c7

Please sign in to comment.