PageRenderTime 53ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/www/libs/Zend/Http/Client.php

https://bitbucket.org/Ppito/kawaiviewmodel2
PHP | 1342 lines | 714 code | 163 blank | 465 comment | 159 complexity | 8ea3bf2516118eabc6ff5df5d59a9a46 MD5 | raw file
Possible License(s): BSD-3-Clause
  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. if (!is_null($this->streamName)) {
  526. return $this->streamName;
  527. }
  528. return $this->config['outputstream'];
  529. }
  530. /**
  531. * Create temporary stream
  532. *
  533. * @throws Exception\RuntimeException
  534. * @return resource
  535. */
  536. protected function openTempStream()
  537. {
  538. $this->streamName = $this->config['outputstream'];
  539. if (!is_string($this->streamName)) {
  540. // If name is not given, create temp name
  541. $this->streamName = tempnam(
  542. isset($this->config['streamtmpdir']) ? $this->config['streamtmpdir'] : sys_get_temp_dir(),
  543. 'Zend\Http\Client'
  544. );
  545. }
  546. ErrorHandler::start();
  547. $fp = fopen($this->streamName, "w+b");
  548. $error = ErrorHandler::stop();
  549. if (false === $fp) {
  550. if ($this->adapter instanceof Client\Adapter\AdapterInterface) {
  551. $this->adapter->close();
  552. }
  553. throw new Exception\RuntimeException("Could not open temp file {$this->streamName}", 0, $error);
  554. }
  555. return $fp;
  556. }
  557. /**
  558. * Create a HTTP authentication "Authorization:" header according to the
  559. * specified user, password and authentication method.
  560. *
  561. * @param string $user
  562. * @param string $password
  563. * @param string $type
  564. * @throws Exception\InvalidArgumentException
  565. * @return Client
  566. */
  567. public function setAuth($user, $password, $type = self::AUTH_BASIC)
  568. {
  569. if (!defined('self::AUTH_' . strtoupper($type))) {
  570. throw new Exception\InvalidArgumentException("Invalid or not supported authentication type: '$type'");
  571. }
  572. if (empty($user)) {
  573. throw new Exception\InvalidArgumentException("The username cannot be empty");
  574. }
  575. $this->auth = array (
  576. 'user' => $user,
  577. 'password' => $password,
  578. 'type' => $type
  579. );
  580. return $this;
  581. }
  582. /**
  583. * Calculate the response value according to the HTTP authentication type
  584. *
  585. * @see http://www.faqs.org/rfcs/rfc2617.html
  586. * @param string $user
  587. * @param string $password
  588. * @param string $type
  589. * @param array $digest
  590. * @param null|string $entityBody
  591. * @throws Exception\InvalidArgumentException
  592. * @return string|boolean
  593. */
  594. protected function calcAuthDigest($user, $password, $type = self::AUTH_BASIC, $digest = array(), $entityBody = null)
  595. {
  596. if (!defined('self::AUTH_' . strtoupper($type))) {
  597. throw new Exception\InvalidArgumentException("Invalid or not supported authentication type: '$type'");
  598. }
  599. $response = false;
  600. switch (strtolower($type)) {
  601. case self::AUTH_BASIC :
  602. // In basic authentication, the user name cannot contain ":"
  603. if (strpos($user, ':') !== false) {
  604. throw new Exception\InvalidArgumentException("The user name cannot contain ':' in Basic HTTP authentication");
  605. }
  606. $response = base64_encode($user . ':' . $password);
  607. break;
  608. case self::AUTH_DIGEST :
  609. if (empty($digest)) {
  610. throw new Exception\InvalidArgumentException("The digest cannot be empty");
  611. }
  612. foreach ($digest as $key => $value) {
  613. if (!defined('self::DIGEST_' . strtoupper($key))) {
  614. throw new Exception\InvalidArgumentException("Invalid or not supported digest authentication parameter: '$key'");
  615. }
  616. }
  617. $ha1 = md5($user . ':' . $digest['realm'] . ':' . $password);
  618. if (empty($digest['qop']) || strtolower($digest['qop']) == 'auth') {
  619. $ha2 = md5($this->getMethod() . ':' . $this->getUri()->getPath());
  620. } elseif (strtolower($digest['qop']) == 'auth-int') {
  621. if (empty($entityBody)) {
  622. throw new Exception\InvalidArgumentException("I cannot use the auth-int digest authentication without the entity body");
  623. }
  624. $ha2 = md5($this->getMethod() . ':' . $this->getUri()->getPath() . ':' . md5($entityBody));
  625. }
  626. if (empty($digest['qop'])) {
  627. $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $ha2);
  628. } else {
  629. $response = md5($ha1 . ':' . $digest['nonce'] . ':' . $digest['nc']
  630. . ':' . $digest['cnonce'] . ':' . $digest['qoc'] . ':' . $ha2);
  631. }
  632. break;
  633. }
  634. return $response;
  635. }
  636. /**
  637. * Reset all the HTTP parameters (auth,cookies,request, response, etc)
  638. *
  639. * @param bool $clearCookies Also clear all valid cookies? (defaults to false)
  640. * @return Client
  641. */
  642. public function resetParameters($clearCookies = false)
  643. {
  644. $uri = $this->getUri();
  645. $this->auth = null;
  646. $this->streamName = null;
  647. $this->encType = null;
  648. $this->request = null;
  649. $this->response = null;
  650. $this->setUri($uri);
  651. if ($clearCookies) {
  652. $this->clearCookies();
  653. }
  654. return $this;
  655. }
  656. /**
  657. * Dispatch
  658. *
  659. * @param Stdlib\RequestInterface $request
  660. * @param Stdlib\ResponseInterface $response
  661. * @return Stdlib\ResponseInterface
  662. */
  663. public function dispatch(Stdlib\RequestInterface $request, Stdlib\ResponseInterface $response = null)
  664. {
  665. $response = $this->send($request);
  666. return $response;
  667. }
  668. /**
  669. * Send HTTP request
  670. *
  671. * @param Request $request
  672. * @return Response
  673. * @throws Exception\RuntimeException
  674. * @throws Client\Exception\RuntimeException
  675. */
  676. public function send(Request $request = null)
  677. {
  678. if ($request !== null) {
  679. $this->setRequest($request);
  680. }
  681. $this->redirectCounter = 0;
  682. $response = null;
  683. // Make sure the adapter is loaded
  684. if ($this->adapter == null) {
  685. $this->setAdapter($this->config['adapter']);
  686. }
  687. // Send the first request. If redirected, continue.
  688. do {
  689. // uri
  690. $uri = $this->getUri();
  691. // query
  692. $query = $this->getRequest()->getQuery();
  693. if (!empty($query)) {
  694. $queryArray = $query->toArray();
  695. if (!empty($queryArray)) {
  696. $newUri = $uri->toString();
  697. $queryString = http_build_query($query);
  698. if ($this->config['rfc3986strict']) {
  699. $queryString = str_replace('+', '%20', $queryString);
  700. }
  701. if (strpos($newUri, '?') !== false) {
  702. $newUri .= '&' . $queryString;
  703. } else {
  704. $newUri .= '?' . $queryString;
  705. }
  706. $uri = new Http($newUri);
  707. }
  708. }
  709. // If we have no ports, set the defaults
  710. if (!$uri->getPort()) {
  711. $uri->setPort($uri->getScheme() == 'https' ? 443 : 80);
  712. }
  713. // method
  714. $method = $this->getRequest()->getMethod();
  715. // body
  716. $body = $this->prepareBody();
  717. // headers
  718. $headers = $this->prepareHeaders($body, $uri);
  719. $secure = $uri->getScheme() == 'https';
  720. // cookies
  721. $cookie = $this->prepareCookies($uri->getHost(), $uri->getPath(), $secure);
  722. if ($cookie->getFieldValue()) {
  723. $headers['Cookie'] = $cookie->getFieldValue();
  724. }
  725. // check that adapter supports streaming before using it
  726. if (is_resource($body) && !($this->adapter instanceof Client\Adapter\StreamInterface)) {
  727. throw new Client\Exception\RuntimeException('Adapter does not support streaming');
  728. }
  729. // calling protected method to allow extending classes
  730. // to wrap the interaction with the adapter
  731. $response = $this->doRequest($uri, $method, $secure, $headers, $body);
  732. if (! $response) {
  733. throw new Exception\RuntimeException('Unable to read response, or response is empty');
  734. }
  735. if ($this->config['storeresponse']) {
  736. $this->lastRawResponse = $response;
  737. } else {
  738. $this->lastRawResponse = null;
  739. }
  740. if ($this->config['outputstream']) {
  741. $stream = $this->getStream();
  742. if (!is_resource($stream) && is_string($stream)) {
  743. $stream = fopen($stream, 'r');
  744. }
  745. $streamMetaData = stream_get_meta_data($stream);
  746. if ($streamMetaData['seekable']) {
  747. rewind($stream);
  748. }
  749. // cleanup the adapter
  750. $this->adapter->setOutputStream(null);
  751. $response = Response\Stream::fromStream($response, $stream);
  752. $response->setStreamName($this->streamName);
  753. if (!is_string($this->config['outputstream'])) {
  754. // we used temp name, will need to clean up
  755. $response->setCleanup(true);
  756. }
  757. } else {
  758. $response = Response::fromString($response);
  759. }
  760. // Get the cookies from response (if any)
  761. $setCookie = $response->getCookie();
  762. if (!empty($setCookie)) {
  763. $this->addCookie($setCookie);
  764. }
  765. // If we got redirected, look for the Location header
  766. if ($response->isRedirect() && ($response->getHeaders()->has('Location'))) {
  767. // Avoid problems with buggy servers that add whitespace at the
  768. // end of some headers
  769. $location = trim($response->getHeaders()->get('Location')->getFieldValue());
  770. // Check whether we send the exact same request again, or drop the parameters
  771. // and send a GET request
  772. if ($response->getStatusCode() == 303 ||
  773. ((! $this->config['strictredirects']) && ($response->getStatusCode() == 302 ||
  774. $response->getStatusCode() == 301))) {
  775. $this->resetParameters();
  776. $this->setMethod(Request::METHOD_GET);
  777. }
  778. // If we got a well formed absolute URI
  779. if (($scheme = substr($location, 0, 6)) &&
  780. ($scheme == 'http:/' || $scheme == 'https:')) {
  781. $this->setUri($location);
  782. } else {
  783. // Split into path and query and set the query
  784. if (strpos($location, '?') !== false) {
  785. list($location, $query) = explode('?', $location, 2);
  786. } else {
  787. $query = '';
  788. }
  789. $this->getUri()->setQuery($query);
  790. // Else, if we got just an absolute path, set it
  791. if (strpos($location, '/') === 0) {
  792. $this->getUri()->setPath($location);
  793. // Else, assume we have a relative path
  794. } else {
  795. // Get the current path directory, removing any trailing slashes
  796. $path = $this->getUri()->getPath();
  797. $path = rtrim(substr($path, 0, strrpos($path, '/')), "/");
  798. $this->getUri()->setPath($path . '/' . $location);
  799. }
  800. }
  801. ++$this->redirectCounter;
  802. } else {
  803. // If we didn't get any location, stop redirecting
  804. break;
  805. }
  806. } while ($this->redirectCounter < $this->config['maxredirects']);
  807. $this->response = $response;
  808. return $response;
  809. }
  810. /**
  811. * Set a file to upload (using a POST request)
  812. *
  813. * Can be used in two ways:
  814. *
  815. * 1. $data is null (default): $filename is treated as the name if a local file which
  816. * will be read and sent. Will try to guess the content type using mime_content_type().
  817. * 2. $data is set - $filename is sent as the file name, but $data is sent as the file
  818. * contents and no file is read from the file system. In this case, you need to
  819. * manually set the Content-Type ($ctype) or it will default to
  820. * application/octet-stream.
  821. *
  822. * @param string $filename Name of file to upload, or name to save as
  823. * @param string $formname Name of form element to send as
  824. * @param string $data Data to send (if null, $filename is read and sent)
  825. * @param string $ctype Content type to use (if $data is set and $ctype is
  826. * null, will be application/octet-stream)
  827. * @return Client
  828. * @throws Exception\RuntimeException
  829. */
  830. public function setFileUpload($filename, $formname, $data = null, $ctype = null)
  831. {
  832. if ($data === null) {
  833. ErrorHandler::start();
  834. $data = file_get_contents($filename);
  835. $error = ErrorHandler::stop();
  836. if ($data === false) {
  837. throw new Exception\RuntimeException("Unable to read file '{$filename}' for upload", 0, $error);
  838. }
  839. if (!$ctype) {
  840. $ctype = $this->detectFileMimeType($filename);
  841. }
  842. }
  843. $this->getRequest()->getFiles()->set($filename, array(
  844. 'formname' => $formname,
  845. 'filename' => basename($filename),
  846. 'ctype' => $ctype,
  847. 'data' => $data
  848. ));
  849. return $this;
  850. }
  851. /**
  852. * Remove a file to upload
  853. *
  854. * @param string $filename
  855. * @return boolean
  856. */
  857. public function removeFileUpload($filename)
  858. {
  859. $file = $this->getRequest()->getFiles()->get($filename);
  860. if (!empty($file)) {
  861. $this->getRequest()->getFiles()->set($filename, null);
  862. return true;
  863. }
  864. return false;
  865. }
  866. /**
  867. * Prepare Cookies
  868. *
  869. * @param string $domain
  870. * @param string $path
  871. * @param boolean $secure
  872. * @return Header\Cookie|boolean
  873. */
  874. protected function prepareCookies($domain, $path, $secure)
  875. {
  876. $validCookies = array();
  877. if (!empty($this->cookies)) {
  878. foreach ($this->cookies as $id => $cookie) {
  879. if ($cookie->isExpired()) {
  880. unset($this->cookies[$id]);
  881. continue;
  882. }
  883. if ($cookie->isValidForRequest($domain, $path, $secure)) {
  884. // OAM hack some domains try to set the cookie multiple times
  885. $validCookies[$cookie->getName()] = $cookie;
  886. }
  887. }
  888. }
  889. $cookies = Header\Cookie::fromSetCookieArray($validCookies);
  890. $cookies->setEncodeValue($this->config['encodecookies']);
  891. return $cookies;
  892. }
  893. /**
  894. * Prepare the request headers
  895. *
  896. * @param resource|string $body
  897. * @param Http $uri
  898. * @throws Exception\RuntimeException
  899. * @return array
  900. */
  901. protected function prepareHeaders($body, $uri)
  902. {
  903. $headers = array();
  904. // Set the host header
  905. if ($this->config['httpversion'] == Request::VERSION_11) {
  906. $host = $uri->getHost();
  907. // If the port is not default, add it
  908. if (!(($uri->getScheme() == 'http' && $uri->getPort() == 80) ||
  909. ($uri->getScheme() == 'https' && $uri->getPort() == 443))) {
  910. $host .= ':' . $uri->getPort();
  911. }
  912. $headers['Host'] = $host;
  913. }
  914. // Set the connection header
  915. if (!$this->getRequest()->getHeaders()->has('Connection')) {
  916. if (!$this->config['keepalive']) {
  917. $headers['Connection'] = 'close';
  918. }
  919. }
  920. // Set the Accept-encoding header if not set - depending on whether
  921. // zlib is available or not.
  922. if (!$this->getRequest()->getHeaders()->has('Accept-Encoding')) {
  923. if (function_exists('gzinflate')) {
  924. $headers['Accept-Encoding'] = 'gzip, deflate';
  925. } else {
  926. $headers['Accept-Encoding'] = 'identity';
  927. }
  928. }
  929. // Set the user agent header
  930. if (!$this->getRequest()->getHeaders()->has('User-Agent') && isset($this->config['useragent'])) {
  931. $headers['User-Agent'] = $this->config['useragent'];
  932. }
  933. // Set HTTP authentication if needed
  934. if (!empty($this->auth)) {
  935. switch ($this->auth['type']) {
  936. case self::AUTH_BASIC :
  937. $auth = $this->calcAuthDigest($this->auth['user'], $this->auth['password'], $this->auth['type']);
  938. if ($auth !== false) {
  939. $headers['Authorization'] = 'Basic ' . $auth;
  940. }
  941. break;
  942. case self::AUTH_DIGEST :
  943. throw new Exception\RuntimeException("The digest authentication is not implemented yet");
  944. }
  945. }
  946. // Content-type
  947. $encType = $this->getEncType();
  948. if (!empty($encType)) {
  949. $headers['Content-Type'] = $encType;
  950. }
  951. if (!empty($body)) {
  952. if (is_resource($body)) {
  953. $fstat = fstat($body);
  954. $headers['Content-Length'] = $fstat['size'];
  955. } else {
  956. $headers['Content-Length'] = strlen($body);
  957. }
  958. }
  959. // Merge the headers of the request (if any)
  960. $requestHeaders = $this->getRequest()->getHeaders()->toArray();
  961. foreach ($requestHeaders as $key => $value) {
  962. $headers[$key] = $value;
  963. }
  964. return $headers;
  965. }
  966. /**
  967. * Prepare the request body (for PATCH, POST and PUT requests)
  968. *
  969. * @return string
  970. * @throws \Zend\Http\Client\Exception\RuntimeException
  971. */
  972. protected function prepareBody()
  973. {
  974. // According to RFC2616, a TRACE request should not have a body.
  975. if ($this->getRequest()->isTrace()) {
  976. return '';
  977. }
  978. $rawBody = $this->getRequest()->getContent();
  979. if (!empty($rawBody)) {
  980. return $rawBody;
  981. }
  982. $body = '';
  983. $totalFiles = 0;
  984. if (!$this->getRequest()->getHeaders()->has('Content-Type')) {
  985. $totalFiles = count($this->getRequest()->getFiles()->toArray());
  986. // If we have files to upload, force encType to multipart/form-data
  987. if ($totalFiles > 0) {
  988. $this->setEncType(self::ENC_FORMDATA);
  989. }
  990. } else {
  991. $this->setEncType($this->getHeader('Content-Type'));
  992. }
  993. // If we have POST parameters or files, encode and add them to the body
  994. if (count($this->getRequest()->getPost()->toArray()) > 0 || $totalFiles > 0) {
  995. if (stripos($this->getEncType(), self::ENC_FORMDATA) === 0) {
  996. $boundary = '---ZENDHTTPCLIENT-' . md5(microtime());
  997. $this->setEncType(self::ENC_FORMDATA, $boundary);
  998. // Get POST parameters and encode them
  999. $params = self::flattenParametersArray($this->getRequest()->getPost()->toArray());
  1000. foreach ($params as $pp) {
  1001. $body .= $this->encodeFormData($boundary, $pp[0], $pp[1]);
  1002. }
  1003. // Encode files
  1004. foreach ($this->getRequest()->getFiles()->toArray() as $file) {
  1005. $fhead = array('Content-Type' => $file['ctype']);
  1006. $body .= $this->encodeFormData($boundary, $file['formname'], $file['data'], $file['filename'], $fhead);
  1007. }
  1008. $body .= "--{$boundary}--\r\n";
  1009. } elseif (stripos($this->getEncType(), self::ENC_URLENCODED) === 0) {
  1010. // Encode body as application/x-www-form-urlencoded
  1011. $body = http_build_query($this->getRequest()->getPost()->toArray());
  1012. } else {
  1013. throw new Client\Exception\RuntimeException("Cannot handle content type '{$this->encType}' automatically");
  1014. }
  1015. }
  1016. return $body;
  1017. }
  1018. /**
  1019. * Attempt to detect the MIME type of a file using available extensions
  1020. *
  1021. * This method will try to detect the MIME type of a file. If the fileinfo
  1022. * extension is available, it will be used. If not, the mime_magic
  1023. * extension which is deprecated but is still available in many PHP setups
  1024. * will be tried.
  1025. *
  1026. * If neither extension is available, the default application/octet-stream
  1027. * MIME type will be returned
  1028. *
  1029. * @param string $file File path
  1030. * @return string MIME type
  1031. */
  1032. protected function detectFileMimeType($file)
  1033. {
  1034. $type = null;
  1035. // First try with fileinfo functions
  1036. if (function_exists('finfo_open')) {
  1037. if (static::$fileInfoDb === null) {
  1038. ErrorHandler::start();
  1039. static::$fileInfoDb = finfo_open(FILEINFO_MIME);
  1040. ErrorHandler::stop();
  1041. }
  1042. if (static::$fileInfoDb) {
  1043. $type = finfo_file(static::$fileInfoDb, $file);
  1044. }
  1045. } elseif (function_exists('mime_content_type')) {
  1046. $type = mime_content_type($file);
  1047. }
  1048. // Fallback to the default application/octet-stream
  1049. if (! $type) {
  1050. $type = 'application/octet-stream';
  1051. }
  1052. return $type;
  1053. }
  1054. /**
  1055. * Encode data to a multipart/form-data part suitable for a POST request.
  1056. *
  1057. * @param string $boundary
  1058. * @param string $name
  1059. * @param mixed $value
  1060. * @param string $filename
  1061. * @param array $headers Associative array of optional headers @example ("Content-Transfer-Encoding" => "binary")
  1062. * @return string
  1063. */
  1064. public function encodeFormData($boundary, $name, $value, $filename = null, $headers = array())
  1065. {
  1066. $ret = "--{$boundary}\r\n" .
  1067. 'Content-Disposition: form-data; name="' . $name . '"';
  1068. if ($filename) {
  1069. $ret .= '; filename="' . $filename . '"';
  1070. }
  1071. $ret .= "\r\n";
  1072. foreach ($headers as $hname => $hvalue) {
  1073. $ret .= "{$hname}: {$hvalue}\r\n";
  1074. }
  1075. $ret .= "\r\n";
  1076. $ret .= "{$value}\r\n";
  1077. return $ret;
  1078. }
  1079. /**
  1080. * Convert an array of parameters into a flat array of (key, value) pairs
  1081. *
  1082. * Will flatten a potentially multi-dimentional array of parameters (such
  1083. * as POST parameters) into a flat array of (key, value) paris. In case
  1084. * of multi-dimentional arrays, square brackets ([]) will be added to the
  1085. * key to indicate an array.
  1086. *
  1087. * @since 1.9
  1088. *
  1089. * @param array $parray
  1090. * @param string $prefix
  1091. * @return array
  1092. */
  1093. protected function flattenParametersArray($parray, $prefix = null)
  1094. {
  1095. if (!is_array($parray)) {
  1096. return $parray;
  1097. }
  1098. $parameters = array();
  1099. foreach ($parray as $name => $value) {
  1100. // Calculate array key
  1101. if ($prefix) {
  1102. if (is_int($name)) {
  1103. $key = $prefix . '[]';
  1104. } else {
  1105. $key = $prefix . "[$name]";
  1106. }
  1107. } else {
  1108. $key = $name;
  1109. }
  1110. if (is_array($value)) {
  1111. $parameters = array_merge($parameters, $this->flattenParametersArray($value, $key));
  1112. } else {
  1113. $parameters[] = array($key, $value);
  1114. }
  1115. }
  1116. return $parameters;
  1117. }
  1118. /**
  1119. * Separating this from send method allows subclasses to wrap
  1120. * the interaction with the adapter
  1121. *
  1122. * @param Http $uri
  1123. * @param string $method
  1124. * @param boolean $secure
  1125. * @param array $headers
  1126. * @param string $body
  1127. * @return string the raw response
  1128. * @throws Exception\RuntimeException
  1129. */
  1130. protected function doRequest(Http $uri, $method, $secure = false, $headers = array(), $body = '')
  1131. {
  1132. // Open the connection, send the request and read the response
  1133. $this->adapter->connect($uri->getHost(), $uri->getPort(), $secure);
  1134. if ($this->config['outputstream']) {
  1135. if ($this->adapter instanceof Client\Adapter\StreamInterface) {
  1136. $stream = $this->openTempStream();
  1137. $this->adapter->setOutputStream($stream);
  1138. } else {
  1139. throw new Exception\RuntimeException('Adapter does not support streaming');
  1140. }
  1141. }
  1142. // HTTP connection
  1143. $this->lastRawRequest = $this->adapter->write($method,
  1144. $uri, $this->config['httpversion'], $headers, $body);
  1145. return $this->adapter->read();
  1146. }
  1147. /**
  1148. * Create a HTTP authentication "Authorization:" header according to the
  1149. * specified user, password and authentication method.
  1150. *
  1151. * @see http://www.faqs.org/rfcs/rfc2617.html
  1152. * @param string $user
  1153. * @param string $password
  1154. * @param string $type
  1155. * @return string
  1156. * @throws Zend\Http\Client\Exception\InvalidArgumentException
  1157. */
  1158. public static function encodeAuthHeader($user, $password, $type = self::AUTH_BASIC)
  1159. {
  1160. $authHeader = null;
  1161. switch ($type) {
  1162. case self::AUTH_BASIC:
  1163. // In basic authentication, the user name cannot contain ":"
  1164. if (strpos($user, ':') !== false) {
  1165. throw new Client\Exception\InvalidArgumentException("The user name cannot contain ':' in 'Basic' HTTP authentication");
  1166. }
  1167. $authHeader = 'Basic ' . base64_encode($user . ':' . $password);
  1168. break;
  1169. //case self::AUTH_DIGEST:
  1170. /**
  1171. * @todo Implement digest authentication
  1172. */
  1173. // break;
  1174. default:
  1175. throw new Client\Exception\InvalidArgumentException("Not a supported HTTP authentication type: '$type'");
  1176. }
  1177. return $authHeader;
  1178. }
  1179. }