PageRenderTime 56ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/library/Zend/Http/Client.php

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