PageRenderTime 57ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/zendframework/zendframework/library/Zend/Http/Client.php

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