diff --git a/src/Curl/Curl.php b/src/Curl/Curl.php index b9b3c53..8c27cef 100644 --- a/src/Curl/Curl.php +++ b/src/Curl/Curl.php @@ -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; + } } diff --git a/tests/PHPCurlClass/PHPCurlClassTest.php b/tests/PHPCurlClass/PHPCurlClassTest.php index 3f6e310..aba7bb4 100644 --- a/tests/PHPCurlClass/PHPCurlClassTest.php +++ b/tests/PHPCurlClass/PHPCurlClassTest.php @@ -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() diff --git a/tests/PHPCurlClass/server.php b/tests/PHPCurlClass/server.php index 124f165..5cd005b 100644 --- a/tests/PHPCurlClass/server.php +++ b/tests/PHPCurlClass/server.php @@ -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']);