Fix #345: Allow POSTing multidimensional array with file

This commit is contained in:
Zach Borboa
2016-08-07 00:04:36 -07:00
parent 4b1084647b
commit 679ce8e3c4
3 changed files with 157 additions and 84 deletions
+64 -53
View File
@@ -135,40 +135,44 @@ class Curl
*/
public function buildPostData($data)
{
$binary_data = false;
if (is_array($data)) {
// Return JSON-encoded string when the request's content-type is JSON.
if (isset($this->headers['Content-Type']) &&
preg_match($this->jsonPattern, $this->headers['Content-Type'])) {
$json_str = json_encode($data);
if (!($json_str === false)) {
$data = $json_str;
}
} else if (self::is_array_multidim($data)) {
$data = self::http_build_multi_query($data);
} else {
$binary_data = false;
foreach ($data as $key => $value) {
// Fix "Notice: Array to string conversion" when $value in curl_setopt($ch, CURLOPT_POSTFIELDS,
// $value) is an array that contains an empty array.
if (is_array($value) && empty($value)) {
$data[$key] = '';
// Fix "curl_setopt(): The usage of the @filename API for file uploading is deprecated. Please use
// the CURLFile class instead". Ignore non-file values prefixed with the @ character.
} elseif (is_string($value) && strpos($value, '@') === 0 && is_file(substr($value, 1))) {
$binary_data = true;
if (class_exists('CURLFile')) {
$data[$key] = new \CURLFile(substr($value, 1));
}
} elseif ($value instanceof \CURLFile) {
$binary_data = true;
}
// Manually build a single-dimensional array from a multi-dimensional array as using curl_setopt($ch,
// CURLOPT_POSTFIELDS, $data) doesn't correctly handle multi-dimensional arrays when files are
// referenced.
if (self::is_array_multidim($data)) {
$data = self::array_flatten_multidim($data);
}
if (!$binary_data) {
$data = http_build_query($data, '', '&');
// Modify array values to ensure any referenced files are properly handled depending on the support of
// the @filename API or CURLFile usage. This also fixes the warning "curl_setopt(): The usage of the
// @filename API for file uploading is deprecated. Please use the CURLFile class instead". Ignore
// non-file values prefixed with the @ character.
if (class_exists('CURLFile')) {
foreach ($data as $key => $value) {
if (is_string($value) && strpos($value, '@') === 0 && is_file(substr($value, 1))) {
$binary_data = true;
$data[$key] = new \CURLFile(substr($value, 1));
} else if ($value instanceof \CURLFile) {
$binary_data = true;
}
}
}
}
}
if (!$binary_data && (is_array($data) || is_object($data))) {
$data = http_build_query($data, '', '&');
}
return $data;
}
@@ -579,7 +583,9 @@ class Curl
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT');
$put_data = $this->buildPostData($data);
if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) {
$this->setHeader('Content-Length', strlen($put_data));
if (is_string($put_data)) {
$this->setHeader('Content-Length', strlen($put_data));
}
}
if (!empty($put_data)) {
$this->setOpt(CURLOPT_POSTFIELDS, $put_data);
@@ -1153,38 +1159,6 @@ class Curl
return $response_headers;
}
/**
* Http Build Multi Query
*
* @access public
* @param $data
* @param $key
*
* @return string
*/
public static function http_build_multi_query($data, $key = null)
{
$query = array();
if (empty($data)) {
return $key . '=';
}
$is_array_assoc = self::is_array_assoc($data);
foreach ($data as $k => $value) {
if (is_string($value) || is_numeric($value)) {
$brackets = $is_array_assoc ? '[' . $k . ']' : '[]';
$query[] = urlencode($key === null ? $k : $key . $brackets) . '=' . rawurlencode($value);
} elseif (is_array($value)) {
$nested = $key === null ? $k : $key . '[' . $k . ']';
$query[] = self::http_build_multi_query($value, $nested);
}
}
return implode('&', $query);
}
/**
* Is Array Assoc
*
@@ -1214,4 +1188,41 @@ class Curl
return (bool)count(array_filter($array, 'is_array'));
}
/**
* Array Flatten Multidim
*
* @access public
* @param $array
* @param $prefix
*
* @return array
*/
public static function array_flatten_multidim($array, $prefix = false) {
$return = array();
if (is_array($array) || is_object($array)) {
if (empty($array)) {
$return[$prefix] = '';
} else {
foreach ($array as $key => $value) {
if (is_scalar($value)) {
if ($prefix) {
$return[$prefix . '[' . $key . ']'] = $value;
} else {
$return[$key] = $value;
}
} else {
if (class_exists('CURLFile') && $value instanceof \CURLFile) {
$return[$key] = $value;
} else {
$return = array_merge($return, self::array_flatten_multidim($value, $prefix ? $prefix . '[' . $key . ']' : $key));
}
}
}
}
} else if ($array === null) {
$return[$prefix] = $array;
}
return $return;
}
}
+86 -29
View File
@@ -310,25 +310,21 @@ class CurlTest extends PHPUnit_Framework_TestCase
public function testPostAssociativeArrayData()
{
$test = new Test();
$this->assertEquals(
'username=myusername' .
'&password=mypassword' .
'&more_data%5Bparam1%5D=something' .
'&more_data%5Bparam2%5D=other%20thing' .
'&more_data%5Bparam3%5D=123' .
'&more_data%5Bparam4%5D=3.14',
$test->server('post_multidimensional', 'POST', array(
'username' => 'myusername',
'password' => 'mypassword',
'more_data' => array(
'param1' => 'something',
'param2' => 'other thing',
'param3' => 123,
'param4' => 3.14,
),
))
$data = array(
'username' => 'myusername',
'password' => 'mypassword',
'more_data' => array(
'param1' => 'something',
'param2' => 'other thing',
'param3' => 123,
'param4' => 3.14,
),
);
$test = new Test();
$test->curl->setDefaultJsonDecoder(true);
$response = $test->server('post_multidimensional', 'POST', $data);
$this->assertEquals($data, $response['post']);
}
public function testPostContentLength()
@@ -353,18 +349,79 @@ class CurlTest extends PHPUnit_Framework_TestCase
public function testPostMultidimensionalData()
{
$test = new Test();
$this->assertEquals(
'key=file&file%5B%5D=wibble&file%5B%5D=wubble&file%5B%5D=wobble',
$test->server('post_multidimensional', 'POST', array(
'key' => 'file',
'file' => array(
'wibble',
'wubble',
'wobble',
),
))
$data = array(
'key' => 'file',
'file' => array(
'wibble',
'wubble',
'wobble',
),
);
$this->assertEquals('key=file&file[0]=wibble&file[1]=wubble&file[2]=wobble', urldecode(http_build_query($data)));
$test = new Test();
$test->curl->setDefaultJsonDecoder(true);
$response = $test->server('post_multidimensional', 'POST', $data);
$this->assertEquals($data, $response['post']);
}
public function testPostMultidimensionalDataWithFile()
{
$tests = array();
$file_path_1 = Helper\get_png();
$tests[] = array(
'file_path' => $file_path_1,
'post_data_image' => '@' . $file_path_1,
);
if (class_exists('CURLFile')) {
$file_path_2 = Helper\get_png();
$tests[] = array(
'file_path' => $file_path_2,
'post_data_image' => new CURLFile($file_path_2),
);
}
foreach ($tests as $test_data) {
$file_path = $test_data['file_path'];
$post_data_image = $test_data['post_data_image'];
$test = new Test();
// Return associative for comparison.
$assoc = true;
$test->curl->setDefaultJsonDecoder($assoc);
// Keep POST data separate from FILES data for comparison.
$post_data_without_file = array(
'key' => 'value',
'alpha' => array(
'a' => '1',
'b' => '2',
'c' => '3',
),
);
$post_data = $post_data_without_file;
$post_data['image'] = $post_data_image;
$test->server('post_multidimensional_with_file', 'POST', $post_data);
// Expect "Content-Type: multipart/form-data" in request headers.
preg_match('/^multipart\/form-data; boundary=/', $test->curl->requestHeaders['Content-Type'], $content_type);
$this->assertTrue(!empty($content_type));
// Expect received POST data to match POSTed data less the file.
$this->assertTrue($test->curl->response['post'] === $post_data_without_file);
// Expect POSTed files is received as $_FILES.
$this->assertTrue(isset($test->curl->response['files']['image']['tmp_name']));
$this->assertEquals(0, $test->curl->response['files']['image']['error']);
unlink($file_path);
$this->assertFalse(file_exists($file_path));
}
}
public function testPostFilePathUpload()
+7 -2
View File
@@ -110,8 +110,13 @@ if ($test === 'http_basic_auth') {
} elseif ($test === 'patch') {
echo $http_raw_post_data;
exit;
} elseif ($test === 'post_multidimensional') {
echo $http_raw_post_data;
} elseif ($test === 'post_multidimensional' ||
$test === 'post_multidimensional_with_file') {
header('Content-Type: application/json');
echo json_encode(array(
'post' => $_POST,
'files' => $_FILES,
), JSON_PRETTY_PRINT);
exit;
} elseif ($test === 'post_file_path_upload') {
echo Helper\mime_type($_FILES[$key]['tmp_name']);