diff --git a/doc/ChangeLog b/doc/ChangeLog index 98946c8a45..961597ed1c 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,3 +1,10 @@ +2012-09-12 Alexey S. Denisov + + * main/Flow/HttpRequest.class.php + main/Net/Http/CurlHttpClient.class.php + main/Utils/UrlParamsUtils.class.php: + allowing curlHttpClient upload files and send post/get arrays with any deep lvl + 2012-09-06 Igor V. Gulyaev * main/Base/Hstore.class.php diff --git a/doc/Migration1.0-1.1 b/doc/Migration1.0-1.1 new file mode 100644 index 0000000000..3d7970d98c --- /dev/null +++ b/doc/Migration1.0-1.1 @@ -0,0 +1 @@ +https://github.com/onPHP/onphp-framework/wiki/Ru%3A1.0to1.1 \ No newline at end of file diff --git a/global.inc.php.tpl b/global.inc.php.tpl index efad9a5c6e..56e75d04d0 100644 --- a/global.inc.php.tpl +++ b/global.inc.php.tpl @@ -76,6 +76,12 @@ ONPHP_ROOT_PATH.'meta'.DIRECTORY_SEPARATOR ); + /** + * @deprecated + */ + if (!defined('ONPHP_CURL_CLIENT_OLD_TO_STRING')) + define('ONPHP_CURL_CLIENT_OLD_TO_STRING', false); + define('ONPHP_META_CLASSES', ONPHP_META_PATH.'classes'.DIRECTORY_SEPARATOR); define( diff --git a/main/Flow/HttpRequest.class.php b/main/Flow/HttpRequest.class.php index 42b1eb25a6..44c044a6cb 100644 --- a/main/Flow/HttpRequest.class.php +++ b/main/Flow/HttpRequest.class.php @@ -29,7 +29,7 @@ final class HttpRequest // reference, not copy private $session = array(); - // uploads + // uploads and downloads (CurlHttpClient) private $files = array(); // all other sh1t @@ -37,10 +37,19 @@ final class HttpRequest private $headers = array(); + /** + * @var HttpMethod + */ private $method = null; + /** + * @var HttpUrl + */ private $url = null; + //for CurlHttpClient if you need to send raw CURLOPT_POSTFIELDS + private $body = null; + /** * @return HttpRequest **/ @@ -341,5 +350,25 @@ public function getUrl() { return $this->url; } + + public function hasBody() + { + return $this->body !== null; + } + + public function getBody() + { + return $this->body; + } + + /** + * @param string $body + * @return HttpRequest + */ + public function setBody($body) + { + $this->body = $body; + return $this; + } } ?> \ No newline at end of file diff --git a/main/Net/Http/CurlHttpClient.class.php b/main/Net/Http/CurlHttpClient.class.php index 574371cd9c..0c528dc848 100644 --- a/main/Net/Http/CurlHttpClient.class.php +++ b/main/Net/Http/CurlHttpClient.class.php @@ -8,7 +8,6 @@ * License, or (at your option) any later version. * * * ***************************************************************************/ -/* $Id: CurlHttpClient.class.php 45 2009-05-08 07:41:33Z lom $ */ /** * @ingroup Http @@ -23,6 +22,10 @@ final class CurlHttpClient implements HttpClient private $multiRequests = array(); private $multiResponses = array(); private $multiThreadOptions = array(); + /** + * @deprecated in the furure will work like this value is false; + */ + private $oldUrlConstructor = ONPHP_CURL_CLIENT_OLD_TO_STRING; /** * @return CurlHttpClient @@ -148,6 +151,26 @@ public function getMaxFileSize() return $this->maxFileSize; } + /** + * @deprecated in the future value always false and method will be removed + * @param bool $oldUrlConstructor + * @return CurlHttpClient + */ + public function setOldUrlConstructor($oldUrlConstructor = false) + { + $this->oldUrlConstructor = ($oldUrlConstructor == true); + return $this; + } + + /** + * @deprecated in the future value always false and method will be removed + * @return bool + */ + public function isOldUrlConstructor() + { + return $this->oldUrlConstructor; + } + /** * @return CurlHttpClient **/ @@ -280,9 +303,14 @@ protected function makeHandle(HttpRequest $request, CurlHttpResponse $response) break; case HttpMethod::POST: + if ($request->getGet()) + $options[CURLOPT_URL] .= + ($request->getUrl()->getQuery() ? '&' : '?') + .$this->argumentsToString($request->getGet()); + $options[CURLOPT_POST] = true; - $options[CURLOPT_POSTFIELDS] = - $this->argumentsToString($request->getPost()); + $options[CURLOPT_POSTFIELDS] = $this->getPostFields($request); + break; default: @@ -337,23 +365,68 @@ protected function makeResponse($handle, CurlHttpResponse $response) return $this; } - private function argumentsToString($array) + private function argumentsToString($array, $isFile = false) { - Assert::isArray($array); - $result = array(); - - foreach ($array as $key => $value) { - if (is_array($value)) { - foreach ($value as $valueKey => $simpleValue) { - $result[] = - $key.'['.$valueKey.']='.urlencode($simpleValue); - } + if ($this->oldUrlConstructor) + return UrlParamsUtils::toStringOneDeepLvl($array); + else + return UrlParamsUtils::toString($array); + } + + private function getPostFields(HttpRequest $request) + { + if ($request->hasBody()) { + return $request->getBody(); + } else { + if ($this->oldUrlConstructor) { + return UrlParamsUtils::toStringOneDeepLvl($request->getPost()); } else { - $result[] = $key.'='.urlencode($value); + $fileList = array_map( + array($this, 'fileFilter'), + UrlParamsUtils::toParamsList($request->getFiles()) + ); + if (empty($fileList)) { + return UrlParamsUtils::toString($request->getPost()); + } else { + $postList = UrlParamsUtils::toParamsList($request->getPost()); + if (!is_null($atParam = $this->findAtParamInPost($postList))) + throw new NetworkException( + 'Security excepion: not allowed send post param '.$atParam + . ' which begins from @ in request which contains files' + ); + + return array_merge($postList, $fileList); + } } } - - return implode('&', $result); + } + + /** + * Return param name which start with symbol @ or null + * @param array $postList + * @return string|null + */ + private function findAtParamInPost($postList) + { + foreach ($postList as $param) + if (mb_stripos($param, '@') === 0) + return $param; + + return null; + } + + /** + * using in getPostFields - array_map func + * @param string $value + * @return string + */ + private function fileFilter($value) + { + Assert::isTrue( + is_readable($value) && is_file($value), + 'couldn\'t access to file with path: '.$value + ); + return '@'.$value; } } ?> \ No newline at end of file diff --git a/main/Utils/UrlParamsUtils.class.php b/main/Utils/UrlParamsUtils.class.php new file mode 100644 index 0000000000..d95288c178 --- /dev/null +++ b/main/Utils/UrlParamsUtils.class.php @@ -0,0 +1,79 @@ + $value) { + if (is_array($value)) { + foreach ($value as $valueKey => $simpleValue) { + $result[] = + $key.'['.$valueKey.']='.urlencode($simpleValue); + } + } else { + $result[] = $key.'='.urlencode($value); + } + } + + return implode('&', $result); + } + + public static function toString($array) + { + $sum = function ($left, $right) {return $left.'='.urlencode($right);}; + $params = self::toParamsList($array, true); + return implode('&', + array_map($sum, array_keys($params), $params) + ); + } + + public static function toParamsList($array, $encodeKey = false) + { + $result = array(); + + self::argumentsToParams($array, $result, '', $encodeKey); + + return $result; + } + + private static function argumentsToParams( + $array, + &$result, + $keyPrefix, + $encodeKey = false + ) { + foreach ($array as $key => $value) { + $filteredKey = $encodeKey ? urlencode($key) : $key; + $fullKey = $keyPrefix + ? ($keyPrefix.'['.$filteredKey.']') + : $filteredKey; + + if (is_array($value)) { + self::argumentsToParams($value, $result, $fullKey, $encodeKey); + } else { + $result[$fullKey] = $value; + } + } + } + } +?> \ No newline at end of file diff --git a/test/config.inc.php.tpl b/test/config.inc.php.tpl index 36184d8f65..bfea8866f0 100644 --- a/test/config.inc.php.tpl +++ b/test/config.inc.php.tpl @@ -54,4 +54,5 @@ VoodooDaoWorker::setDefaultHandler('CacheSegmentHandler'); define('__LOCAL_DEBUG__', true); + define('ONPHP_CURL_TEST_URL', 'http://localhost/curlTest.php'); //set here url to test script test/main/data/curlTest/curlTest.php ?> diff --git a/test/main/Net/CurlHttpClientTest.class.php b/test/main/Net/CurlHttpClientTest.class.php new file mode 100644 index 0000000000..faaae0b0b6 --- /dev/null +++ b/test/main/Net/CurlHttpClientTest.class.php @@ -0,0 +1,194 @@ +fail (self::$failTestMsg); + + $this->assertEquals( + $this->generateString(array(), array(), array(), ''), + self::$emptyMsg, + 'wrong server empty response' + ); + } + + public function testGetWithAdditionalGet() + { + $get = array( + 'a' => array('b@&=' => array('c' => '@d@[]')), + 'e' => array('f' => array('&1&', '3', '5')), + ); + + $request = $this->spawnRequest(HttpMethod::get(), 'urlGet=really')-> + setGet($get)-> + setPost(array('post' => 'value')); + + $response = $this->spawnClient()->send($request); + + $this->assertEquals( + $this->generateString(array('urlGet' => 'really') + $get, array(), array(), ''), + $response->getBody() + ); + } + + public function testPostAndFilesWithMultiCurl() + { + $get = array( + 'get' => 'value', + ); + $post1 = array( + 'c' => array( + 'd&=@' => '@', + 'e' => array( + 'f' => array('1' => '2'), + 'g' => array('4' => '3'), + ), + 'k' => $this->getFile1Path(), + ) + ); + $post2 = array('post' => 'value'); + $files = array( + 'file1' => $this->getFile1Path(), + 'file2' => $this->getFile2Path(), + ); + $body = file_get_contents($this->getFile1Path()); + + $request1 = $this->spawnRequest(HttpMethod::post(), 'urlGet=super')-> + setGet($get)-> + setPost($post1); + $request2 = $this->spawnRequest(HttpMethod::post())-> + setPost($post2)-> + setFiles($files); + $request3 = $this->spawnRequest(HttpMethod::post())-> + setBody($body); + + $client = $this->spawnClient()-> + addRequest($request1)-> + addRequest($request2)-> + addRequest($request3); + $client->multiSend(); + + //check response 1st request + $this->assertEquals( + $this->generateString(array('urlGet' => 'super') + $get, $post1, array(), UrlParamsUtils::toString($post1)), + $client->getResponse($request1)->getBody() + ); + + //check response 2nd request + $filesExpectation = array( + 'file1' => file_get_contents($this->getFile1Path()), + 'file2' => file_get_contents($this->getFile2Path()), + ); + $this->assertEquals( + $this->generateString(array(), $post2, $filesExpectation, ''), + $client->getResponse($request2)->getBody() + ); + + //check response 3rd request + $this->assertEquals( + $this->generateString(array(), array(), array(), $body), + $client->getResponse($request3)->getBody() + ); + } + + public function testSecurityExceptionWithSendingFileAndAtInPost() + { + $post = array( + 'a' => array( + array('b' => '@foobar') + ) + ); + + $files = array('file' => $this->getFile1Path()); + + $request = $this->spawnRequest(HttpMethod::post())-> + setPost($post)-> + setFiles($files); + + try { + $this->spawnClient()->send($request); + $this->fail('expected NetworkException about security'); + } catch (NetworkException $e) { + $this->assertStringStartsWith('Security excepion:', $e->getMessage()); + } + } + + public function testSendingNotExistsFile() + { + $files = array('file' => $this->getFileNotExists()); + + $request = $this->spawnRequest(HttpMethod::post())-> + setFiles($files); + + try { + $this->spawnClient()->send($request); + $this->fail('expected exception about not exists file'); + } catch (WrongArgumentException $e) { + $this->assertStringStartsWith('couldn\'t access to file with path:', $e->getMessage()); + } + } + + /** + * @param HttpMethod $method + * @return HttpRequest + */ + private function spawnRequest(HttpMethod $method, $urlPostfix = '') + { + $url = HttpUrl::create()->parse(ONPHP_CURL_TEST_URL); + $glue = $url->getQuery() ? '&' : '?'; + + return HttpRequest::create()-> + setUrl($url->parse(ONPHP_CURL_TEST_URL.$glue.$urlPostfix))-> + setMethod($method); + } + + /** + * @return CurlHttpClient + */ + private function spawnClient() + { + return CurlHttpClient::create()-> + setOldUrlConstructor(false)-> + setTimeout(5); + } + + private function generateString($get, $post, $files, $inputString) + { + ob_start(); + var_dump($get, $post, $files, $inputString); + return ob_get_clean(); + } + + private function getFile1Path() + { + return $this->getFileDirPath().'contents'; + } + + private function getFile2Path() + { + return $this->getFileDirPath().'contents'; + } + + private function getFileNotExists() + { + return $this->getFileDirPath().'notexists'; + } + + private function getFileDirPath() + { + return ONPHP_TEST_PATH.'main/data/directory/'; + } + } +?> \ No newline at end of file diff --git a/test/main/Utils/UrlParamsUtilsTest.class.php b/test/main/Utils/UrlParamsUtilsTest.class.php new file mode 100644 index 0000000000..6b26231d62 --- /dev/null +++ b/test/main/Utils/UrlParamsUtilsTest.class.php @@ -0,0 +1,57 @@ + '1', + 'c' => '@3', + 'g' => array('1' => '1', '0' => '[0]'), + ); + + $this->assertEquals( + 'a=1&c=%403&g[1]=1&g[0]=%5B0%5D', + UrlParamsUtils::toStringOneDeepLvl($scope) + ); + + $scope['z'] = array('2' => array('8' => '8')); + + try { + UrlParamsUtils::toStringOneDeepLvl($scope); + $this->fail('expected exception'); + } catch (BaseException $e) { + $this->assertEquals( + 'urlencode() expects parameter 1 to be string, array given', + $e->getMessage() + ); + } + } + + public function testAnyDeepLvl() + { + $scope = array( + 'foo' => array('foo' => array('foo' => '@bar')), + 'bar' => array('@bar' => array('bar' => "foo[]я")), + 'fo' => array( + array('o', 'ba', 'r'), + ) + ); + + $this->assertEquals( + array( + 'foo[foo][foo]' => '@bar', + 'bar[@bar][bar]' => 'foo[]я', + 'fo[0][0]' => 'o', + 'fo[0][1]' => 'ba', + 'fo[0][2]' => 'r', + ), + UrlParamsUtils::toParamsList($scope) + ); + + $this->assertEquals( + 'foo[foo][foo]=%40bar&bar[%40bar][bar]=foo%5B%5D%D1%8F&fo[0][0]=o&fo[0][1]=ba&fo[0][2]=r', + UrlParamsUtils::toString($scope) + ); + } + } +?> \ No newline at end of file diff --git a/test/main/data/curlTest/curlTest.php b/test/main/data/curlTest/curlTest.php new file mode 100644 index 0000000000..8137edf6fb --- /dev/null +++ b/test/main/data/curlTest/curlTest.php @@ -0,0 +1,12 @@ + $value) { + if (isset($value['tmp_name'])) + $files[$fileName] = file_get_contents($value['tmp_name']); + } + var_dump($_GET, $_POST, $files, file_get_contents('php://input')); +} catch (Exception $e) { + var_dump(get_class($e), $e->getMessage(), $e->getCode(), $e->getFile(), $e->getLine(), $e->getTraceAsString()); +} \ No newline at end of file