├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── run-tests.sh ├── src ├── Laravel │ └── cURL.php ├── Request.php ├── Response.php ├── cURL.php └── cURLException.php └── tests ├── functional └── cURLTest.php ├── server ├── digest-auth.php ├── echo.php ├── failure.php ├── success.php └── upload.php └── unit ├── RequestTest.php └── ResponseTest.php /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | 'on': 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | tags: 9 | - '**' 10 | pull_request: 11 | branches: 12 | - '**' 13 | schedule: 14 | - cron: '0 8 1 * *' 15 | 16 | jobs: 17 | phpunit: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | php-version: 22 | - '5.6' 23 | - '7.0' 24 | - '7.1' 25 | - '7.2' 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.php-version }} 31 | tools: composer 32 | - run: composer install --dev 33 | - run: ./run-tests.sh -v 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2015 Andreas Lutro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-curl 2 | 3 | [![Build Status](https://travis-ci.org/anlutro/php-curl.png?branch=master)](https://travis-ci.org/anlutro/php-curl) 4 | [![Latest Stable Version](https://poser.pugx.org/anlutro/curl/v/stable.svg)](https://github.com/anlutro/php-curl/releases) 5 | [![Latest Unstable Version](https://poser.pugx.org/anlutro/curl/v/unstable.svg)](https://github.com/anlutro/php-curl/branches/active) 6 | [![License](https://poser.pugx.org/anlutro/curl/license.svg)](http://opensource.org/licenses/MIT) 7 | 8 | 9 | The smallest possible OOP wrapper for PHP's curl capabilities. 10 | 11 | Note that this is **not** meant as a high-level abstraction. You should still know how "pure PHP" curl works, you need to know the curl options to set, and you need to know some HTTP basics. 12 | 13 | If you're looking for a more user-friendly abstraction, check out [Guzzle](http://guzzle.readthedocs.org/en/latest/). 14 | 15 | ## Installation 16 | 17 | $ composer require anlutro/curl 18 | 19 | ## Usage 20 | 21 | ```php 22 | $curl = new anlutro\cURL\cURL; 23 | 24 | $response = $curl->get('http://www.google.com'); 25 | 26 | // easily build an url with a query string 27 | $url = $curl->buildUrl('http://www.google.com', ['s' => 'curl']); 28 | $response = $curl->get($url); 29 | 30 | // the post, put and patch methods takes an array of POST data 31 | $response = $curl->post($url, ['api_key' => 'my_key', 'post' => 'data']); 32 | 33 | // add "json" to the start of the method to convert the data to a JSON string 34 | // and send the header "Content-Type: application/json" 35 | $response = $curl->jsonPost($url, ['post' => 'data']); 36 | 37 | // if you don't want any conversion to be done to the provided data, for example 38 | // if you want to post an XML string, add "raw" to the start of the method 39 | $response = $curl->rawPost($url, 'rawPost($url, ['file' => $file]); 44 | 45 | // a response object is returned 46 | var_dump($response->statusCode); // response status code integer (for example, 200) 47 | var_dump($response->statusText); // full response status (for example, '200 OK') 48 | echo $response->body; 49 | var_dump($response->headers); // array of headers 50 | var_dump($response->info); // array of curl info 51 | ``` 52 | 53 | If you need to send headers or set [cURL options](http://php.net/manual/en/function.curl-setopt.php) you can manipulate a request object directly. `send()` finalizes the request and returns the result. 54 | 55 | ```php 56 | // newRequest, newJsonRequest and newRawRequest returns a Request object 57 | $request = $curl->newRequest('post', $url, ['foo' => 'bar']) 58 | ->setHeader('Accept-Charset', 'utf-8') 59 | ->setHeader('Accept-Language', 'en-US') 60 | ->setOption(CURLOPT_CAINFO, '/path/to/cert') 61 | ->setOption(CURLOPT_FOLLOWLOCATION, true); 62 | $response = $request->send(); 63 | ``` 64 | 65 | You can also use `setHeaders(array $headers)` and `setOptions(array $options)` to replace all the existing options. 66 | 67 | Note that some curl options are reset when you call `send()`. Look at the source code of the method `cURL::prepareMethod` for a full overview of which options are overwritten. 68 | 69 | HTTP basic authentication: 70 | 71 | ```php 72 | $request = $curl->newRequest('post', $url, ['foo' => 'bar']) 73 | ->setUser($username)->setPass($password); 74 | ``` 75 | 76 | ### Laravel 77 | 78 | The package comes with a facade you can use if you prefer the static method calls over dependency injection. 79 | 80 | You do **not** need to add a service provider. 81 | 82 | Optionally, add `'cURL' => 'anlutro\cURL\Laravel\cURL'` to the array of aliases in `config/app.php`. 83 | 84 | Replace `$curl->` with `cURL::` in the examples above. 85 | 86 | ## Contact 87 | 88 | Open an issue on GitHub if you have any problems or suggestions. 89 | 90 | ## License 91 | 92 | The contents of this repository is released under the [MIT license](http://opensource.org/licenses/MIT). 93 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anlutro/curl", 3 | "description": "Simple OOP cURL wrapper.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Andreas Lutro", 8 | "email": "anlutro@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.3.0" 13 | }, 14 | "require-dev": { 15 | "mockery/mockery": "0.9.*", 16 | "phpunit/phpunit": "4.*" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "anlutro\\cURL\\": "src/" 21 | } 22 | }, 23 | "config": { 24 | "preferred-install": "dist" 25 | }, 26 | "minimum-stability": "dev", 27 | "prefer-stable": true 28 | } 29 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/unit 16 | 17 | 18 | ./tests/functional 19 | 20 | 21 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$TRAVIS_PHP_VERSION" = "" ]; then 4 | php_version=$(php --version | grep -oh 'PHP ([0-9]\.[0-9])' | sed 's/PHP //') 5 | else 6 | php_version=$TRAVIS_PHP_VERSION 7 | fi 8 | 9 | if [ "$php_version" != "5.3" ] && [ "$php_version" != "hhvm" ]; then 10 | php -S localhost:8080 -t tests/server > /dev/null 2>&1 & 11 | php_pid=$! 12 | export CURL_TEST_SERVER_RUNNING=1 13 | fi 14 | 15 | if [ -e vendor/bin/phpunit ]; then 16 | phpunit=vendor/bin/phpunit 17 | else 18 | phpunit=phpunit 19 | fi 20 | 21 | $phpunit $@ 22 | ret=$? 23 | 24 | if [ $CURL_TEST_SERVER_RUNNING ]; then 25 | kill $php_pid 26 | fi 27 | 28 | exit $ret 29 | -------------------------------------------------------------------------------- /src/Laravel/cURL.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package PHP cURL 8 | */ 9 | 10 | namespace anlutro\cURL\Laravel; 11 | 12 | use Illuminate\Support\Facades\Facade; 13 | 14 | /** 15 | * cURL facade class. 16 | */ 17 | class cURL extends Facade 18 | { 19 | public static function getFacadeAccessor() 20 | { 21 | return 'anlutro\cURL\cURL'; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package PHP cURL 8 | */ 9 | 10 | namespace anlutro\cURL; 11 | 12 | /** 13 | * cURL request representation class. 14 | */ 15 | class Request 16 | { 17 | /** 18 | * ENCODING_* constants, used for specifying encoding options 19 | */ 20 | const ENCODING_QUERY = 0; 21 | const ENCODING_JSON = 1; 22 | const ENCODING_RAW = 2; 23 | 24 | /** 25 | * Allowed methods => allows postdata 26 | * 27 | * @var array 28 | */ 29 | public static $methods = array( 30 | 'get' => false, 31 | 'head' => false, 32 | 'post' => true, 33 | 'put' => true, 34 | 'patch' => true, 35 | 'delete' => true, 36 | 'options' => false, 37 | ); 38 | 39 | /** 40 | * cURL wrapper instance 41 | * 42 | * @var cURL 43 | */ 44 | private $curl; 45 | 46 | /** 47 | * The HTTP method to use. Defaults to GET. 48 | * 49 | * @var string 50 | */ 51 | private $method = 'get'; 52 | 53 | /** 54 | * The URL the request is sent to. 55 | * 56 | * @var string 57 | */ 58 | private $url = ''; 59 | 60 | /** 61 | * The headers sent with the request. 62 | * 63 | * @var array 64 | */ 65 | private $headers = array(); 66 | 67 | /** 68 | * The cookies sent with the request. 69 | * 70 | * @var array 71 | */ 72 | private $cookies = array(); 73 | 74 | /** 75 | * POST data sent with the request. 76 | * 77 | * @var mixed 78 | */ 79 | private $data = array(); 80 | 81 | /** 82 | * Optional cURL options. 83 | * 84 | * @var array 85 | */ 86 | private $options = array(); 87 | 88 | /** 89 | * Username to authenticate the request of cURL. 90 | * 91 | * @var array 92 | */ 93 | private $user = ''; 94 | 95 | /** 96 | * Password to authenticate the request of cURL. 97 | * 98 | * @var array 99 | */ 100 | private $pass = ''; 101 | 102 | /** 103 | * The type of processing to perform to encode the POST data 104 | * 105 | * @var int 106 | */ 107 | private $encoding = Request::ENCODING_QUERY; 108 | 109 | /** 110 | * @param cURL $curl 111 | */ 112 | public function __construct(cURL $curl) 113 | { 114 | $this->curl = $curl; 115 | } 116 | 117 | /** 118 | * Set the HTTP method of the request. 119 | * 120 | * @param string $method 121 | */ 122 | public function setMethod($method) 123 | { 124 | $method = strtolower($method); 125 | 126 | if (!array_key_exists($method, static::$methods)) { 127 | throw new \InvalidArgumentException("Method [$method] not a valid HTTP method."); 128 | } 129 | 130 | if ($this->data && !static::$methods[$method]) { 131 | throw new \LogicException('Request has POST data, but tried changing HTTP method to one that does not allow POST data'); 132 | } 133 | 134 | $this->method = $method; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * Get the HTTP method of the request. 141 | * 142 | * @return string 143 | */ 144 | public function getMethod() 145 | { 146 | return $this->method; 147 | } 148 | 149 | /** 150 | * Set the URL of the request. 151 | * 152 | * @param string $url 153 | */ 154 | public function setUrl($url) 155 | { 156 | $this->url = $url; 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * Get the URL of the request. 163 | * 164 | * @return string 165 | */ 166 | public function getUrl() 167 | { 168 | return $this->url; 169 | } 170 | 171 | /** 172 | * Set a specific header to be sent with the request. 173 | * 174 | * @param string $key Can also be a string in "foo: bar" format 175 | * @param mixed $value 176 | * @param boolean $preserveCase 177 | */ 178 | public function setHeader($key, $value = null, $preserveCase = false) 179 | { 180 | if ($value === null) { 181 | list($key, $value) = explode(':', $value, 2); 182 | } 183 | 184 | if (!$preserveCase) { 185 | $key = strtolower($key); 186 | } 187 | 188 | $key = trim($key); 189 | $this->headers[$key] = trim($value); 190 | 191 | return $this; 192 | } 193 | 194 | /** 195 | * Set the headers to be sent with the request. 196 | * 197 | * Pass an associative array - e.g. ['Content-Type' => 'application/json'] 198 | * and the correct header formatting - e.g. 'Content-Type: application/json' 199 | * will be done for you when the request is sent. 200 | * 201 | * @param array $headers 202 | */ 203 | public function setHeaders(array $headers) 204 | { 205 | $this->headers = array(); 206 | 207 | foreach ($headers as $key => $value) { 208 | $this->setHeader($key, $value); 209 | } 210 | 211 | return $this; 212 | } 213 | 214 | /** 215 | * Get a specific header from the request. 216 | * 217 | * @param string $key 218 | * 219 | * @return mixed 220 | */ 221 | public function getHeader($key) 222 | { 223 | $key = strtolower($key); 224 | 225 | return isset($this->headers[$key]) ? $this->headers[$key] : null; 226 | } 227 | 228 | /** 229 | * Get the headers to be sent with the request. 230 | * 231 | * @return array 232 | */ 233 | public function getHeaders() 234 | { 235 | return $this->headers; 236 | } 237 | 238 | /** 239 | * Set a cookie. 240 | * 241 | * @param string $key 242 | * @param string $value 243 | */ 244 | public function setCookie($key, $value) 245 | { 246 | $this->cookies[$key] = $value; 247 | $this->updateCookieHeader(); 248 | 249 | return $this; 250 | } 251 | 252 | /** 253 | * Replace the request's cookies. 254 | * 255 | * @param array $cookies 256 | */ 257 | public function setCookies(array $cookies) 258 | { 259 | $this->cookies = $cookies; 260 | $this->updateCookieHeader(); 261 | 262 | return $this; 263 | } 264 | 265 | /** 266 | * Read the request cookies and set the cookie header. 267 | * 268 | * @return void 269 | */ 270 | private function updateCookieHeader() 271 | { 272 | $strings = array(); 273 | 274 | foreach ($this->cookies as $key => $value) { 275 | $strings[] = "{$key}={$value}"; 276 | } 277 | 278 | $this->setHeader('cookie', implode('; ', $strings)); 279 | } 280 | 281 | /** 282 | * Get a specific cookie from the request. 283 | * 284 | * @param string $key 285 | * 286 | * @return string|null 287 | */ 288 | public function getCookie($key) 289 | { 290 | return isset($this->cookies[$key]) ? $this->cookies[$key] : null; 291 | } 292 | 293 | /** 294 | * Get all the request's cookies. 295 | * 296 | * @return string[] 297 | */ 298 | public function getCookies() 299 | { 300 | return $this->cookies; 301 | } 302 | 303 | /** 304 | * Format the headers to an array of 'key: val' which can be passed to 305 | * curl_setopt. 306 | * 307 | * @return array 308 | */ 309 | public function formatHeaders() 310 | { 311 | $headers = array(); 312 | 313 | foreach ($this->headers as $key => $val) { 314 | if (is_string($key)) { 315 | $headers[] = $key . ': ' . $val; 316 | } else { 317 | $headers[] = $val; 318 | } 319 | } 320 | 321 | return $headers; 322 | } 323 | 324 | /** 325 | * Set the POST data to be sent with the request. 326 | * 327 | * @param mixed $data 328 | */ 329 | public function setData($data) 330 | { 331 | if ($data && !static::$methods[$this->method]) { 332 | throw new \InvalidArgumentException("HTTP method [$this->method] does not allow POST data."); 333 | } 334 | 335 | $this->data = $data; 336 | 337 | return $this; 338 | } 339 | 340 | /** 341 | * Check whether the request has any data. 342 | * 343 | * @return boolean 344 | */ 345 | public function hasData() 346 | { 347 | return static::$methods[$this->method] && (bool) $this->encodeData(); 348 | } 349 | 350 | /** 351 | * Get the POST data to be sent with the request. 352 | * 353 | * @return mixed 354 | */ 355 | public function getData() 356 | { 357 | return $this->data; 358 | } 359 | 360 | /** 361 | * Set the encoding to use on the POST data, and (possibly) associated Content-Type headers 362 | * 363 | * @param int $encoding a Request::ENCODING_* constant 364 | */ 365 | public function setEncoding($encoding) 366 | { 367 | $encoding = intval($encoding); 368 | 369 | if ( 370 | $encoding !== static::ENCODING_QUERY && 371 | $encoding !== static::ENCODING_JSON && 372 | $encoding !== static::ENCODING_RAW 373 | ) { 374 | throw new \InvalidArgumentException("Encoding [$encoding] not a known Request::ENCODING_* constant"); 375 | } 376 | 377 | if ($encoding === static::ENCODING_JSON && !$this->getHeader('Content-Type')) { 378 | $this->setHeader('Content-Type', 'application/json'); 379 | } 380 | 381 | $this->encoding = $encoding; 382 | 383 | return $this; 384 | } 385 | 386 | /** 387 | * Get the current encoding which will be used on the POST data 388 | * 389 | * @return int a Request::ENCODING_* constant 390 | */ 391 | public function getEncoding() 392 | { 393 | return $this->encoding; 394 | } 395 | 396 | /** 397 | * Encode the POST data as a string. 398 | * 399 | * @return string 400 | */ 401 | public function encodeData() 402 | { 403 | switch ($this->encoding) { 404 | case static::ENCODING_JSON: 405 | return json_encode($this->data); 406 | case static::ENCODING_QUERY: 407 | return (!is_null($this->data) ? http_build_query($this->data) : ''); 408 | case static::ENCODING_RAW: 409 | return $this->data; 410 | default: 411 | $msg = "Encoding [$this->encoding] not a known Request::ENCODING_* constant"; 412 | throw new \UnexpectedValueException($msg); 413 | } 414 | } 415 | 416 | /** 417 | * Set a specific curl option for the request. 418 | * 419 | * @param int $key 420 | * @param mixed $value 421 | */ 422 | public function setOption($key, $value) 423 | { 424 | $this->options[$key] = $value; 425 | 426 | return $this; 427 | } 428 | 429 | /** 430 | * Set the cURL options for the request. 431 | * 432 | * @param array $options 433 | */ 434 | public function setOptions(array $options) 435 | { 436 | $this->options = $options; 437 | 438 | return $this; 439 | } 440 | 441 | /** 442 | * Get a specific curl option from the request. 443 | * 444 | * @param int $key 445 | * 446 | * @return mixed 447 | */ 448 | public function getOption($key) 449 | { 450 | return isset($this->options[$key]) ? $this->options[$key] : null; 451 | } 452 | 453 | /** 454 | * Get the cURL options for the request. 455 | * 456 | * @return array 457 | */ 458 | public function getOptions() 459 | { 460 | return $this->options; 461 | } 462 | 463 | /** 464 | * Set the HTTP basic username and password. 465 | * 466 | * @param string $user 467 | * @param string $pass 468 | * 469 | * @return string 470 | */ 471 | public function auth($user, $pass) 472 | { 473 | $this->user = $user; 474 | $this->pass = $pass; 475 | 476 | return $this; 477 | } 478 | 479 | /** 480 | * Set an username to authenticate the request of curl. 481 | * 482 | * @param string $user 483 | * 484 | * @return static 485 | */ 486 | public function setUser($user) 487 | { 488 | $this->user = $user; 489 | 490 | return $this; 491 | } 492 | 493 | /** 494 | * Set a password to authenticate the request of curl. 495 | * 496 | * @param string $pass 497 | * 498 | * @return static 499 | */ 500 | public function setPass($pass) 501 | { 502 | $this->pass = $pass; 503 | 504 | return $this; 505 | } 506 | 507 | /** 508 | * If username and password is set, returns a string of 'username:password'. 509 | * If not, returns null. 510 | * 511 | * @return string|null 512 | */ 513 | public function getUserAndPass() 514 | { 515 | if ($this->user) { 516 | return $this->user . ':' . $this->pass; 517 | } 518 | 519 | return null; 520 | } 521 | 522 | /** 523 | * Whether the request is JSON or not. 524 | * 525 | * @return boolean 526 | */ 527 | public function isJson() 528 | { 529 | return $this->encoding === static::ENCODING_JSON; 530 | } 531 | 532 | /** 533 | * Send the request. 534 | * 535 | * @return Response 536 | */ 537 | public function send() 538 | { 539 | return $this->curl->sendRequest($this); 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /src/Response.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package PHP cURL 8 | */ 9 | 10 | namespace anlutro\cURL; 11 | 12 | /** 13 | * cURL response representation class. 14 | */ 15 | class Response 16 | { 17 | /** 18 | * The response headers. 19 | * 20 | * @var array 21 | */ 22 | public $headers = array(); 23 | 24 | /** 25 | * The response body. 26 | * 27 | * @var string|null 28 | */ 29 | public $body; 30 | 31 | /** 32 | * The results of curl_getinfo on the response request. 33 | * 34 | * @var array|false 35 | */ 36 | public $info; 37 | 38 | /** 39 | * The response code including text, e.g. '200 OK'. 40 | * 41 | * @var string 42 | */ 43 | public $statusText; 44 | 45 | /** 46 | * The response code. 47 | * 48 | * @var int 49 | */ 50 | public $statusCode; 51 | 52 | /** 53 | * @param string|null $body 54 | * @param string $headers 55 | * @param mixed $info 56 | */ 57 | public function __construct($body, $headers, $info = array()) 58 | { 59 | $this->body = $body; 60 | $this->info = $info; 61 | $this->parseHeader($headers); 62 | } 63 | 64 | /** 65 | * Parse a header string. 66 | * 67 | * @param string $header 68 | * 69 | * @return void 70 | */ 71 | protected function parseHeader($header) 72 | { 73 | if ($header === "") { 74 | throw new \UnexpectedValueException('Empty header string passed!'); 75 | } 76 | $headers = explode("\r\n", trim($header)); 77 | $this->parseHeaders($headers); 78 | } 79 | 80 | /** 81 | * Parse an array of headers. 82 | * 83 | * @param array $headers 84 | * 85 | * @return void 86 | */ 87 | protected function parseHeaders(array $headers) 88 | { 89 | if (count($headers) === 0) { 90 | throw new \UnexpectedValueException('No headers passed!'); 91 | } 92 | 93 | $this->headers = array(); 94 | 95 | // find and set the HTTP status code and reason 96 | $firstHeader = array_shift($headers); 97 | if (!preg_match('/^HTTP\/\d(\.\d)? [0-9]{3}/', $firstHeader)) { 98 | throw new \UnexpectedValueException('Invalid response header'); 99 | } 100 | list(, $status) = explode(' ', $firstHeader, 2); 101 | $code = explode(' ', $status); 102 | $code = (int) $code[0]; 103 | 104 | // special handling for HTTP 100 responses 105 | if ($code === 100) { 106 | // remove empty header lines between 100 and actual HTTP status 107 | foreach ($headers as $idx => $header) { 108 | if ($header) { 109 | break; 110 | } 111 | } 112 | 113 | // start the process over with the 100 continue header stripped away 114 | return $this->parseHeaders(array_slice($headers, $idx)); 115 | } 116 | 117 | // handle cases where CURLOPT_HTTPAUTH is being used, in which case 118 | // curl_exec may cause two HTTP responses 119 | if ( 120 | array_key_exists(CURLINFO_HTTPAUTH_AVAIL, $this->info) && 121 | $this->info[CURLINFO_HTTPAUTH_AVAIL] > 0 && 122 | $code === 401 123 | ) { 124 | $foundAuthenticateHeader = false; 125 | $foundSecondHttpResponse = false; 126 | foreach ($headers as $idx => $header) { 127 | if ($foundAuthenticateHeader === false && strpos(strtolower($header), 'www-authenticate:') === 0) { 128 | $foundAuthenticateHeader = true; 129 | } 130 | if ($foundAuthenticateHeader && preg_match('/^HTTP\/\d(\.\d)? [0-9]{3}/', $header)) { 131 | $foundSecondHttpResponse = true; 132 | break; 133 | } 134 | } 135 | 136 | // discard the original response. 137 | if ($foundAuthenticateHeader && $foundSecondHttpResponse) { 138 | $headers = array_slice($headers, $idx); 139 | return $this->parseHeaders($headers); 140 | } 141 | } 142 | 143 | $this->statusText = $status; 144 | $this->statusCode = $code; 145 | 146 | foreach ($headers as $header) { 147 | // skip empty lines 148 | if (!$header) { 149 | continue; 150 | } 151 | 152 | $delimiter = strpos($header, ':'); 153 | if (!$delimiter) { 154 | continue; 155 | } 156 | 157 | $key = trim(strtolower(substr($header, 0, $delimiter))); 158 | $val = ltrim(substr($header, $delimiter + 1)); 159 | 160 | if (isset($this->headers[$key])) { 161 | if (is_array($this->headers[$key])) { 162 | $this->headers[$key][] = $val; 163 | } else { 164 | $this->headers[$key] = array($this->headers[$key], $val); 165 | } 166 | } else { 167 | $this->headers[$key] = $val; 168 | } 169 | } 170 | } 171 | 172 | /** 173 | * Get a specific header from the response. 174 | * 175 | * @param string $key 176 | * 177 | * @return mixed 178 | */ 179 | public function getHeader($key) 180 | { 181 | $key = strtolower($key); 182 | 183 | return array_key_exists($key, $this->headers) ? 184 | $this->headers[$key] : null; 185 | } 186 | 187 | /** 188 | * Gets all the headers of the response. 189 | * 190 | * @return array 191 | */ 192 | public function getHeaders() 193 | { 194 | return $this->headers; 195 | } 196 | 197 | /** 198 | * Get the response body. 199 | * 200 | * @return string 201 | */ 202 | public function getBody() 203 | { 204 | // usually because CURLOPT_FILE is set 205 | if ($this->body === null) { 206 | throw new \UnexpectedValueException("Response has no body!"); 207 | } 208 | 209 | return $this->body; 210 | } 211 | 212 | /** 213 | * Convert the response instance to an array. 214 | * 215 | * @return array 216 | */ 217 | public function toArray() 218 | { 219 | return array( 220 | 'headers' => $this->headers, 221 | 'body' => $this->body, 222 | 'info' => $this->info 223 | ); 224 | } 225 | 226 | /** 227 | * Convert the response object to a JSON string. 228 | * 229 | * @return string 230 | */ 231 | public function toJson() 232 | { 233 | return json_encode($this->toArray()); 234 | } 235 | 236 | /** 237 | * Convert the object to its string representation by returning the body. 238 | * 239 | * @return string 240 | */ 241 | public function __toString() 242 | { 243 | return (string) $this->getBody(); 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/cURL.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package PHP cURL 8 | */ 9 | 10 | namespace anlutro\cURL; 11 | 12 | /** 13 | * cURL wrapper class. 14 | * 15 | * @method Response get(string $url) Execute a GET request 16 | * @method Response delete(string $url) Execute a DELETE request 17 | * @method Response head(string $url) Execute a HEAD request 18 | * @method Response post(string $url, mixed $data) Execute a POST request 19 | * @method Response put(string $url, mixed $data) Execute a PUT request 20 | * @method Response patch(string $url, mixed $data) Execute a PATCH request 21 | * @method Response jsonGet(string $url) Execute a JSON GET request 22 | * @method Response jsonDelete(string $url) Execute a JSON DELETE request 23 | * @method Response jsonHead(string $url) Execute a JSON HEAD request 24 | * @method Response jsonPost(string $url, mixed $data) Execute a JSON POST request 25 | * @method Response jsonPut(string $url, mixed $data) Execute a JSON PUT request 26 | * @method Response jsonPatch(string $url, mixed $data) Execute a JSON PATCH request 27 | * @method Response rawGet(string $url) Execute a raw GET request 28 | * @method Response rawDelete(string $url) Execute a raw DELETE request 29 | * @method Response rawHead(string $url) Execute a raw HEAD request 30 | * @method Response rawPost(string $url, mixed $data) Execute a raw POST request 31 | * @method Response rawPut(string $url, mixed $data) Execute a raw PUT request 32 | * @method Response rawPatch(string $url, mixed $data) Execute a raw PATCH request 33 | */ 34 | class cURL 35 | { 36 | /** 37 | * The cURL resource. 38 | */ 39 | protected $ch; 40 | 41 | /** 42 | * The request class to use. 43 | * 44 | * @var string 45 | */ 46 | protected $requestClass = 'anlutro\cURL\Request'; 47 | 48 | /** 49 | * The response class to use. 50 | * 51 | * @var string 52 | */ 53 | protected $responseClass = 'anlutro\cURL\Response'; 54 | 55 | /** 56 | * The default headers. 57 | * 58 | * @var array 59 | */ 60 | protected $defaultHeaders = array(); 61 | 62 | /** 63 | * The default curl options. 64 | * 65 | * @var array 66 | */ 67 | protected $defaultOptions = array(); 68 | 69 | /** 70 | * Get allowed methods. 71 | * 72 | * @return array 73 | */ 74 | public function getAllowedMethods() 75 | { 76 | return Request::$methods; 77 | } 78 | 79 | /** 80 | * Set the request class. 81 | * 82 | * @param string $class 83 | */ 84 | public function setRequestClass($class) 85 | { 86 | $this->requestClass = $class; 87 | } 88 | 89 | /** 90 | * Set the response class. 91 | * 92 | * @param string $class 93 | */ 94 | public function setResponseClass($class) 95 | { 96 | $this->responseClass = $class; 97 | } 98 | 99 | /** 100 | * Set the default headers for every request. 101 | * 102 | * @param array $headers 103 | */ 104 | public function setDefaultHeaders(array $headers) 105 | { 106 | $this->defaultHeaders = $headers; 107 | } 108 | 109 | /** 110 | * Get the default headers. 111 | * 112 | * @return array 113 | */ 114 | public function getDefaultHeaders() 115 | { 116 | return $this->defaultHeaders; 117 | } 118 | 119 | /** 120 | * Set the default curl options for every request. 121 | * 122 | * @param array $options 123 | */ 124 | public function setDefaultOptions(array $options) 125 | { 126 | $this->defaultOptions = $options; 127 | } 128 | 129 | /** 130 | * Get the default options. 131 | * 132 | * @return array 133 | */ 134 | public function getDefaultOptions() 135 | { 136 | return $this->defaultOptions; 137 | } 138 | 139 | /** 140 | * Build an URL with an optional query string. 141 | * 142 | * @param string $url the base URL without any query string 143 | * @param array $query array of GET parameters 144 | * 145 | * @return string 146 | */ 147 | public function buildUrl($url, array $query) 148 | { 149 | if (empty($query)) { 150 | return $url; 151 | } 152 | 153 | $parts = parse_url($url); 154 | 155 | $queryString = ''; 156 | if (isset($parts['query']) && $parts['query']) { 157 | $queryString .= $parts['query'].'&'.http_build_query($query); 158 | } else { 159 | $queryString .= http_build_query($query); 160 | } 161 | 162 | $retUrl = $parts['scheme'].'://'.$parts['host']; 163 | if (isset($parts['port'])) { 164 | $retUrl .= ':'.$parts['port']; 165 | } 166 | 167 | if (isset($parts['path'])) { 168 | $retUrl .= $parts['path']; 169 | } 170 | 171 | if ($queryString) { 172 | $retUrl .= '?' . $queryString; 173 | } 174 | 175 | return $retUrl; 176 | } 177 | 178 | /** 179 | * Create a new response object and set its values. 180 | * 181 | * @param string $method get, post, etc 182 | * @param string $url 183 | * @param mixed $data POST data 184 | * @param int $encoding Request::ENCODING_* constant specifying how to process the POST data 185 | * 186 | * @return Request 187 | */ 188 | public function newRequest($method, $url, $data = array(), $encoding = Request::ENCODING_QUERY) 189 | { 190 | $class = $this->requestClass; 191 | $request = new $class($this); 192 | 193 | if ($this->defaultHeaders) { 194 | $request->setHeaders($this->defaultHeaders); 195 | } 196 | if ($this->defaultOptions) { 197 | $request->setOptions($this->defaultOptions); 198 | } 199 | $request->setMethod($method); 200 | $request->setUrl($url); 201 | $request->setData($data); 202 | $request->setEncoding($encoding); 203 | 204 | return $request; 205 | } 206 | 207 | /** 208 | * Create a new JSON request and set its values. 209 | * 210 | * @param string $method get, post etc 211 | * @param string $url 212 | * @param mixed $data POST data 213 | * 214 | * @return Request 215 | */ 216 | public function newJsonRequest($method, $url, $data = array()) 217 | { 218 | return $this->newRequest($method, $url, $data, Request::ENCODING_JSON); 219 | } 220 | 221 | /** 222 | * Create a new raw request and set its values. 223 | * 224 | * @param string $method get, post etc 225 | * @param string $url 226 | * @param mixed $data request body 227 | * 228 | * @return Request 229 | */ 230 | public function newRawRequest($method, $url, $data = '') 231 | { 232 | return $this->newRequest($method, $url, $data, Request::ENCODING_RAW); 233 | } 234 | 235 | /** 236 | * Prepare the curl resource for sending a request. 237 | * 238 | * @param Request $request 239 | * 240 | * @return void 241 | */ 242 | public function prepareRequest(Request $request) 243 | { 244 | $this->ch = curl_init(); 245 | curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); 246 | curl_setopt($this->ch, CURLOPT_HEADER, true); 247 | if ($auth = $request->getUserAndPass()) { 248 | curl_setopt($this->ch, CURLOPT_USERPWD, $auth); 249 | } 250 | curl_setopt($this->ch, CURLOPT_URL, $request->getUrl()); 251 | 252 | $options = $request->getOptions(); 253 | if (!empty($options)) { 254 | curl_setopt_array($this->ch, $options); 255 | } 256 | 257 | $method = $request->getMethod(); 258 | curl_setopt($this->ch, CURLOPT_CUSTOMREQUEST, strtoupper($method)); 259 | 260 | curl_setopt($this->ch, CURLOPT_HTTPHEADER, $request->formatHeaders()); 261 | 262 | if ($request->hasData()) { 263 | curl_setopt($this->ch, CURLOPT_POSTFIELDS, $request->encodeData()); 264 | } 265 | 266 | if ($method === 'head') { 267 | curl_setopt($this->ch, CURLOPT_NOBODY, true); 268 | } 269 | } 270 | 271 | /** 272 | * Send a request. 273 | * 274 | * @param Request $request 275 | * 276 | * @return Response 277 | */ 278 | public function sendRequest(Request $request) 279 | { 280 | $this->prepareRequest($request); 281 | 282 | $result = curl_exec($this->ch); 283 | 284 | if ($result === false) { 285 | $errno = curl_errno($this->ch); 286 | $errmsg = curl_error($this->ch); 287 | $msg = "cURL request failed with error [$errno]: $errmsg"; 288 | curl_close($this->ch); 289 | throw new cURLException($request, $msg, $errno); 290 | } 291 | 292 | $response = $this->createResponseObject($result, $request); 293 | 294 | curl_close($this->ch); 295 | 296 | return $response; 297 | } 298 | 299 | /** 300 | * Extract the response info, header and body from a cURL response. Saves 301 | * the data in variables stored on the object. 302 | * 303 | * @param string $response 304 | * @param Request $request 305 | * 306 | * @return Response 307 | */ 308 | protected function createResponseObject($response, Request $request) 309 | { 310 | $info = curl_getinfo($this->ch); 311 | $headerSize = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); 312 | // needed for the Response class to know that it may have to parse 2 HTTP responses 313 | $info[CURLINFO_HTTPAUTH_AVAIL] = curl_getinfo($this->ch, CURLINFO_HTTPAUTH_AVAIL); 314 | 315 | if ($file = $request->getOption(CURLOPT_FILE)) { 316 | // file may be opened write-only, and even when it isn't, 317 | // seeking/reading seems to be buggy 318 | $fileMeta = stream_get_meta_data($file); 319 | $file = fopen($fileMeta['uri'], 'r'); 320 | $headers = fread($file, $headerSize); 321 | fclose($file); 322 | $body = null; 323 | } else { 324 | $headers = substr($response, 0, $headerSize); 325 | $body = substr($response, $headerSize); 326 | } 327 | 328 | $class = $this->responseClass; 329 | 330 | return new $class($body, $headers, $info); 331 | } 332 | 333 | /** 334 | * Handle dynamic calls to the class. 335 | * 336 | * @param string $func 337 | * @param array $args 338 | * 339 | * @return mixed 340 | */ 341 | public function __call($func, $args) 342 | { 343 | $method = strtolower($func); 344 | 345 | $encoding = Request::ENCODING_QUERY; 346 | 347 | if (substr($method, 0, 4) === 'json') { 348 | $encoding = Request::ENCODING_JSON; 349 | $method = substr($method, 4); 350 | } elseif (substr($method, 0, 3) === 'raw') { 351 | $encoding = Request::ENCODING_RAW; 352 | $method = substr($method, 3); 353 | } 354 | 355 | if (!array_key_exists($method, Request::$methods)) { 356 | throw new \BadMethodCallException("Method [$method] not a valid HTTP method."); 357 | } 358 | 359 | if (!isset($args[0])) { 360 | throw new \BadMethodCallException('Missing argument 1 ($url) for '.__CLASS__.'::'.$func); 361 | } 362 | $url = $args[0]; 363 | 364 | if (isset($args[1])) { 365 | $data = $args[1]; 366 | } else { 367 | $data = null; 368 | } 369 | 370 | $request = $this->newRequest($method, $url, $data, $encoding); 371 | 372 | return $this->sendRequest($request); 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /src/cURLException.php: -------------------------------------------------------------------------------- 1 | 6 | * @license http://opensource.org/licenses/MIT 7 | * @package PHP cURL 8 | */ 9 | 10 | namespace anlutro\cURL; 11 | 12 | use Exception; 13 | use RuntimeException; 14 | 15 | class cURLException extends RuntimeException 16 | { 17 | /** 18 | * The request that triggered the exception. 19 | * 20 | * @var Request 21 | */ 22 | protected $request; 23 | 24 | /** 25 | * Constructor. 26 | * 27 | * @param Request|null $request 28 | * @param string $message 29 | * @param integer $code 30 | */ 31 | public function __construct(Request $request, $message = "", $code = 0) 32 | { 33 | parent::__construct($message, $code); 34 | $this->request = $request; 35 | } 36 | 37 | /** 38 | * Get the request that triggered the exception. 39 | * 40 | * @return Request 41 | */ 42 | public function getRequest() 43 | { 44 | return $this->request; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/functional/cURLTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('The web server is not running.'); 14 | } 15 | if (!extension_loaded('curl')) { 16 | $this->markTestSkipped('The curl extension is not installed.'); 17 | } 18 | } 19 | 20 | private function makeCurl() 21 | { 22 | return new anlutro\cURL\cURL; 23 | } 24 | 25 | /** @test */ 26 | public function successfulResponse() 27 | { 28 | $r = $this->makeCurl()->get(static::URL.'/success.php'); 29 | $this->assertEquals(200, $r->statusCode); 30 | $this->assertEquals('200 OK', $r->statusText); 31 | $this->assertEquals('OK', $r->body); 32 | $this->assertNotNull($r->headers); 33 | $this->assertNotNull($r->info); 34 | } 35 | 36 | /** @test */ 37 | public function failedResponse() 38 | { 39 | $r = $this->makeCurl()->get(static::URL.'/failure.php'); 40 | $this->assertEquals(500, $r->statusCode); 41 | $this->assertEquals('500 Internal Server Error', $r->statusText); 42 | $this->assertEquals('Failure', $r->body); 43 | $this->assertNotNull($r->headers); 44 | $this->assertNotNull($r->info); 45 | } 46 | 47 | /** @test */ 48 | public function queryRequestBody() 49 | { 50 | $r = $this->makeCurl()->post(static::URL.'/echo.php', array('foo' => 'bar')); 51 | $this->assertEquals('foo=bar', $r->body); 52 | } 53 | 54 | /** @test */ 55 | public function queryRequestEmptyArrayBody() 56 | { 57 | $r = $this->makeCurl()->post(static::URL.'/echo.php', array()); 58 | $this->assertEquals('', $r->body); 59 | } 60 | 61 | /** @test */ 62 | public function queryRequestEmptyObjectBody() 63 | { 64 | $r = $this->makeCurl()->post(static::URL.'/echo.php', new \stdClass()); 65 | $this->assertEquals('', $r->body); 66 | } 67 | 68 | /** @test */ 69 | public function jsonRequestBody() 70 | { 71 | $r = $this->makeCurl()->jsonPost(static::URL.'/echo.php', array('foo' => 'bar')); 72 | $this->assertEquals('{"foo":"bar"}', $r->body); 73 | } 74 | 75 | /** @test */ 76 | public function jsonRequestEmptyArrayBody() 77 | { 78 | $r = $this->makeCurl()->jsonPost(static::URL.'/echo.php', array()); 79 | $this->assertEquals('[]', $r->body); 80 | } 81 | 82 | /** @test */ 83 | public function jsonRequestEmptyObjectBody() 84 | { 85 | $r = $this->makeCurl()->jsonPost(static::URL.'/echo.php', new \stdClass()); 86 | $this->assertEquals('{}', $r->body); 87 | } 88 | 89 | /** @test */ 90 | public function rawRequestBody() 91 | { 92 | $r = $this->makeCurl()->rawPost(static::URL.'/echo.php', ''); 93 | $this->assertEquals('', $r->body); 94 | } 95 | 96 | /** @test */ 97 | public function rawRequestEmptyBody() 98 | { 99 | $r = $this->makeCurl()->rawPost(static::URL.'/echo.php', ''); 100 | $this->assertEquals('', $r->body); 101 | } 102 | 103 | /** @test */ 104 | public function fileUpload() 105 | { 106 | $file = __FILE__; 107 | if (function_exists('curl_file_create')) { 108 | $data = array('file' => curl_file_create($file)); 109 | } else { 110 | $data = array('file' => '@'.$file); 111 | } 112 | 113 | $r = $this->makeCurl()->rawPost(static::URL.'/upload.php', $data); 114 | $this->assertEquals(basename($file)."\t".filesize($file)."\n", $r->body); 115 | } 116 | 117 | /** @test */ 118 | public function digestAuth() 119 | { 120 | $curl = $this->makeCurl(); 121 | $request = $curl->newRequest('get', static::URL . '/digest-auth.php'); 122 | $request->auth('guest', 'guest'); 123 | $request->setOption(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); 124 | $response = $curl->sendRequest($request); 125 | $this->assertEquals(200, $response->statusCode); 126 | } 127 | 128 | /** @test */ 129 | public function throwsExceptionOnCurlError() 130 | { 131 | $this->setExpectedException('anlutro\cURL\cURLException', 'cURL request failed with error [7]:'); 132 | $this->makeCurl()->get('http://0.0.0.0'); 133 | } 134 | 135 | /** @test */ 136 | public function throwsExceptionWithMissingUrl() 137 | { 138 | $this->setExpectedException('BadMethodCallException', 'Missing argument 1 ($url) for anlutro\cURL\cURL::get'); 139 | $this->makeCurl()->get(); 140 | } 141 | 142 | /** @test */ 143 | public function throwsExceptionWhenDataProvidedButNotAllowed() 144 | { 145 | $this->setExpectedException('InvalidArgumentException', 'HTTP method [options] does not allow POST data.'); 146 | $this->makeCurl()->options('http://localhost', array('foo' => 'bar')); 147 | } 148 | 149 | /** @test */ 150 | public function defaultHeadersAreAdded() 151 | { 152 | $curl = $this->makeCurl(); 153 | $curl->setDefaultHeaders(array('foo' => 'bar')); 154 | $request = $curl->newRequest('post', 'does-not-matter'); 155 | $this->assertEquals('bar', $request->getHeader('foo')); 156 | } 157 | 158 | /** @test */ 159 | public function defaultOptionsAreAdded() 160 | { 161 | $curl = $this->makeCurl(); 162 | $curl->setDefaultOptions(array('foo' => 'bar')); 163 | $request = $curl->newRequest('post', 'does-not-matter'); 164 | $this->assertEquals('bar', $request->getOption('foo')); 165 | } 166 | 167 | /** @test */ 168 | public function curloptFileWorks() 169 | { 170 | $r = $this->makeCurl() 171 | ->newRequest('get', static::URL.'/success.php') 172 | ->setOption(CURLOPT_FILE, $fh = tmpfile()) 173 | ->send(); 174 | $this->assertEquals(200, $r->statusCode); 175 | $this->assertEquals('200 OK', $r->statusText); 176 | $this->assertNotNull($r->headers); 177 | $this->assertNotNull($r->info); 178 | $this->assertNull($r->body); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/server/digest-auth.php: -------------------------------------------------------------------------------- 1 | password 7 | $users = array('admin' => 'mypass', 'guest' => 'guest'); 8 | 9 | 10 | if (empty($_SERVER['PHP_AUTH_DIGEST'])) { 11 | header('HTTP/1.1 401 Unauthorized'); 12 | header('WWW-Authenticate: Digest realm="'.$realm. 13 | '",qop="auth",nonce="'.uniqid().'",opaque="'.md5($realm).'"'); 14 | 15 | die('Text to send if user hits Cancel button'); 16 | } 17 | 18 | 19 | // analyze the PHP_AUTH_DIGEST variable 20 | if (!($data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST'])) || 21 | !isset($users[$data['username']])) 22 | die('Wrong Credentials!'); 23 | 24 | 25 | // generate the valid response 26 | $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]); 27 | $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']); 28 | $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2); 29 | 30 | if ($data['response'] != $valid_response) 31 | die('Wrong Credentials!'); 32 | 33 | // ok, valid username & password 34 | echo 'You are logged in as: ' . $data['username']; 35 | 36 | 37 | // function to parse the http auth header 38 | function http_digest_parse($txt) 39 | { 40 | // protect against missing data 41 | $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1); 42 | $data = array(); 43 | $keys = implode('|', array_keys($needed_parts)); 44 | 45 | preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER); 46 | 47 | foreach ($matches as $m) { 48 | $data[$m[1]] = $m[3] ? $m[3] : $m[4]; 49 | unset($needed_parts[$m[1]]); 50 | } 51 | 52 | return $needed_parts ? false : $data; 53 | } 54 | -------------------------------------------------------------------------------- /tests/server/echo.php: -------------------------------------------------------------------------------- 1 | makeRequest(); 16 | 17 | $r->setUrl('foo'); 18 | $this->assertEquals('foo', $r->getUrl()); 19 | 20 | $r->setMethod('post'); 21 | $this->assertEquals('post', $r->getMethod()); 22 | 23 | $r->setData(array('foo' => 'bar')); 24 | $this->assertEquals(array('foo' => 'bar'), $r->getData()); 25 | 26 | $r->setOptions(array('bar' => 'baz')); 27 | $this->assertEquals(array('bar' => 'baz'), $r->getOptions()); 28 | 29 | $r->setHeaders(array('baz' => 'foo')); 30 | $this->assertEquals(array('baz' => 'foo'), $r->getHeaders()); 31 | 32 | $r->setHeader('bar', 'baz'); 33 | $this->assertEquals(array('baz' => 'foo', 'bar' => 'baz'), $r->getHeaders()); 34 | } 35 | 36 | /** @test */ 37 | public function encodeData() 38 | { 39 | $r = $this->makeRequest(); 40 | $r->setMethod('post'); 41 | 42 | $r->setData(array('foo' => 'bar', 'bar' => 'baz')); 43 | $this->assertEquals('foo=bar&bar=baz', $r->encodeData()); 44 | 45 | $r->setEncoding(Request::ENCODING_JSON); 46 | $this->assertEquals('{"foo":"bar","bar":"baz"}', $r->encodeData()); 47 | 48 | $r->setEncoding(Request::ENCODING_RAW); 49 | $r->setData('ArbitraryValue'); 50 | $this->assertEquals('ArbitraryValue', $r->encodeData()); 51 | } 52 | 53 | /** @test */ 54 | public function formatHeaders() 55 | { 56 | $r = $this->makeRequest(); 57 | 58 | $r->setHeaders(array('foo' => 'bar', 'bar' => 'baz')); 59 | $this->assertEquals(array('foo: bar', 'bar: baz'), $r->formatHeaders()); 60 | 61 | $r->setHeaders(array('foo: bar', 'bar: baz')); 62 | $this->assertEquals(array('foo: bar', 'bar: baz'), $r->formatHeaders()); 63 | } 64 | 65 | /** @test */ 66 | public function headersAreCaseInsensitive() 67 | { 68 | $r = $this->makeRequest(); 69 | 70 | $r->setHeader('foo', 'bar'); 71 | $r->setHeader('Foo', 'bar'); 72 | $r->setHeader('FOO', 'bar'); 73 | $this->assertEquals(array('foo' => 'bar'), $r->getHeaders()); 74 | } 75 | 76 | /** 77 | * @test 78 | * @expectedException InvalidArgumentException 79 | */ 80 | public function invalidMethod() 81 | { 82 | $this->makeRequest()->setMethod('foo'); 83 | } 84 | 85 | /** 86 | * @test 87 | * @expectedException InvalidArgumentException 88 | */ 89 | public function invalidEncoding() 90 | { 91 | $this->makeRequest()->setEncoding(999); 92 | } 93 | 94 | /** @test */ 95 | public function userAndPass() 96 | { 97 | $r = $this->makeRequest(); 98 | $this->assertEquals(null, $r->getUserAndPass()); 99 | $r->setUser('foo'); 100 | $this->assertEquals('foo:', $r->getUserAndPass()); 101 | $r->setPass('bar'); 102 | $this->assertEquals('foo:bar', $r->getUserAndPass()); 103 | } 104 | 105 | /** @test */ 106 | public function cookies() 107 | { 108 | $r = $this->makeRequest(); 109 | 110 | $this->assertEquals(array(), $r->getCookies()); 111 | 112 | $r->setCookie('foo', 'bar'); 113 | $this->assertEquals(array('foo' => 'bar'), $r->getCookies()); 114 | $this->assertEquals('bar', $r->getCookie('foo')); 115 | $this->assertEquals('foo=bar', $r->getHeader('cookie')); 116 | 117 | $r->setCookie('bar', 'baz'); 118 | $this->assertEquals(array('foo' => 'bar', 'bar' => 'baz'), $r->getCookies()); 119 | $this->assertEquals('baz', $r->getCookie('bar')); 120 | $this->assertEquals('foo=bar; bar=baz', $r->getHeader('cookie')); 121 | 122 | $r->setCookies(array('baz' => 'foo')); 123 | $this->assertEquals(array('baz' => 'foo'), $r->getCookies()); 124 | $this->assertEquals('foo', $r->getCookie('baz')); 125 | $this->assertEquals('baz=foo', $r->getHeader('cookie')); 126 | } 127 | 128 | /** @test */ 129 | public function emptyJsonGetRequestHasNoData() 130 | { 131 | $r = $this->makeRequest(); 132 | $r->setEncoding(Request::ENCODING_JSON); 133 | $r->setMethod('get'); 134 | 135 | $this->assertFalse($r->hasData()); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/unit/ResponseTest.php: -------------------------------------------------------------------------------- 1 | makeResponse('', 'HTTP/1.1 200 OK'); 16 | $this->assertEquals(200, $r->statusCode); 17 | $this->assertEquals('200 OK', $r->statusText); 18 | 19 | $r = $this->makeResponse('', 'HTTP/1.1 302 Found'); 20 | $this->assertEquals(302, $r->statusCode); 21 | $this->assertEquals('302 Found', $r->statusText); 22 | } 23 | 24 | /** @test */ 25 | public function parsesHttp2ResponseCorrectly() 26 | { 27 | $r = $this->makeResponse('', 'HTTP/2 200'); 28 | $this->assertEquals(200, $r->statusCode); 29 | $this->assertEquals('200', $r->statusText); 30 | } 31 | 32 | /** @test */ 33 | public function parsesHeaderStringCorrectly() 34 | { 35 | $header = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 0"; 36 | $r = $this->makeResponse('', $header); 37 | $this->assertEquals('text/plain', $r->getHeader('content-type')); 38 | $this->assertEquals('0', $r->getHeader('content-length')); 39 | $this->assertEquals(null, $r->getHeader('x-nonexistant')); 40 | } 41 | 42 | /** @test */ 43 | public function duplicateHeadersAreHandled() 44 | { 45 | $header = "HTTP/1.1 200 OK\r\nX-Var: A\r\nX-Var: B\r\nX-Var: C"; 46 | $r = $this->makeResponse('', $header); 47 | $this->assertEquals(array('A', 'B', 'C'), $r->getHeader('X-Var')); 48 | } 49 | 50 | /** @test */ 51 | public function httpContinueResponsesAreHandled() 52 | { 53 | $header = "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nx-var: foo"; 54 | $r = $this->makeResponse('', $header); 55 | $this->assertEquals(200, $r->statusCode); 56 | $this->assertEquals('foo', $r->getHeader('x-var')); 57 | } 58 | 59 | /** @test */ 60 | public function throwsExceptionIfHeaderDoesntStartWithHttpStatus() 61 | { 62 | $this->setExpectedException('UnexpectedValueException', 'Invalid response header'); 63 | $this->makeResponse('', 'x-var: foo'); 64 | } 65 | 66 | /** @test */ 67 | public function httpUnauthorizedResponsesContainingMultipleStatusesAreHandled() 68 | { 69 | $header = "HTTP/1.1 401 Unauthorized\r\nwww-authenticate: digest something\r\n\r\nHTTP/1.1 200 OK\r\nx-var: foo"; 70 | $r = $this->makeResponse('', $header, [CURLINFO_HTTPAUTH_AVAIL => CURLAUTH_DIGEST]); 71 | $this->assertEquals(200, $r->statusCode); 72 | $this->assertEquals('foo', $r->getHeader('x-var')); 73 | } 74 | } 75 | --------------------------------------------------------------------------------