Ajax: Support binary data (including FormData)

Two changes have been applied:
* prefilters are now applied before data is converted to a string;
  this allows prefilters to disable such a conversion
* a prefilter for binary data is added; it disables data conversion
  for non-string non-plain-object `data`; for `FormData` bodies, it
  removes manually-set `Content-Type` header - this is required
  as browsers need to append their own boundary to the header

Ref gh-4150
Closes gh-5197
This commit is contained in:
Michał Gołębiowski-Owczarek 2023-02-01 13:48:35 +01:00 committed by GitHub
parent 0b9c5037f7
commit a7ed9a7b63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 109 additions and 5 deletions

View File

@ -55,6 +55,7 @@
"karma-qunit": "4.1.2", "karma-qunit": "4.1.2",
"karma-webkit-launcher": "2.1.0", "karma-webkit-launcher": "2.1.0",
"load-grunt-tasks": "5.1.0", "load-grunt-tasks": "5.1.0",
"multiparty": "4.2.3",
"native-promise-only": "0.8.1", "native-promise-only": "0.8.1",
"playwright-webkit": "1.29.2", "playwright-webkit": "1.29.2",
"promises-aplus-tests": "2.1.2", "promises-aplus-tests": "2.1.2",

View File

@ -562,14 +562,14 @@ jQuery.extend( {
} }
} }
// Apply prefilters
inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
// Convert data if not already a string // Convert data if not already a string
if ( s.data && s.processData && typeof s.data !== "string" ) { if ( s.data && s.processData && typeof s.data !== "string" ) {
s.data = jQuery.param( s.data, s.traditional ); s.data = jQuery.param( s.data, s.traditional );
} }
// Apply prefilters
inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
// If request was aborted inside a prefilter, stop there // If request was aborted inside a prefilter, stop there
if ( completed ) { if ( completed ) {
return jqXHR; return jqXHR;

17
src/ajax/binary.js Normal file
View File

@ -0,0 +1,17 @@
import jQuery from "../core.js";
import "../ajax.js";
jQuery.ajaxPrefilter( function( s ) {
// Binary data needs to be passed to XHR as-is without stringification.
if ( typeof s.data !== "string" && !jQuery.isPlainObject( s.data ) ) {
s.processData = false;
}
// `Content-Type` for requests with `FormData` bodies needs to be set
// by the browser as it needs to append the `boundary` it generated.
if ( s.data instanceof window.FormData ) {
s.contentType = false;
}
} );

1
src/jquery.js vendored
View File

@ -23,6 +23,7 @@ import "./ajax.js";
import "./ajax/xhr.js"; import "./ajax/xhr.js";
import "./ajax/script.js"; import "./ajax/script.js";
import "./ajax/jsonp.js"; import "./ajax/jsonp.js";
import "./ajax/binary.js";
import "./ajax/load.js"; import "./ajax/load.js";
import "./core/parseXML.js"; import "./core/parseXML.js";
import "./core/parseHTML.js"; import "./core/parseHTML.js";

View File

@ -124,6 +124,17 @@ QUnit.assert.ok( true, "mock executed");';
echo "$cleanCallback($text)\n"; echo "$cleanCallback($text)\n";
} }
protected function formData( $req ) {
$prefix = 'multipart/form-data; boundary=--';
$contentTypeValue = $req->headers[ 'CONTENT-TYPE' ];
if ( substr( $contentTypeValue, 0, strlen( $prefix ) ) === $prefix ) {
echo 'key1 -> ' . $_POST[ 'key1' ] . ', key2 -> ' . $_POST[ 'key2' ];
} else {
echo 'Incorrect Content-Type: ' . $contentTypeValue .
"\nExpected prefix: " . $prefix;
}
}
protected function error( $req ) { protected function error( $req ) {
header( 'HTTP/1.0 400 Bad Request' ); header( 'HTTP/1.0 400 Bad Request' );
if ( isset( $req->query['json'] ) ) { if ( isset( $req->query['json'] ) ) {

View File

@ -174,8 +174,11 @@ function url( value ) {
} }
// Ajax testing helper // Ajax testing helper
this.ajaxTest = function( title, expect, options ) { this.ajaxTest = function( title, expect, options, wrapper ) {
QUnit.test( title, function( assert ) { if ( !wrapper ) {
wrapper = QUnit.test;
}
wrapper.call( QUnit, title, function( assert ) {
assert.expect( expect ); assert.expect( expect );
var requestOptions; var requestOptions;

View File

@ -3,6 +3,7 @@
const url = require( "url" ); const url = require( "url" );
const fs = require( "fs" ); const fs = require( "fs" );
const getRawBody = require( "raw-body" ); const getRawBody = require( "raw-body" );
const multiparty = require( "multiparty" );
let cspLog = ""; let cspLog = "";
@ -141,6 +142,19 @@ const mocks = {
resp.writeHead( 200 ); resp.writeHead( 200 );
resp.end( `${ cleanCallback( callback ) }(${ JSON.stringify( body ) })\n` ); resp.end( `${ cleanCallback( callback ) }(${ JSON.stringify( body ) })\n` );
}, },
formData: function( req, resp, next ) {
const prefix = "multipart/form-data; boundary=--";
const contentTypeValue = req.headers[ "content-type" ];
resp.writeHead( 200 );
if ( ( prefix || "" ).startsWith( prefix ) ) {
getMultiPartContent( req ).then( function( { fields = {} } ) {
resp.end( `key1 -> ${ fields.key1 }, key2 -> ${ fields.key2 }` );
}, next );
} else {
resp.end( `Incorrect Content-Type: ${ contentTypeValue
}\nExpected prefix: ${ prefix }` );
}
},
error: function( req, resp ) { error: function( req, resp ) {
if ( req.query.json ) { if ( req.query.json ) {
resp.writeHead( 400, { "content-type": "application/json" } ); resp.writeHead( 400, { "content-type": "application/json" } );
@ -363,4 +377,18 @@ function getBody( req ) {
} ); } );
} }
function getMultiPartContent( req ) {
return new Promise( function( resolve ) {
if ( req.method !== "POST" ) {
resolve( "" );
return;
}
const form = new multiparty.Form();
form.parse( req, function( _err, fields, files ) {
resolve( { fields, files } );
} );
} );
}
module.exports = MockserverMiddlewareFactory; module.exports = MockserverMiddlewareFactory;

View File

@ -3105,4 +3105,47 @@ if ( typeof window.ArrayBuffer === "undefined" || typeof new XMLHttpRequest().re
assert.ok( jQuery.active === 0, "ajax active counter should be zero: " + jQuery.active ); assert.ok( jQuery.active === 0, "ajax active counter should be zero: " + jQuery.active );
} ); } );
ajaxTest( "jQuery.ajax() - FormData", 1, function( assert ) {
var formData = new FormData();
formData.append( "key1", "value1" );
formData.append( "key2", "value2" );
return {
url: url( "mock.php?action=formData" ),
method: "post",
data: formData,
success: function( data ) {
assert.strictEqual( data, "key1 -> value1, key2 -> value2",
"FormData sent correctly" );
}
};
} );
ajaxTest( "jQuery.ajax() - URLSearchParams", 1, function( assert ) {
var urlSearchParams = new URLSearchParams();
urlSearchParams.append( "name", "peter" );
return {
url: url( "mock.php?action=name" ),
method: "post",
data: urlSearchParams,
success: function( data ) {
assert.strictEqual( data, "pan", "URLSearchParams sent correctly" );
}
};
}, QUnit.testUnlessIE );
ajaxTest( "jQuery.ajax() - Blob", 1, function( assert ) {
var blob = new Blob( [ "name=peter" ], { type: "text/plain" } );
return {
url: url( "mock.php?action=name" ),
method: "post",
data: blob,
success: function( data ) {
assert.strictEqual( data, "pan", "Blob sent correctly" );
}
};
} );
} )(); } )();