1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Http; 11 12use ArrayIterator; 13use Traversable; 14use Zend\Stdlib; 15use Zend\Stdlib\ArrayUtils; 16use Zend\Stdlib\ErrorHandler; 17use Zend\Uri\Http; 18 19/** 20 * Http client 21 */ 22class Client implements Stdlib\DispatchableInterface 23{ 24 /** 25 * @const string Supported HTTP Authentication methods 26 */ 27 const AUTH_BASIC = 'basic'; 28 const AUTH_DIGEST = 'digest'; 29 30 /** 31 * @const string POST data encoding methods 32 */ 33 const ENC_URLENCODED = 'application/x-www-form-urlencoded'; 34 const ENC_FORMDATA = 'multipart/form-data'; 35 36 /** 37 * @const string DIGEST Authentication 38 */ 39 const DIGEST_REALM = 'realm'; 40 const DIGEST_QOP = 'qop'; 41 const DIGEST_NONCE = 'nonce'; 42 const DIGEST_OPAQUE = 'opaque'; 43 const DIGEST_NC = 'nc'; 44 const DIGEST_CNONCE = 'cnonce'; 45 46 /** 47 * @var Response 48 */ 49 protected $response; 50 51 /** 52 * @var Request 53 */ 54 protected $request; 55 56 /** 57 * @var Client\Adapter\AdapterInterface 58 */ 59 protected $adapter; 60 61 /** 62 * @var array 63 */ 64 protected $auth = array(); 65 66 /** 67 * @var string 68 */ 69 protected $streamName = null; 70 71 /** 72 * @var array of Header\SetCookie 73 */ 74 protected $cookies = array(); 75 76 /** 77 * @var string 78 */ 79 protected $encType = ''; 80 81 /** 82 * @var Request 83 */ 84 protected $lastRawRequest = null; 85 86 /** 87 * @var Response 88 */ 89 protected $lastRawResponse = null; 90 91 /** 92 * @var int 93 */ 94 protected $redirectCounter = 0; 95 96 /** 97 * Configuration array, set using the constructor or using ::setOptions() 98 * 99 * @var array 100 */ 101 protected $config = array( 102 'maxredirects' => 5, 103 'strictredirects' => false, 104 'useragent' => 'Zend\Http\Client', 105 'timeout' => 10, 106 'adapter' => 'Zend\Http\Client\Adapter\Socket', 107 'httpversion' => Request::VERSION_11, 108 'storeresponse' => true, 109 'keepalive' => false, 110 'outputstream' => false, 111 'encodecookies' => true, 112 'argseparator' => null, 113 'rfc3986strict' => false 114 ); 115 116 /** 117 * Fileinfo magic database resource 118 * 119 * This variable is populated the first time _detectFileMimeType is called 120 * and is then reused on every call to this method 121 * 122 * @var resource 123 */ 124 protected static $fileInfoDb = null; 125 126 /** 127 * Constructor 128 * 129 * @param string $uri 130 * @param array|Traversable $options 131 */ 132 public function __construct($uri = null, $options = null) 133 { 134 if ($uri !== null) { 135 $this->setUri($uri); 136 } 137 if ($options !== null) { 138 $this->setOptions($options); 139 } 140 } 141 142 /** 143 * Set configuration parameters for this HTTP client 144 * 145 * @param array|Traversable $options 146 * @return Client 147 * @throws Client\Exception\InvalidArgumentException 148 */ 149 public function setOptions($options = array()) 150 { 151 if ($options instanceof Traversable) { 152 $options = ArrayUtils::iteratorToArray($options); 153 } 154 if (!is_array($options)) { 155 throw new Client\Exception\InvalidArgumentException('Config parameter is not valid'); 156 } 157 158 /** Config Key Normalization */ 159 foreach ($options as $k => $v) { 160 $this->config[str_replace(array('-', '_', ' ', '.'), '', strtolower($k))] = $v; // replace w/ normalized 161 } 162 163 // Pass configuration options to the adapter if it exists 164 if ($this->adapter instanceof Client\Adapter\AdapterInterface) { 165 $this->adapter->setOptions($options); 166 } 167 168 return $this; 169 } 170 171 /** 172 * Load the connection adapter 173 * 174 * While this method is not called more than one for a client, it is 175 * separated from ->request() to preserve logic and readability 176 * 177 * @param Client\Adapter\AdapterInterface|string $adapter 178 * @return Client 179 * @throws Client\Exception\InvalidArgumentException 180 */ 181 public function setAdapter($adapter) 182 { 183 if (is_string($adapter)) { 184 if (!class_exists($adapter)) { 185 throw new Client\Exception\InvalidArgumentException('Unable to locate adapter class "' . $adapter . '"'); 186 } 187 $adapter = new $adapter; 188 } 189 190 if (! $adapter instanceof Client\Adapter\AdapterInterface) { 191 throw new Client\Exception\InvalidArgumentException('Passed adapter is not a HTTP connection adapter'); 192 } 193 194 $this->adapter = $adapter; 195 $config = $this->config; 196 unset($config['adapter']); 197 $this->adapter->setOptions($config); 198 return $this; 199 } 200 201 /** 202 * Load the connection adapter 203 * 204 * @return Client\Adapter\AdapterInterface $adapter 205 */ 206 public function getAdapter() 207 { 208 if (! $this->adapter) { 209 $this->setAdapter($this->config['adapter']); 210 } 211 212 return $this->adapter; 213 } 214 215 /** 216 * Set request 217 * 218 * @param Request $request 219 * @return Client 220 */ 221 public function setRequest(Request $request) 222 { 223 $this->request = $request; 224 return $this; 225 } 226 227 /** 228 * Get Request 229 * 230 * @return Request 231 */ 232 public function getRequest() 233 { 234 if (empty($this->request)) { 235 $this->request = new Request(); 236 $this->request->setAllowCustomMethods(false); 237 } 238 return $this->request; 239 } 240 241 /** 242 * Set response 243 * 244 * @param Response $response 245 * @return Client 246 */ 247 public function setResponse(Response $response) 248 { 249 $this->response = $response; 250 return $this; 251 } 252 253 /** 254 * Get Response 255 * 256 * @return Response 257 */ 258 public function getResponse() 259 { 260 if (empty($this->response)) { 261 $this->response = new Response(); 262 } 263 return $this->response; 264 } 265 266 /** 267 * Get the last request (as a string) 268 * 269 * @return string 270 */ 271 public function getLastRawRequest() 272 { 273 return $this->lastRawRequest; 274 } 275 276 /** 277 * Get the last response (as a string) 278 * 279 * @return string 280 */ 281 public function getLastRawResponse() 282 { 283 return $this->lastRawResponse; 284 } 285 286 /** 287 * Get the redirections count 288 * 289 * @return int 290 */ 291 public function getRedirectionsCount() 292 { 293 return $this->redirectCounter; 294 } 295 296 /** 297 * Set Uri (to the request) 298 * 299 * @param string|Http $uri 300 * @return Client 301 */ 302 public function setUri($uri) 303 { 304 if (!empty($uri)) { 305 // remember host of last request 306 $lastHost = $this->getRequest()->getUri()->getHost(); 307 $this->getRequest()->setUri($uri); 308 309 // if host changed, the HTTP authentication should be cleared for security 310 // reasons, see #4215 for a discussion - currently authentication is also 311 // cleared for peer subdomains due to technical limits 312 $nextHost = $this->getRequest()->getUri()->getHost(); 313 if (!preg_match('/' . preg_quote($lastHost, '/') . '$/i', $nextHost)) { 314 $this->clearAuth(); 315 } 316 317 // Set auth if username and password has been specified in the uri 318 if ($this->getUri()->getUser() && $this->getUri()->getPassword()) { 319 $this->setAuth($this->getUri()->getUser(), $this->getUri()->getPassword()); 320 } 321 322 // We have no ports, set the defaults 323 if (! $this->getUri()->getPort()) { 324 $this->getUri()->setPort(($this->getUri()->getScheme() == 'https' ? 443 : 80)); 325 } 326 } 327 return $this; 328 } 329 330 /** 331 * Get uri (from the request) 332 * 333 * @return Http 334 */ 335 public function getUri() 336 { 337 return $this->getRequest()->getUri(); 338 } 339 340 /** 341 * Set the HTTP method (to the request) 342 * 343 * @param string $method 344 * @return Client 345 */ 346 public function setMethod($method) 347 { 348 $method = $this->getRequest()->setMethod($method)->getMethod(); 349 350 if (empty($this->encType) 351 && in_array( 352 $method, 353 array( 354 Request::METHOD_POST, 355 Request::METHOD_PUT, 356 Request::METHOD_DELETE, 357 Request::METHOD_PATCH, 358 Request::METHOD_OPTIONS, 359 ), 360 true 361 ) 362 ) { 363 $this->setEncType(self::ENC_URLENCODED); 364 } 365 366 return $this; 367 } 368 369 /** 370 * Get the HTTP method 371 * 372 * @return string 373 */ 374 public function getMethod() 375 { 376 return $this->getRequest()->getMethod(); 377 } 378 379 /** 380 * Set the query string argument separator 381 * 382 * @param string $argSeparator 383 * @return Client 384 */ 385 public function setArgSeparator($argSeparator) 386 { 387 $this->setOptions(array("argseparator" => $argSeparator)); 388 return $this; 389 } 390 391 /** 392 * Get the query string argument separator 393 * 394 * @return string 395 */ 396 public function getArgSeparator() 397 { 398 $argSeparator = $this->config['argseparator']; 399 if (empty($argSeparator)) { 400 $argSeparator = ini_get('arg_separator.output'); 401 $this->setArgSeparator($argSeparator); 402 } 403 return $argSeparator; 404 } 405 406 /** 407 * Set the encoding type and the boundary (if any) 408 * 409 * @param string $encType 410 * @param string $boundary 411 * @return Client 412 */ 413 public function setEncType($encType, $boundary = null) 414 { 415 if (null === $encType || empty($encType)) { 416 $this->encType = null; 417 return $this; 418 } 419 420 if (! empty($boundary)) { 421 $encType .= sprintf('; boundary=%s', $boundary); 422 } 423 424 $this->encType = $encType; 425 return $this; 426 } 427 428 /** 429 * Get the encoding type 430 * 431 * @return string 432 */ 433 public function getEncType() 434 { 435 return $this->encType; 436 } 437 438 /** 439 * Set raw body (for advanced use cases) 440 * 441 * @param string $body 442 * @return Client 443 */ 444 public function setRawBody($body) 445 { 446 $this->getRequest()->setContent($body); 447 return $this; 448 } 449 450 /** 451 * Set the POST parameters 452 * 453 * @param array $post 454 * @return Client 455 */ 456 public function setParameterPost(array $post) 457 { 458 $this->getRequest()->getPost()->fromArray($post); 459 return $this; 460 } 461 462 /** 463 * Set the GET parameters 464 * 465 * @param array $query 466 * @return Client 467 */ 468 public function setParameterGet(array $query) 469 { 470 $this->getRequest()->getQuery()->fromArray($query); 471 return $this; 472 } 473 474 /** 475 * Reset all the HTTP parameters (request, response, etc) 476 * 477 * @param bool $clearCookies Also clear all valid cookies? (defaults to false) 478 * @param bool $clearAuth Also clear http authentication? (defaults to true) 479 * @return Client 480 */ 481 public function resetParameters($clearCookies = false /*, $clearAuth = true */) 482 { 483 $clearAuth = true; 484 if (func_num_args() > 1) { 485 $clearAuth = func_get_arg(1); 486 } 487 488 $uri = $this->getUri(); 489 490 $this->streamName = null; 491 $this->encType = null; 492 $this->request = null; 493 $this->response = null; 494 $this->lastRawRequest = null; 495 $this->lastRawResponse = null; 496 497 $this->setUri($uri); 498 499 if ($clearCookies) { 500 $this->clearCookies(); 501 } 502 503 if ($clearAuth) { 504 $this->clearAuth(); 505 } 506 507 return $this; 508 } 509 510 /** 511 * Return the current cookies 512 * 513 * @return array 514 */ 515 public function getCookies() 516 { 517 return $this->cookies; 518 } 519 520 /** 521 * Get the cookie Id (name+domain+path) 522 * 523 * @param Header\SetCookie|Header\Cookie $cookie 524 * @return string|bool 525 */ 526 protected function getCookieId($cookie) 527 { 528 if (($cookie instanceof Header\SetCookie) || ($cookie instanceof Header\Cookie)) { 529 return $cookie->getName() . $cookie->getDomain() . $cookie->getPath(); 530 } 531 return false; 532 } 533 534 /** 535 * Add a cookie 536 * 537 * @param array|ArrayIterator|Header\SetCookie|string $cookie 538 * @param string $value 539 * @param string $expire 540 * @param string $path 541 * @param string $domain 542 * @param bool $secure 543 * @param bool $httponly 544 * @param string $maxAge 545 * @param string $version 546 * @throws Exception\InvalidArgumentException 547 * @return Client 548 */ 549 public function addCookie($cookie, $value = null, $expire = null, $path = null, $domain = null, $secure = false, $httponly = true, $maxAge = null, $version = null) 550 { 551 if (is_array($cookie) || $cookie instanceof ArrayIterator) { 552 foreach ($cookie as $setCookie) { 553 if ($setCookie instanceof Header\SetCookie) { 554 $this->cookies[$this->getCookieId($setCookie)] = $setCookie; 555 } else { 556 throw new Exception\InvalidArgumentException('The cookie parameter is not a valid Set-Cookie type'); 557 } 558 } 559 } elseif (is_string($cookie) && $value !== null) { 560 $setCookie = new Header\SetCookie($cookie, $value, $expire, $path, $domain, $secure, $httponly, $maxAge, $version); 561 $this->cookies[$this->getCookieId($setCookie)] = $setCookie; 562 } elseif ($cookie instanceof Header\SetCookie) { 563 $this->cookies[$this->getCookieId($cookie)] = $cookie; 564 } else { 565 throw new Exception\InvalidArgumentException('Invalid parameter type passed as Cookie'); 566 } 567 return $this; 568 } 569 570 /** 571 * Set an array of cookies 572 * 573 * @param array $cookies 574 * @throws Exception\InvalidArgumentException 575 * @return Client 576 */ 577 public function setCookies($cookies) 578 { 579 if (is_array($cookies)) { 580 $this->clearCookies(); 581 foreach ($cookies as $name => $value) { 582 $this->addCookie($name, $value); 583 } 584 } else { 585 throw new Exception\InvalidArgumentException('Invalid cookies passed as parameter, it must be an array'); 586 } 587 return $this; 588 } 589 590 /** 591 * Clear all the cookies 592 */ 593 public function clearCookies() 594 { 595 $this->cookies = array(); 596 } 597 598 /** 599 * Set the headers (for the request) 600 * 601 * @param Headers|array $headers 602 * @throws Exception\InvalidArgumentException 603 * @return Client 604 */ 605 public function setHeaders($headers) 606 { 607 if (is_array($headers)) { 608 $newHeaders = new Headers(); 609 $newHeaders->addHeaders($headers); 610 $this->getRequest()->setHeaders($newHeaders); 611 } elseif ($headers instanceof Headers) { 612 $this->getRequest()->setHeaders($headers); 613 } else { 614 throw new Exception\InvalidArgumentException('Invalid parameter headers passed'); 615 } 616 return $this; 617 } 618 619 /** 620 * Check if exists the header type specified 621 * 622 * @param string $name 623 * @return bool 624 */ 625 public function hasHeader($name) 626 { 627 $headers = $this->getRequest()->getHeaders(); 628 629 if ($headers instanceof Headers) { 630 return $headers->has($name); 631 } 632 633 return false; 634 } 635 636 /** 637 * Get the header value of the request 638 * 639 * @param string $name 640 * @return string|bool 641 */ 642 public function getHeader($name) 643 { 644 $headers = $this->getRequest()->getHeaders(); 645 646 if ($headers instanceof Headers) { 647 if ($headers->get($name)) { 648 return $headers->get($name)->getFieldValue(); 649 } 650 } 651 return false; 652 } 653 654 /** 655 * Set streaming for received data 656 * 657 * @param string|bool $streamfile Stream file, true for temp file, false/null for no streaming 658 * @return \Zend\Http\Client 659 */ 660 public function setStream($streamfile = true) 661 { 662 $this->setOptions(array("outputstream" => $streamfile)); 663 return $this; 664 } 665 666 /** 667 * Get status of streaming for received data 668 * @return bool|string 669 */ 670 public function getStream() 671 { 672 if (null !== $this->streamName) { 673 return $this->streamName; 674 } 675 676 return $this->config['outputstream']; 677 } 678 679 /** 680 * Create temporary stream 681 * 682 * @throws Exception\RuntimeException 683 * @return resource 684 */ 685 protected function openTempStream() 686 { 687 $this->streamName = $this->config['outputstream']; 688 689 if (!is_string($this->streamName)) { 690 // If name is not given, create temp name 691 $this->streamName = tempnam( 692 isset($this->config['streamtmpdir']) ? $this->config['streamtmpdir'] : sys_get_temp_dir(), 693 'Zend\Http\Client' 694 ); 695 } 696 697 ErrorHandler::start(); 698 $fp = fopen($this->streamName, "w+b"); 699 $error = ErrorHandler::stop(); 700 if (false === $fp) { 701 if ($this->adapter instanceof Client\Adapter\AdapterInterface) { 702 $this->adapter->close(); 703 } 704 throw new Exception\RuntimeException("Could not open temp file {$this->streamName}", 0, $error); 705 } 706 707 return $fp; 708 } 709 710 /** 711 * Create a HTTP authentication "Authorization:" header according to the 712 * specified user, password and authentication method. 713 * 714 * @param string $user 715 * @param string $password 716 * @param string $type 717 * @throws Exception\InvalidArgumentException 718 * @return Client 719 */ 720 public function setAuth($user, $password, $type = self::AUTH_BASIC) 721 { 722 if (!defined('static::AUTH_' . strtoupper($type))) { 723 throw new Exception\InvalidArgumentException("Invalid or not supported authentication type: '$type'"); 724 } 725 726 if (empty($user)) { 727 throw new Exception\InvalidArgumentException("The username cannot be empty"); 728 } 729 730 $this->auth = array( 731 'user' => $user, 732 'password' => $password, 733 'type' => $type 734 ); 735 736 return $this; 737 } 738 739 /** 740 * Clear http authentication 741 */ 742 public function clearAuth() 743 { 744 $this->auth = array(); 745 } 746 747 /** 748 * Calculate the response value according to the HTTP authentication type 749 * 750 * @see http://www.faqs.org/rfcs/rfc2617.html 751 * @param string $user 752 * @param string $password 753 * @param string $type 754 * @param array $digest 755 * @param null|string $entityBody 756 * @throws Exception\InvalidArgumentException 757 * @return string|bool 758 */ 759 protected function calcAuthDigest($user, $password, $type = self::AUTH_BASIC, $digest = array(), $entityBody = null) 760 { 761 if (!defined('self::AUTH_' . strtoupper($type))) { 762 throw new Exception\InvalidArgumentException("Invalid or not supported authentication type: '$type'"); 763 } 764 $response = false; 765 switch (strtolower($type)) { 766 case self::AUTH_BASIC : 767 // In basic authentication, the user name cannot contain ":" 768 if (strpos($user, ':') !== false) { 769 throw new Exception\InvalidArgumentException("The user name cannot contain ':' in Basic HTTP authentication"); 770 } 771 $response = base64_encode($user . ':' . $password); 772 break; 773 case self::AUTH_DIGEST : 774 if (empty($digest)) { 775 throw new Exception\InvalidArgumentException("The digest cannot be empty"); 776 } 777 foreach ($digest as $key => $value) { 778 if (!defined('self::DIGEST_' . strtoupper($key))) { 779 throw new Exception\InvalidArgumentException("Invalid or not supported digest authentication parameter: '$key'"); 780 } 781 } 782 $ha1 = md5($user . ':' . $digest['realm'] . ':' . $password); 783 if (empty($digest['qop']) || strtolower($digest['qop']) == 'auth') { 784 $ha2 = md5($this->getMethod() . ':' . $this->getUri()->getPath()); 785 } elseif (strtolower($digest['qop']) == 'auth-int') { 786 if (empty($entityBody)) { 787 throw new Exception\InvalidArgumentException("I cannot use the auth-int digest authentication without the entity body"); 788 } 789 $ha2 = md5($this->getMethod() . ':' . $this->getUri()->getPath() . ':' . md5($entityBody)); 790 } 791 if (empty($digest['qop'])) { 792 $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $ha2); 793 } else { 794 $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $digest['nc'] 795 . ':' . $digest['cnonce'] . ':' . $digest['qoc'] . ':' . $ha2); 796 } 797 break; 798 } 799 return $response; 800 } 801 802 /** 803 * Dispatch 804 * 805 * @param Stdlib\RequestInterface $request 806 * @param Stdlib\ResponseInterface $response 807 * @return Stdlib\ResponseInterface 808 */ 809 public function dispatch(Stdlib\RequestInterface $request, Stdlib\ResponseInterface $response = null) 810 { 811 $response = $this->send($request); 812 return $response; 813 } 814 815 /** 816 * Send HTTP request 817 * 818 * @param Request $request 819 * @return Response 820 * @throws Exception\RuntimeException 821 * @throws Client\Exception\RuntimeException 822 */ 823 public function send(Request $request = null) 824 { 825 if ($request !== null) { 826 $this->setRequest($request); 827 } 828 829 $this->redirectCounter = 0; 830 831 $adapter = $this->getAdapter(); 832 833 // Send the first request. If redirected, continue. 834 do { 835 // uri 836 $uri = $this->getUri(); 837 838 // query 839 $query = $this->getRequest()->getQuery(); 840 841 if (!empty($query)) { 842 $queryArray = $query->toArray(); 843 844 if (!empty($queryArray)) { 845 $newUri = $uri->toString(); 846 $queryString = http_build_query($queryArray, null, $this->getArgSeparator()); 847 848 if ($this->config['rfc3986strict']) { 849 $queryString = str_replace('+', '%20', $queryString); 850 } 851 852 if (strpos($newUri, '?') !== false) { 853 $newUri .= $this->getArgSeparator() . $queryString; 854 } else { 855 $newUri .= '?' . $queryString; 856 } 857 858 $uri = new Http($newUri); 859 } 860 } 861 // If we have no ports, set the defaults 862 if (!$uri->getPort()) { 863 $uri->setPort($uri->getScheme() == 'https' ? 443 : 80); 864 } 865 866 // method 867 $method = $this->getRequest()->getMethod(); 868 869 // this is so the correct Encoding Type is set 870 $this->setMethod($method); 871 872 // body 873 $body = $this->prepareBody(); 874 875 // headers 876 $headers = $this->prepareHeaders($body, $uri); 877 878 $secure = $uri->getScheme() == 'https'; 879 880 // cookies 881 $cookie = $this->prepareCookies($uri->getHost(), $uri->getPath(), $secure); 882 if ($cookie->getFieldValue()) { 883 $headers['Cookie'] = $cookie->getFieldValue(); 884 } 885 886 // check that adapter supports streaming before using it 887 if (is_resource($body) && !($adapter instanceof Client\Adapter\StreamInterface)) { 888 throw new Client\Exception\RuntimeException('Adapter does not support streaming'); 889 } 890 891 // calling protected method to allow extending classes 892 // to wrap the interaction with the adapter 893 $response = $this->doRequest($uri, $method, $secure, $headers, $body); 894 895 if (! $response) { 896 throw new Exception\RuntimeException('Unable to read response, or response is empty'); 897 } 898 899 if ($this->config['storeresponse']) { 900 $this->lastRawResponse = $response; 901 } else { 902 $this->lastRawResponse = null; 903 } 904 905 if ($this->config['outputstream']) { 906 $stream = $this->getStream(); 907 if (!is_resource($stream) && is_string($stream)) { 908 $stream = fopen($stream, 'r'); 909 } 910 $streamMetaData = stream_get_meta_data($stream); 911 if ($streamMetaData['seekable']) { 912 rewind($stream); 913 } 914 // cleanup the adapter 915 $adapter->setOutputStream(null); 916 $response = Response\Stream::fromStream($response, $stream); 917 $response->setStreamName($this->streamName); 918 if (!is_string($this->config['outputstream'])) { 919 // we used temp name, will need to clean up 920 $response->setCleanup(true); 921 } 922 } else { 923 $response = $this->getResponse()->fromString($response); 924 } 925 926 // Get the cookies from response (if any) 927 $setCookies = $response->getCookie(); 928 if (!empty($setCookies)) { 929 $this->addCookie($setCookies); 930 } 931 932 // If we got redirected, look for the Location header 933 if ($response->isRedirect() && ($response->getHeaders()->has('Location'))) { 934 // Avoid problems with buggy servers that add whitespace at the 935 // end of some headers 936 $location = trim($response->getHeaders()->get('Location')->getFieldValue()); 937 938 // Check whether we send the exact same request again, or drop the parameters 939 // and send a GET request 940 if ($response->getStatusCode() == 303 || 941 ((! $this->config['strictredirects']) && ($response->getStatusCode() == 302 || 942 $response->getStatusCode() == 301))) { 943 $this->resetParameters(false, false); 944 $this->setMethod(Request::METHOD_GET); 945 } 946 947 // If we got a well formed absolute URI 948 if (($scheme = substr($location, 0, 6)) && 949 ($scheme == 'http:/' || $scheme == 'https:')) { 950 // setURI() clears parameters if host changed, see #4215 951 $this->setUri($location); 952 } else { 953 // Split into path and query and set the query 954 if (strpos($location, '?') !== false) { 955 list($location, $query) = explode('?', $location, 2); 956 } else { 957 $query = ''; 958 } 959 $this->getUri()->setQuery($query); 960 961 // Else, if we got just an absolute path, set it 962 if (strpos($location, '/') === 0) { 963 $this->getUri()->setPath($location); 964 // Else, assume we have a relative path 965 } else { 966 // Get the current path directory, removing any trailing slashes 967 $path = $this->getUri()->getPath(); 968 $path = rtrim(substr($path, 0, strrpos($path, '/')), "/"); 969 $this->getUri()->setPath($path . '/' . $location); 970 } 971 } 972 ++$this->redirectCounter; 973 } else { 974 // If we didn't get any location, stop redirecting 975 break; 976 } 977 } while ($this->redirectCounter <= $this->config['maxredirects']); 978 979 $this->response = $response; 980 return $response; 981 } 982 983 /** 984 * Fully reset the HTTP client (auth, cookies, request, response, etc.) 985 * 986 * @return Client 987 */ 988 public function reset() 989 { 990 $this->resetParameters(); 991 $this->clearAuth(); 992 $this->clearCookies(); 993 994 return $this; 995 } 996 997 /** 998 * Set a file to upload (using a POST request) 999 * 1000 * Can be used in two ways: 1001 * 1002 * 1. $data is null (default): $filename is treated as the name if a local file which 1003 * will be read and sent. Will try to guess the content type using mime_content_type(). 1004 * 2. $data is set - $filename is sent as the file name, but $data is sent as the file 1005 * contents and no file is read from the file system. In this case, you need to 1006 * manually set the Content-Type ($ctype) or it will default to 1007 * application/octet-stream. 1008 * 1009 * @param string $filename Name of file to upload, or name to save as 1010 * @param string $formname Name of form element to send as 1011 * @param string $data Data to send (if null, $filename is read and sent) 1012 * @param string $ctype Content type to use (if $data is set and $ctype is 1013 * null, will be application/octet-stream) 1014 * @return Client 1015 * @throws Exception\RuntimeException 1016 */ 1017 public function setFileUpload($filename, $formname, $data = null, $ctype = null) 1018 { 1019 if ($data === null) { 1020 ErrorHandler::start(); 1021 $data = file_get_contents($filename); 1022 $error = ErrorHandler::stop(); 1023 if ($data === false) { 1024 throw new Exception\RuntimeException("Unable to read file '{$filename}' for upload", 0, $error); 1025 } 1026 if (!$ctype) { 1027 $ctype = $this->detectFileMimeType($filename); 1028 } 1029 } 1030 1031 $this->getRequest()->getFiles()->set($filename, array( 1032 'formname' => $formname, 1033 'filename' => basename($filename), 1034 'ctype' => $ctype, 1035 'data' => $data 1036 )); 1037 1038 return $this; 1039 } 1040 1041 /** 1042 * Remove a file to upload 1043 * 1044 * @param string $filename 1045 * @return bool 1046 */ 1047 public function removeFileUpload($filename) 1048 { 1049 $file = $this->getRequest()->getFiles()->get($filename); 1050 if (!empty($file)) { 1051 $this->getRequest()->getFiles()->set($filename, null); 1052 return true; 1053 } 1054 return false; 1055 } 1056 1057 /** 1058 * Prepare Cookies 1059 * 1060 * @param string $domain 1061 * @param string $path 1062 * @param bool $secure 1063 * @return Header\Cookie|bool 1064 */ 1065 protected function prepareCookies($domain, $path, $secure) 1066 { 1067 $validCookies = array(); 1068 1069 if (!empty($this->cookies)) { 1070 foreach ($this->cookies as $id => $cookie) { 1071 if ($cookie->isExpired()) { 1072 unset($this->cookies[$id]); 1073 continue; 1074 } 1075 1076 if ($cookie->isValidForRequest($domain, $path, $secure)) { 1077 // OAM hack some domains try to set the cookie multiple times 1078 $validCookies[$cookie->getName()] = $cookie; 1079 } 1080 } 1081 } 1082 1083 $cookies = Header\Cookie::fromSetCookieArray($validCookies); 1084 $cookies->setEncodeValue($this->config['encodecookies']); 1085 1086 return $cookies; 1087 } 1088 1089 /** 1090 * Prepare the request headers 1091 * 1092 * @param resource|string $body 1093 * @param Http $uri 1094 * @throws Exception\RuntimeException 1095 * @return array 1096 */ 1097 protected function prepareHeaders($body, $uri) 1098 { 1099 $headers = array(); 1100 1101 // Set the host header 1102 if ($this->config['httpversion'] == Request::VERSION_11) { 1103 $host = $uri->getHost(); 1104 // If the port is not default, add it 1105 if (!(($uri->getScheme() == 'http' && $uri->getPort() == 80) || 1106 ($uri->getScheme() == 'https' && $uri->getPort() == 443))) { 1107 $host .= ':' . $uri->getPort(); 1108 } 1109 1110 $headers['Host'] = $host; 1111 } 1112 1113 // Set the connection header 1114 if (!$this->getRequest()->getHeaders()->has('Connection')) { 1115 if (!$this->config['keepalive']) { 1116 $headers['Connection'] = 'close'; 1117 } 1118 } 1119 1120 // Set the Accept-encoding header if not set - depending on whether 1121 // zlib is available or not. 1122 if (!$this->getRequest()->getHeaders()->has('Accept-Encoding')) { 1123 if (function_exists('gzinflate')) { 1124 $headers['Accept-Encoding'] = 'gzip, deflate'; 1125 } else { 1126 $headers['Accept-Encoding'] = 'identity'; 1127 } 1128 } 1129 1130 1131 // Set the user agent header 1132 if (!$this->getRequest()->getHeaders()->has('User-Agent') && isset($this->config['useragent'])) { 1133 $headers['User-Agent'] = $this->config['useragent']; 1134 } 1135 1136 // Set HTTP authentication if needed 1137 if (!empty($this->auth)) { 1138 switch ($this->auth['type']) { 1139 case self::AUTH_BASIC : 1140 $auth = $this->calcAuthDigest($this->auth['user'], $this->auth['password'], $this->auth['type']); 1141 if ($auth !== false) { 1142 $headers['Authorization'] = 'Basic ' . $auth; 1143 } 1144 break; 1145 case self::AUTH_DIGEST : 1146 if (!$this->adapter instanceof Client\Adapter\Curl) { 1147 throw new Exception\RuntimeException("The digest authentication is only available for curl adapters (Zend\\Http\\Client\\Adapter\\Curl)"); 1148 } 1149 1150 $this->adapter->setCurlOption(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); 1151 $this->adapter->setCurlOption(CURLOPT_USERPWD, $this->auth['user'] . ':' . $this->auth['password']); 1152 } 1153 } 1154 1155 // Content-type 1156 $encType = $this->getEncType(); 1157 if (!empty($encType)) { 1158 $headers['Content-Type'] = $encType; 1159 } 1160 1161 if (!empty($body)) { 1162 if (is_resource($body)) { 1163 $fstat = fstat($body); 1164 $headers['Content-Length'] = $fstat['size']; 1165 } else { 1166 $headers['Content-Length'] = strlen($body); 1167 } 1168 } 1169 1170 // Merge the headers of the request (if any) 1171 // here we need right 'http field' and not lowercase letters 1172 $requestHeaders = $this->getRequest()->getHeaders(); 1173 foreach ($requestHeaders as $requestHeaderElement) { 1174 $headers[$requestHeaderElement->getFieldName()] = $requestHeaderElement->getFieldValue(); 1175 } 1176 return $headers; 1177 } 1178 1179 1180 /** 1181 * Prepare the request body (for PATCH, POST and PUT requests) 1182 * 1183 * @return string 1184 * @throws \Zend\Http\Client\Exception\RuntimeException 1185 */ 1186 protected function prepareBody() 1187 { 1188 // According to RFC2616, a TRACE request should not have a body. 1189 if ($this->getRequest()->isTrace()) { 1190 return ''; 1191 } 1192 1193 $rawBody = $this->getRequest()->getContent(); 1194 if (!empty($rawBody)) { 1195 return $rawBody; 1196 } 1197 1198 $body = ''; 1199 $totalFiles = 0; 1200 1201 if (!$this->getRequest()->getHeaders()->has('Content-Type')) { 1202 $totalFiles = count($this->getRequest()->getFiles()->toArray()); 1203 // If we have files to upload, force encType to multipart/form-data 1204 if ($totalFiles > 0) { 1205 $this->setEncType(self::ENC_FORMDATA); 1206 } 1207 } else { 1208 $this->setEncType($this->getHeader('Content-Type')); 1209 } 1210 1211 // If we have POST parameters or files, encode and add them to the body 1212 if (count($this->getRequest()->getPost()->toArray()) > 0 || $totalFiles > 0) { 1213 if (stripos($this->getEncType(), self::ENC_FORMDATA) === 0) { 1214 $boundary = '---ZENDHTTPCLIENT-' . md5(microtime()); 1215 $this->setEncType(self::ENC_FORMDATA, $boundary); 1216 1217 // Get POST parameters and encode them 1218 $params = self::flattenParametersArray($this->getRequest()->getPost()->toArray()); 1219 foreach ($params as $pp) { 1220 $body .= $this->encodeFormData($boundary, $pp[0], $pp[1]); 1221 } 1222 1223 // Encode files 1224 foreach ($this->getRequest()->getFiles()->toArray() as $file) { 1225 $fhead = array('Content-Type' => $file['ctype']); 1226 $body .= $this->encodeFormData($boundary, $file['formname'], $file['data'], $file['filename'], $fhead); 1227 } 1228 $body .= "--{$boundary}--\r\n"; 1229 } elseif (stripos($this->getEncType(), self::ENC_URLENCODED) === 0) { 1230 // Encode body as application/x-www-form-urlencoded 1231 $body = http_build_query($this->getRequest()->getPost()->toArray()); 1232 } else { 1233 throw new Client\Exception\RuntimeException("Cannot handle content type '{$this->encType}' automatically"); 1234 } 1235 } 1236 1237 return $body; 1238 } 1239 1240 1241 /** 1242 * Attempt to detect the MIME type of a file using available extensions 1243 * 1244 * This method will try to detect the MIME type of a file. If the fileinfo 1245 * extension is available, it will be used. If not, the mime_magic 1246 * extension which is deprecated but is still available in many PHP setups 1247 * will be tried. 1248 * 1249 * If neither extension is available, the default application/octet-stream 1250 * MIME type will be returned 1251 * 1252 * @param string $file File path 1253 * @return string MIME type 1254 */ 1255 protected function detectFileMimeType($file) 1256 { 1257 $type = null; 1258 1259 // First try with fileinfo functions 1260 if (function_exists('finfo_open')) { 1261 if (static::$fileInfoDb === null) { 1262 ErrorHandler::start(); 1263 static::$fileInfoDb = finfo_open(FILEINFO_MIME); 1264 ErrorHandler::stop(); 1265 } 1266 1267 if (static::$fileInfoDb) { 1268 $type = finfo_file(static::$fileInfoDb, $file); 1269 } 1270 } elseif (function_exists('mime_content_type')) { 1271 $type = mime_content_type($file); 1272 } 1273 1274 // Fallback to the default application/octet-stream 1275 if (! $type) { 1276 $type = 'application/octet-stream'; 1277 } 1278 1279 return $type; 1280 } 1281 1282 /** 1283 * Encode data to a multipart/form-data part suitable for a POST request. 1284 * 1285 * @param string $boundary 1286 * @param string $name 1287 * @param mixed $value 1288 * @param string $filename 1289 * @param array $headers Associative array of optional headers @example ("Content-Transfer-Encoding" => "binary") 1290 * @return string 1291 */ 1292 public function encodeFormData($boundary, $name, $value, $filename = null, $headers = array()) 1293 { 1294 $ret = "--{$boundary}\r\n" . 1295 'Content-Disposition: form-data; name="' . $name . '"'; 1296 1297 if ($filename) { 1298 $ret .= '; filename="' . $filename . '"'; 1299 } 1300 $ret .= "\r\n"; 1301 1302 foreach ($headers as $hname => $hvalue) { 1303 $ret .= "{$hname}: {$hvalue}\r\n"; 1304 } 1305 $ret .= "\r\n"; 1306 $ret .= "{$value}\r\n"; 1307 1308 return $ret; 1309 } 1310 1311 /** 1312 * Convert an array of parameters into a flat array of (key, value) pairs 1313 * 1314 * Will flatten a potentially multi-dimentional array of parameters (such 1315 * as POST parameters) into a flat array of (key, value) paris. In case 1316 * of multi-dimentional arrays, square brackets ([]) will be added to the 1317 * key to indicate an array. 1318 * 1319 * @since 1.9 1320 * 1321 * @param array $parray 1322 * @param string $prefix 1323 * @return array 1324 */ 1325 protected function flattenParametersArray($parray, $prefix = null) 1326 { 1327 if (!is_array($parray)) { 1328 return $parray; 1329 } 1330 1331 $parameters = array(); 1332 1333 foreach ($parray as $name => $value) { 1334 // Calculate array key 1335 if ($prefix) { 1336 if (is_int($name)) { 1337 $key = $prefix . '[]'; 1338 } else { 1339 $key = $prefix . "[$name]"; 1340 } 1341 } else { 1342 $key = $name; 1343 } 1344 1345 if (is_array($value)) { 1346 $parameters = array_merge($parameters, $this->flattenParametersArray($value, $key)); 1347 } else { 1348 $parameters[] = array($key, $value); 1349 } 1350 } 1351 1352 return $parameters; 1353 } 1354 1355 /** 1356 * Separating this from send method allows subclasses to wrap 1357 * the interaction with the adapter 1358 * 1359 * @param Http $uri 1360 * @param string $method 1361 * @param bool $secure 1362 * @param array $headers 1363 * @param string $body 1364 * @return string the raw response 1365 * @throws Exception\RuntimeException 1366 */ 1367 protected function doRequest(Http $uri, $method, $secure = false, $headers = array(), $body = '') 1368 { 1369 // Open the connection, send the request and read the response 1370 $this->adapter->connect($uri->getHost(), $uri->getPort(), $secure); 1371 1372 if ($this->config['outputstream']) { 1373 if ($this->adapter instanceof Client\Adapter\StreamInterface) { 1374 $stream = $this->openTempStream(); 1375 $this->adapter->setOutputStream($stream); 1376 } else { 1377 throw new Exception\RuntimeException('Adapter does not support streaming'); 1378 } 1379 } 1380 // HTTP connection 1381 $this->lastRawRequest = $this->adapter->write( 1382 $method, 1383 $uri, 1384 $this->config['httpversion'], 1385 $headers, 1386 $body 1387 ); 1388 1389 return $this->adapter->read(); 1390 } 1391 1392 /** 1393 * Create a HTTP authentication "Authorization:" header according to the 1394 * specified user, password and authentication method. 1395 * 1396 * @see http://www.faqs.org/rfcs/rfc2617.html 1397 * @param string $user 1398 * @param string $password 1399 * @param string $type 1400 * @return string 1401 * @throws Client\Exception\InvalidArgumentException 1402 */ 1403 public static function encodeAuthHeader($user, $password, $type = self::AUTH_BASIC) 1404 { 1405 switch ($type) { 1406 case self::AUTH_BASIC: 1407 // In basic authentication, the user name cannot contain ":" 1408 if (strpos($user, ':') !== false) { 1409 throw new Client\Exception\InvalidArgumentException("The user name cannot contain ':' in 'Basic' HTTP authentication"); 1410 } 1411 1412 return 'Basic ' . base64_encode($user . ':' . $password); 1413 1414 //case self::AUTH_DIGEST: 1415 /** 1416 * @todo Implement digest authentication 1417 */ 1418 // break; 1419 1420 default: 1421 throw new Client\Exception\InvalidArgumentException("Not a supported HTTP authentication type: '$type'"); 1422 1423 } 1424 1425 return; 1426 } 1427} 1428