PageRenderTime 41ms CodeModel.GetById 6ms RepoModel.GetById 0ms app.codeStats 1ms

/library/Zend/Http/Client.php

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