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