Skip to content

Commit 560ce28

Browse files
committed
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.
1 parent 95d05ce commit 560ce28

File tree

6 files changed

+252
-85
lines changed

6 files changed

+252
-85
lines changed

eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export default [
126126
QUnit: false,
127127
url: true,
128128
compareVersions: true,
129+
jQueryVersionSince: false,
129130
expectWarning: true,
130131
expectNoWarning: true,
131132
startIframeTest: true,

src/jquery/ajax.js

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { migrateWarn, migratePatchAndWarnFunc, migratePatchFunc } from "../main.
55
if ( jQuery.ajax ) {
66

77
var oldAjax = jQuery.ajax,
8-
rjsonp = /(=)\?(?=&|$)|\?\?/;
8+
oldCallbacks = [],
9+
guid = "migrate-" + Date.now(),
10+
origJsonpCallback = jQuery.ajaxSettings.jsonpCallback,
11+
rjsonp = /(=)\?(?=&|$)|\?\?/,
12+
rquery = /\?/;
913

1014
migratePatchFunc( jQuery, "ajax", function() {
1115
var jQXHR = oldAjax.apply( this, arguments );
@@ -23,16 +27,120 @@ migratePatchFunc( jQuery, "ajax", function() {
2327
return jQXHR;
2428
}, "jqXHR-methods" );
2529

26-
// Only trigger the logic in jQuery <4 as the JSON-to-JSONP auto-promotion
27-
// behavior is gone in jQuery 4.0 and as it has security implications, we don't
28-
// want to restore the legacy behavior.
29-
if ( !jQueryVersionSince( "4.0.0" ) ) {
30+
jQuery.ajaxSetup( {
31+
jsonpCallback: function() {
3032

31-
// Register this prefilter before the jQuery one. Otherwise, a promoted
32-
// request is transformed into one with the script dataType and we can't
33-
// catch it anymore.
33+
// Source is virtually the same as in Core, but we need to duplicate
34+
// to maintain a proper `oldCallbacks` reference.
35+
if ( jQuery.migrateIsPatchEnabled( "jsonp-promotion" ) ) {
36+
var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( guid++ ) );
37+
this[ callback ] = true;
38+
return callback;
39+
} else {
40+
return origJsonpCallback.apply( this, arguments );
41+
}
42+
}
43+
} );
44+
45+
// Register this prefilter before the jQuery one. Otherwise, a promoted
46+
// request is transformed into one with the script dataType, and we can't
47+
// catch it anymore.
48+
if ( jQueryVersionSince( "4.0.0" ) ) {
49+
50+
// Code mostly from:
51+
// https://github.com/jquery/jquery/blob/fa0058af426c4e482059214c29c29f004254d9a1/src/ajax/jsonp.js#L20-L97
52+
jQuery.ajaxPrefilter( "+json", function( s, originalSettings, jqXHR ) {
53+
54+
if ( !jQuery.migrateIsPatchEnabled( "jsonp-promotion" ) ) {
55+
return;
56+
}
57+
58+
var callbackName, overwritten, responseContainer,
59+
jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
60+
"url" :
61+
typeof s.data === "string" &&
62+
( s.contentType || "" )
63+
.indexOf( "application/x-www-form-urlencoded" ) === 0 &&
64+
rjsonp.test( s.data ) && "data"
65+
);
66+
67+
// Handle iff the expected data type is "jsonp" or we have a parameter to set
68+
if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
69+
migrateWarn( "jsonp-promotion", "JSON-to-JSONP auto-promotion is deprecated" );
70+
71+
// Get callback name, remembering preexisting value associated with it
72+
callbackName = s.jsonpCallback = typeof s.jsonpCallback === "function" ?
73+
s.jsonpCallback() :
74+
s.jsonpCallback;
75+
76+
// Insert callback into url or form data
77+
if ( jsonProp ) {
78+
s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
79+
} else if ( s.jsonp !== false ) {
80+
s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
81+
}
82+
83+
// Use data converter to retrieve json after script execution
84+
s.converters[ "script json" ] = function() {
85+
if ( !responseContainer ) {
86+
jQuery.error( callbackName + " was not called" );
87+
}
88+
return responseContainer[ 0 ];
89+
};
90+
91+
// Force json dataType
92+
s.dataTypes[ 0 ] = "json";
93+
94+
// Install callback
95+
overwritten = window[ callbackName ];
96+
window[ callbackName ] = function() {
97+
responseContainer = arguments;
98+
};
99+
100+
// Clean-up function (fires after converters)
101+
jqXHR.always( function() {
102+
103+
// If previous value didn't exist - remove it
104+
if ( overwritten === undefined ) {
105+
jQuery( window ).removeProp( callbackName );
106+
107+
// Otherwise restore preexisting value
108+
} else {
109+
window[ callbackName ] = overwritten;
110+
}
111+
112+
// Save back as free
113+
if ( s[ callbackName ] ) {
114+
115+
// Make sure that re-using the options doesn't screw things around
116+
s.jsonpCallback = originalSettings.jsonpCallback;
117+
118+
// Save the callback name for future use
119+
oldCallbacks.push( callbackName );
120+
}
121+
122+
// Call if it was a function and we have a response
123+
if ( responseContainer && typeof overwritten === "function" ) {
124+
overwritten( responseContainer[ 0 ] );
125+
}
126+
127+
responseContainer = overwritten = undefined;
128+
} );
129+
130+
// Delegate to script
131+
return "script";
132+
}
133+
} );
134+
} else {
135+
136+
// jQuery <4 already contains this prefixer; don't duplicate the whole logic,
137+
// but only enough to know when to warn.
34138
jQuery.ajaxPrefilter( "+json", function( s ) {
35139

140+
if ( !jQuery.migrateIsPatchEnabled( "jsonp-promotion" ) ) {
141+
return;
142+
}
143+
36144
// Warn if JSON-to-JSONP auto-promotion happens.
37145
if ( s.jsonp !== false && ( rjsonp.test( s.url ) ||
38146
typeof s.data === "string" &&
@@ -45,4 +153,12 @@ if ( !jQueryVersionSince( "4.0.0" ) ) {
45153
} );
46154
}
47155

156+
157+
// Don't trigger the above logic in jQuery >=4 by default as the JSON-to-JSONP
158+
// auto-promotion behavior is gone in jQuery 4.0 and as it has security implications,
159+
// we don't want to restore the legacy behavior by default.
160+
if ( jQueryVersionSince( "4.0.0" ) ) {
161+
jQuery.migrateDisablePatches( "jsonp-promotion" );
162+
}
163+
48164
}

test/data/jsonpScript.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* global customJsonpCallback */
2+
3+
"use strict";
4+
5+
customJsonpCallback( { answer: 42 } );

test/data/testinit.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@
233233

234234
// Re-disable patches disabled by default
235235
jQuery.migrateDisablePatches( "self-closed-tags" );
236+
if ( jQueryVersionSince( "4.0.0" ) ) {
237+
jQuery.migrateDisablePatches( "jsonp-promotion" );
238+
}
236239
}
237240
} );
238241
}

test/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
<!-- A promise polyfill -->
1515
<script src="../external/npo/npo.js"></script>
1616

17+
<!-- Version comparisons -->
18+
<script src="data/compareVersions.js"></script>
19+
1720
<!-- Load a jQuery and jquery-migrate plugin file based on URL -->
1821
<script src="data/testinit.js"></script>
1922
<script>
@@ -25,9 +28,6 @@
2528
TestManager.loadProject( "jquery-migrate", "min", true );
2629
</script>
2730

28-
<!-- Version comparisons -->
29-
<script src="data/compareVersions.js"></script>
30-
3131
<!-- Unit test files -->
3232
<script>
3333
TestManager.loadTests();

0 commit comments

Comments
 (0)