From 3ebc94a1cd5526e19b072ad49c023e0b3fe984f0 Mon Sep 17 00:00:00 2001 From: Gennady Kovshenin Date: Sun, 11 Feb 2018 23:37:32 +0500 Subject: [PATCH 1/3] Initial cURL file upload support Inspired by #289 --- library/Requests.php | 9 +++++---- library/Requests/Transport/cURL.php | 25 +++++++++++++++++++----- library/Requests/Transport/fsockopen.php | 3 ++- tests/Transport/Base.php | 9 +++++++++ 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/library/Requests.php b/library/Requests.php index bb266189c..86c786acc 100644 --- a/library/Requests.php +++ b/library/Requests.php @@ -259,13 +259,14 @@ public static function trace($url, $headers = array(), $options = array()) { * @param array $headers * @param array $data * @param array $options + * @param array $files * @return Requests_Response */ /** * Send a POST request */ - public static function post($url, $headers = array(), $data = array(), $options = array()) { - return self::request($url, $headers, $data, self::POST, $options); + public static function post($url, $headers = array(), $data = array(), $options = array(), $files = array()) { + return self::request($url, $headers, $data, self::POST, $options, $files); } /** * Send a PUT request @@ -354,7 +355,7 @@ public static function patch($url, $headers, $data = array(), $options = array() * @param array $options Options for the request (see description for more information) * @return Requests_Response */ - public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array()) { + public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array(), $files = array()) { if (empty($options['type'])) { $options['type'] = $type; } @@ -376,7 +377,7 @@ public static function request($url, $headers = array(), $data = array(), $type $capabilities = array('ssl' => $need_ssl); $transport = self::get_transport($capabilities); } - $response = $transport->request($url, $headers, $data, $options); + $response = $transport->request($url, $headers, $data, $options, $files); $options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options)); diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 4429edb64..7e0339d83 100644 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -125,12 +125,13 @@ public function __destruct() { * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see Requests::response()} for documentation + * @param array $files File uploads * @return string Raw HTTP result */ - public function request($url, $headers = array(), $data = array(), $options = array()) { + public function request($url, $headers = array(), $data = array(), $options = array(), $files = array()) { $this->hooks = $options['hooks']; - $this->setup_handle($url, $headers, $data, $options); + $this->setup_handle($url, $headers, $data, $options, $files); $options['hooks']->dispatch('curl.before_send', array(&$this->handle)); @@ -305,8 +306,9 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see Requests::response()} for documentation + * @param array $files File uploads */ - protected function setup_handle($url, $headers, $data, $options) { + protected function setup_handle($url, $headers, $data, $options, $files = array()) { $options['hooks']->dispatch('curl.before_request', array(&$this->handle)); // Force closing the connection for old versions of cURL (<7.22). @@ -321,13 +323,26 @@ protected function setup_handle($url, $headers, $data, $options) { if ($data_format === 'query') { $url = self::format_get($url, $data); - $data = ''; + $data = array(); } - elseif (!is_string($data)) { + + if (!is_string($data) && empty($files)) { $data = http_build_query($data, null, '&'); } } + if (!empty($files)) { + if (function_exists('curl_file_create')) { + foreach($files as $key => $path) { + $data[$key] = curl_file_create($path); + } + } else { + foreach($files as $key => $path) { + $data[$key] = "@$path"; + } + } + } + switch ($options['type']) { case Requests::POST: curl_setopt($this->handle, CURLOPT_POST, true); diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index 21cb56d5e..a4fb2cb33 100644 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -53,9 +53,10 @@ class Requests_Transport_fsockopen implements Requests_Transport { * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see Requests::response()} for documentation + * @param array $files File uploads * @return string Raw HTTP result */ - public function request($url, $headers = array(), $data = array(), $options = array()) { + public function request($url, $headers = array(), $data = array(), $options = array(), $files = array()) { $options['hooks']->dispatch('fsockopen.before_request'); $url_parts = parse_url($url); diff --git a/tests/Transport/Base.php b/tests/Transport/Base.php index 566e09fad..9d1ffc016 100644 --- a/tests/Transport/Base.php +++ b/tests/Transport/Base.php @@ -845,4 +845,13 @@ public function testBodyDataFormat() { $this->assertEquals(httpbin('/post'), $result['url']); $this->assertEquals(array('test' => 'true', 'test2' => 'test'), $result['form']); } + + public function testFileUploads() { + file_put_contents($tmpfile = tempnam(sys_get_temp_dir(), 'requests'), 'some secret bytes, yo'); + $request = Requests::post('http://httpbin.org/post', array(), array('foo' => 'bar'), $this->getOptions(), array('file1' => $tmpfile)); + + $result = json_decode($request->body, true); + $this->assertEquals($result['files']['file1'], 'some secret bytes, yo'); + $this->assertEquals($result['form']['foo'], 'bar'); + } } From 1b60f1436f3d591a837272eade31346854f354b0 Mon Sep 17 00:00:00 2001 From: Gennady Kovshenin Date: Mon, 12 Feb 2018 00:54:38 +0500 Subject: [PATCH 2/3] Initial fsockopen transport upload support Next up: test multiple files, request_multi See #289 --- library/Requests/Transport/fsockopen.php | 32 ++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index a4fb2cb33..985b3471a 100644 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -154,14 +154,14 @@ public function request($url, $headers = array(), $data = array(), $options = ar $out = sprintf("%s %s HTTP/%.1f\r\n", $options['type'], $path, $options['protocol_version']); if ($options['type'] !== Requests::TRACE) { - if (is_array($data)) { + if (is_array($data) && empty($files)) { $request_body = http_build_query($data, null, '&'); } else { $request_body = $data; } - if (!empty($data)) { + if (!empty($data) && empty($files)) { if (!isset($case_insensitive_headers['Content-Length'])) { $headers['Content-Length'] = strlen($request_body); } @@ -170,6 +170,34 @@ public function request($url, $headers = array(), $data = array(), $options = ar $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; } } + + if (!empty($files)) { + $boundary = sha1(time()); + $headers['Content-Type'] = "multipart/form-data; boundary=$boundary"; + + $request_body = ''; + + if (!empty($data)) { + foreach ($data as $key => $value) { + $request_body .= "--$boundary\r\n"; + $request_body .= "Content-Disposition: form-data; name=\"$key\""; + $request_body .= "\r\n\r\n" . $value . "\r\n"; + } + } + + foreach ($files as $key => $path) { + $filename = basename($path); + $request_body .= "--$boundary\r\n"; + $request_body .= "Content-Disposition: form-data; name=\"$key\"; filename=\"$filename\""; + // @todo Compression, encoding (base64), etc. + // @todo Large files can hit PHP memory limits quite quickly + $request_body .= "\r\n\r\n" . file_get_contents($path) . "\r\n"; + } + + $request_body .= "--$boundary--\r\n\r\n"; + + $headers['Content-Length'] = strlen($request_body); + } } if (!isset($case_insensitive_headers['Host'])) { From a47689dfdfd6d74b175403b01efe7811347a6be4 Mon Sep 17 00:00:00 2001 From: Gennady Kovshenin Date: Thu, 15 Feb 2018 22:39:44 +0500 Subject: [PATCH 3/3] Introduce Requests_File for simple file uploads --- library/Requests.php | 9 ++- library/Requests/Exception/File.php | 5 ++ library/Requests/File.php | 70 ++++++++++++++++++++++++ library/Requests/Transport/cURL.php | 33 ++++++----- library/Requests/Transport/fsockopen.php | 34 +++++++----- tests/File.php | 39 +++++++++++++ tests/Transport/Base.php | 2 +- tests/phpunit.xml.dist | 1 + 8 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 library/Requests/Exception/File.php create mode 100644 library/Requests/File.php create mode 100644 tests/File.php diff --git a/library/Requests.php b/library/Requests.php index 86c786acc..bb266189c 100644 --- a/library/Requests.php +++ b/library/Requests.php @@ -259,14 +259,13 @@ public static function trace($url, $headers = array(), $options = array()) { * @param array $headers * @param array $data * @param array $options - * @param array $files * @return Requests_Response */ /** * Send a POST request */ - public static function post($url, $headers = array(), $data = array(), $options = array(), $files = array()) { - return self::request($url, $headers, $data, self::POST, $options, $files); + public static function post($url, $headers = array(), $data = array(), $options = array()) { + return self::request($url, $headers, $data, self::POST, $options); } /** * Send a PUT request @@ -355,7 +354,7 @@ public static function patch($url, $headers, $data = array(), $options = array() * @param array $options Options for the request (see description for more information) * @return Requests_Response */ - public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array(), $files = array()) { + public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array()) { if (empty($options['type'])) { $options['type'] = $type; } @@ -377,7 +376,7 @@ public static function request($url, $headers = array(), $data = array(), $type $capabilities = array('ssl' => $need_ssl); $transport = self::get_transport($capabilities); } - $response = $transport->request($url, $headers, $data, $options, $files); + $response = $transport->request($url, $headers, $data, $options); $options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options)); diff --git a/library/Requests/Exception/File.php b/library/Requests/Exception/File.php new file mode 100644 index 000000000..e4a62e6b8 --- /dev/null +++ b/library/Requests/Exception/File.php @@ -0,0 +1,5 @@ +path = $path; + $this->type = $type ? $type : mime_content_type($path); + $this->name = $name ? $name : basename($path); + } + + /** + * Retrieve the contents into a string. + * + * Caution: large files will fill up the RAM. + * + * @return string The contents. + */ + public function get_contents() { + return file_get_contents($this->path); + } +} diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index 7e0339d83..945f0efbf 100644 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -125,13 +125,12 @@ public function __destruct() { * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see Requests::response()} for documentation - * @param array $files File uploads * @return string Raw HTTP result */ - public function request($url, $headers = array(), $data = array(), $options = array(), $files = array()) { + public function request($url, $headers = array(), $data = array(), $options = array()) { $this->hooks = $options['hooks']; - $this->setup_handle($url, $headers, $data, $options, $files); + $this->setup_handle($url, $headers, $data, $options); $options['hooks']->dispatch('curl.before_send', array(&$this->handle)); @@ -306,9 +305,8 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see Requests::response()} for documentation - * @param array $files File uploads */ - protected function setup_handle($url, $headers, $data, $options, $files = array()) { + protected function setup_handle($url, $headers, $data, $options) { $options['hooks']->dispatch('curl.before_request', array(&$this->handle)); // Force closing the connection for old versions of cURL (<7.22). @@ -318,27 +316,34 @@ protected function setup_handle($url, $headers, $data, $options, $files = array( $headers = Requests::flatten($headers); + $files = array(); + if (!empty($data)) { $data_format = $options['data_format']; + if (is_array($data)) { + foreach($data as $key => $value) { + if ($value instanceof Requests_File) { + $files[$key] = $value; + } + } + } + if ($data_format === 'query') { $url = self::format_get($url, $data); $data = array(); } - - if (!is_string($data) && empty($files)) { - $data = http_build_query($data, null, '&'); - } } if (!empty($files)) { if (function_exists('curl_file_create')) { - foreach($files as $key => $path) { - $data[$key] = curl_file_create($path); + foreach($files as $key => $file) { + $data[$key] = curl_file_create($file->path, $file->type, $file->name); } - } else { - foreach($files as $key => $path) { - $data[$key] = "@$path"; + } + else { + foreach($files as $key => $file) { + $data[$key] = "@{$file->path}"; } } } diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index 985b3471a..02b6e9239 100644 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -53,10 +53,9 @@ class Requests_Transport_fsockopen implements Requests_Transport { * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD * @param array $options Request options, see {@see Requests::response()} for documentation - * @param array $files File uploads * @return string Raw HTTP result */ - public function request($url, $headers = array(), $data = array(), $options = array(), $files = array()) { + public function request($url, $headers = array(), $data = array(), $options = array()) { $options['hooks']->dispatch('fsockopen.before_request'); $url_parts = parse_url($url); @@ -154,6 +153,16 @@ public function request($url, $headers = array(), $data = array(), $options = ar $out = sprintf("%s %s HTTP/%.1f\r\n", $options['type'], $path, $options['protocol_version']); if ($options['type'] !== Requests::TRACE) { + $files = array(); + + if (is_array($data)) { + foreach($data as $key => $value) { + if ($value instanceof Requests_File) { + $files[$key] = $value; + } + } + } + if (is_array($data) && empty($files)) { $request_body = http_build_query($data, null, '&'); } @@ -180,18 +189,17 @@ public function request($url, $headers = array(), $data = array(), $options = ar if (!empty($data)) { foreach ($data as $key => $value) { $request_body .= "--$boundary\r\n"; - $request_body .= "Content-Disposition: form-data; name=\"$key\""; - $request_body .= "\r\n\r\n" . $value . "\r\n"; - } - } - foreach ($files as $key => $path) { - $filename = basename($path); - $request_body .= "--$boundary\r\n"; - $request_body .= "Content-Disposition: form-data; name=\"$key\"; filename=\"$filename\""; - // @todo Compression, encoding (base64), etc. - // @todo Large files can hit PHP memory limits quite quickly - $request_body .= "\r\n\r\n" . file_get_contents($path) . "\r\n"; + if ($value instanceof Requests_File) { + $request_body .= "Content-Disposition: form-data; name=\"$key\"; filename=\"$value->name\"\r\n"; + $request_body .= "Content-Type: $value->type"; + $request_body .= "\r\n\r\n" . $value->get_contents() . "\r\n"; + } + else { + $request_body .= "Content-Disposition: form-data; name=\"$key\""; + $request_body .= "\r\n\r\n" . $value . "\r\n"; + } + } } $request_body .= "--$boundary--\r\n\r\n"; diff --git a/tests/File.php b/tests/File.php new file mode 100644 index 000000000..d2ea89ccf --- /dev/null +++ b/tests/File.php @@ -0,0 +1,39 @@ +assertEquals($file->path, $tmpfile); + $this->assertEquals($file->type, 'text/plain'); + $this->assertEquals($file->name, 'readme.txt'); + + file_put_contents($tmpfile = tempnam(sys_get_temp_dir(), 'requests'), 'hello'); + $file = new Requests_File($tmpfile); + $this->assertEquals($file->name, basename($tmpfile)); + + $this->assertEquals($file->get_contents(), 'hello'); + } + + public function testMime() { + file_put_contents($tmpfile = tempnam(sys_get_temp_dir(), 'requests'), 'hello'); + $file = new Requests_File($tmpfile); + $this->assertEquals($file->type, 'text/plain'); + + file_put_contents($tmpfile = tempnam(sys_get_temp_dir(), 'requests'), "\xff\xd8\xff"); + $file = new Requests_File($tmpfile); + $this->assertEquals($file->type, 'image/jpeg'); + + file_put_contents($tmpfile = tempnam(sys_get_temp_dir(), 'requests'), "\x78\x01"); + $file = new Requests_File($tmpfile); + $this->assertEquals($file->type, 'application/octet-stream'); + } +} diff --git a/tests/Transport/Base.php b/tests/Transport/Base.php index 9d1ffc016..f3438f1f4 100644 --- a/tests/Transport/Base.php +++ b/tests/Transport/Base.php @@ -848,7 +848,7 @@ public function testBodyDataFormat() { public function testFileUploads() { file_put_contents($tmpfile = tempnam(sys_get_temp_dir(), 'requests'), 'some secret bytes, yo'); - $request = Requests::post('http://httpbin.org/post', array(), array('foo' => 'bar'), $this->getOptions(), array('file1' => $tmpfile)); + $request = Requests::post('http://httpbin.org/post', array(), array('foo' => 'bar', 'file1' => new Requests_File($tmpfile)), $this->getOptions()); $result = json_decode($request->body, true); $this->assertEquals($result['files']['file1'], 'some secret bytes, yo'); diff --git a/tests/phpunit.xml.dist b/tests/phpunit.xml.dist index 64e0b8260..1e3602332 100644 --- a/tests/phpunit.xml.dist +++ b/tests/phpunit.xml.dist @@ -13,6 +13,7 @@ ChunkedEncoding.php Cookies.php + File.php IDNAEncoder.php IRI.php Requests.php