Client.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. <?php
  2. namespace Guzzle\Http;
  3. use Guzzle\Common\Collection;
  4. use Guzzle\Common\AbstractHasDispatcher;
  5. use Guzzle\Common\Exception\ExceptionCollection;
  6. use Guzzle\Common\Exception\InvalidArgumentException;
  7. use Guzzle\Common\Exception\RuntimeException;
  8. use Guzzle\Common\Version;
  9. use Guzzle\Parser\ParserRegistry;
  10. use Guzzle\Parser\UriTemplate\UriTemplateInterface;
  11. use Guzzle\Http\Message\RequestInterface;
  12. use Guzzle\Http\Message\RequestFactory;
  13. use Guzzle\Http\Message\RequestFactoryInterface;
  14. use Guzzle\Http\Curl\CurlMultiInterface;
  15. use Guzzle\Http\Curl\CurlMultiProxy;
  16. use Guzzle\Http\Curl\CurlHandle;
  17. use Guzzle\Http\Curl\CurlVersion;
  18. /**
  19. * HTTP client
  20. */
  21. class Client extends AbstractHasDispatcher implements ClientInterface
  22. {
  23. /** @deprecated Use [request.options][params] */
  24. const REQUEST_PARAMS = 'request.params';
  25. const REQUEST_OPTIONS = 'request.options';
  26. const CURL_OPTIONS = 'curl.options';
  27. const SSL_CERT_AUTHORITY = 'ssl.certificate_authority';
  28. const DISABLE_REDIRECTS = RedirectPlugin::DISABLE;
  29. const DEFAULT_SELECT_TIMEOUT = 1.0;
  30. const MAX_HANDLES = 3;
  31. /** @var Collection Default HTTP headers to set on each request */
  32. protected $defaultHeaders;
  33. /** @var string The user agent string to set on each request */
  34. protected $userAgent;
  35. /** @var Collection Parameter object holding configuration data */
  36. private $config;
  37. /** @var Url Base URL of the client */
  38. private $baseUrl;
  39. /** @var CurlMultiInterface CurlMulti object used internally */
  40. private $curlMulti;
  41. /** @var UriTemplateInterface URI template owned by the client */
  42. private $uriTemplate;
  43. /** @var RequestFactoryInterface Request factory used by the client */
  44. protected $requestFactory;
  45. public static function getAllEvents()
  46. {
  47. return array(self::CREATE_REQUEST);
  48. }
  49. /**
  50. * @param string $baseUrl Base URL of the web service
  51. * @param array|Collection $config Configuration settings
  52. *
  53. * @throws RuntimeException if cURL is not installed
  54. */
  55. public function __construct($baseUrl = '', $config = null)
  56. {
  57. if (!extension_loaded('curl')) {
  58. // @codeCoverageIgnoreStart
  59. throw new RuntimeException('The PHP cURL extension must be installed to use Guzzle.');
  60. // @codeCoverageIgnoreEnd
  61. }
  62. $this->setConfig($config ?: new Collection());
  63. $this->initSsl();
  64. $this->setBaseUrl($baseUrl);
  65. $this->defaultHeaders = new Collection();
  66. $this->setRequestFactory(RequestFactory::getInstance());
  67. $this->userAgent = $this->getDefaultUserAgent();
  68. if (!$this->config[self::DISABLE_REDIRECTS]) {
  69. $this->addSubscriber(new RedirectPlugin());
  70. }
  71. }
  72. final public function setConfig($config)
  73. {
  74. if ($config instanceof Collection) {
  75. $this->config = $config;
  76. } elseif (is_array($config)) {
  77. $this->config = new Collection($config);
  78. } else {
  79. throw new InvalidArgumentException('Config must be an array or Collection');
  80. }
  81. return $this;
  82. }
  83. final public function getConfig($key = false)
  84. {
  85. return $key ? $this->config[$key] : $this->config;
  86. }
  87. /**
  88. * Set a default request option on the client that will be used as a default for each request
  89. *
  90. * @param string $keyOrPath request.options key (e.g. allow_redirects) or path to a nested key (e.g. headers/foo)
  91. * @param mixed $value Value to set
  92. *
  93. * @return $this
  94. */
  95. public function setDefaultOption($keyOrPath, $value)
  96. {
  97. $keyOrPath = self::REQUEST_OPTIONS . '/' . $keyOrPath;
  98. $this->config->setPath($keyOrPath, $value);
  99. return $this;
  100. }
  101. /**
  102. * Retrieve a default request option from the client
  103. *
  104. * @param string $keyOrPath request.options key (e.g. allow_redirects) or path to a nested key (e.g. headers/foo)
  105. *
  106. * @return mixed|null
  107. */
  108. public function getDefaultOption($keyOrPath)
  109. {
  110. $keyOrPath = self::REQUEST_OPTIONS . '/' . $keyOrPath;
  111. return $this->config->getPath($keyOrPath);
  112. }
  113. final public function setSslVerification($certificateAuthority = true, $verifyPeer = true, $verifyHost = 2)
  114. {
  115. $opts = $this->config[self::CURL_OPTIONS] ?: array();
  116. if ($certificateAuthority === true) {
  117. // use bundled CA bundle, set secure defaults
  118. $opts[CURLOPT_CAINFO] = __DIR__ . '/Resources/cacert.pem';
  119. $opts[CURLOPT_SSL_VERIFYPEER] = true;
  120. $opts[CURLOPT_SSL_VERIFYHOST] = 2;
  121. } elseif ($certificateAuthority === false) {
  122. unset($opts[CURLOPT_CAINFO]);
  123. $opts[CURLOPT_SSL_VERIFYPEER] = false;
  124. $opts[CURLOPT_SSL_VERIFYHOST] = 0;
  125. } elseif ($verifyPeer !== true && $verifyPeer !== false && $verifyPeer !== 1 && $verifyPeer !== 0) {
  126. throw new InvalidArgumentException('verifyPeer must be 1, 0 or boolean');
  127. } elseif ($verifyHost !== 0 && $verifyHost !== 1 && $verifyHost !== 2) {
  128. throw new InvalidArgumentException('verifyHost must be 0, 1 or 2');
  129. } else {
  130. $opts[CURLOPT_SSL_VERIFYPEER] = $verifyPeer;
  131. $opts[CURLOPT_SSL_VERIFYHOST] = $verifyHost;
  132. if (is_file($certificateAuthority)) {
  133. unset($opts[CURLOPT_CAPATH]);
  134. $opts[CURLOPT_CAINFO] = $certificateAuthority;
  135. } elseif (is_dir($certificateAuthority)) {
  136. unset($opts[CURLOPT_CAINFO]);
  137. $opts[CURLOPT_CAPATH] = $certificateAuthority;
  138. } else {
  139. throw new RuntimeException(
  140. 'Invalid option passed to ' . self::SSL_CERT_AUTHORITY . ': ' . $certificateAuthority
  141. );
  142. }
  143. }
  144. $this->config->set(self::CURL_OPTIONS, $opts);
  145. return $this;
  146. }
  147. public function createRequest($method = 'GET', $uri = null, $headers = null, $body = null, array $options = array())
  148. {
  149. if (!$uri) {
  150. $url = $this->getBaseUrl();
  151. } else {
  152. if (!is_array($uri)) {
  153. $templateVars = null;
  154. } else {
  155. list($uri, $templateVars) = $uri;
  156. }
  157. if (strpos($uri, '://')) {
  158. // Use absolute URLs as-is
  159. $url = $this->expandTemplate($uri, $templateVars);
  160. } else {
  161. $url = Url::factory($this->getBaseUrl())->combine($this->expandTemplate($uri, $templateVars));
  162. }
  163. }
  164. // If default headers are provided, then merge them under any explicitly provided headers for the request
  165. if (count($this->defaultHeaders)) {
  166. if (!$headers) {
  167. $headers = $this->defaultHeaders->toArray();
  168. } elseif (is_array($headers)) {
  169. $headers += $this->defaultHeaders->toArray();
  170. } elseif ($headers instanceof Collection) {
  171. $headers = $headers->toArray() + $this->defaultHeaders->toArray();
  172. }
  173. }
  174. return $this->prepareRequest($this->requestFactory->create($method, (string) $url, $headers, $body), $options);
  175. }
  176. public function getBaseUrl($expand = true)
  177. {
  178. return $expand ? $this->expandTemplate($this->baseUrl) : $this->baseUrl;
  179. }
  180. public function setBaseUrl($url)
  181. {
  182. $this->baseUrl = $url;
  183. return $this;
  184. }
  185. public function setUserAgent($userAgent, $includeDefault = false)
  186. {
  187. if ($includeDefault) {
  188. $userAgent .= ' ' . $this->getDefaultUserAgent();
  189. }
  190. $this->userAgent = $userAgent;
  191. return $this;
  192. }
  193. /**
  194. * Get the default User-Agent string to use with Guzzle
  195. *
  196. * @return string
  197. */
  198. public function getDefaultUserAgent()
  199. {
  200. return 'Guzzle/' . Version::VERSION
  201. . ' curl/' . CurlVersion::getInstance()->get('version')
  202. . ' PHP/' . PHP_VERSION;
  203. }
  204. public function get($uri = null, $headers = null, $options = array())
  205. {
  206. // BC compat: $options can be a string, resource, etc to specify where the response body is downloaded
  207. return is_array($options)
  208. ? $this->createRequest('GET', $uri, $headers, null, $options)
  209. : $this->createRequest('GET', $uri, $headers, $options);
  210. }
  211. public function head($uri = null, $headers = null, array $options = array())
  212. {
  213. return $this->createRequest('HEAD', $uri, $headers, null, $options);
  214. }
  215. public function delete($uri = null, $headers = null, $body = null, array $options = array())
  216. {
  217. return $this->createRequest('DELETE', $uri, $headers, $body, $options);
  218. }
  219. public function put($uri = null, $headers = null, $body = null, array $options = array())
  220. {
  221. return $this->createRequest('PUT', $uri, $headers, $body, $options);
  222. }
  223. public function patch($uri = null, $headers = null, $body = null, array $options = array())
  224. {
  225. return $this->createRequest('PATCH', $uri, $headers, $body, $options);
  226. }
  227. public function post($uri = null, $headers = null, $postBody = null, array $options = array())
  228. {
  229. return $this->createRequest('POST', $uri, $headers, $postBody, $options);
  230. }
  231. public function options($uri = null, array $options = array())
  232. {
  233. return $this->createRequest('OPTIONS', $uri, $options);
  234. }
  235. public function send($requests)
  236. {
  237. if (!($requests instanceof RequestInterface)) {
  238. return $this->sendMultiple($requests);
  239. }
  240. try {
  241. /** @var $requests RequestInterface */
  242. $this->getCurlMulti()->add($requests)->send();
  243. return $requests->getResponse();
  244. } catch (ExceptionCollection $e) {
  245. throw $e->getFirst();
  246. }
  247. }
  248. /**
  249. * Set a curl multi object to be used internally by the client for transferring requests.
  250. *
  251. * @param CurlMultiInterface $curlMulti Multi object
  252. *
  253. * @return self
  254. */
  255. public function setCurlMulti(CurlMultiInterface $curlMulti)
  256. {
  257. $this->curlMulti = $curlMulti;
  258. return $this;
  259. }
  260. /**
  261. * @return CurlMultiInterface|CurlMultiProxy
  262. */
  263. public function getCurlMulti()
  264. {
  265. if (!$this->curlMulti) {
  266. $this->curlMulti = new CurlMultiProxy(
  267. self::MAX_HANDLES,
  268. $this->getConfig('select_timeout') ?: self::DEFAULT_SELECT_TIMEOUT
  269. );
  270. }
  271. return $this->curlMulti;
  272. }
  273. public function setRequestFactory(RequestFactoryInterface $factory)
  274. {
  275. $this->requestFactory = $factory;
  276. return $this;
  277. }
  278. /**
  279. * Set the URI template expander to use with the client
  280. *
  281. * @param UriTemplateInterface $uriTemplate URI template expander
  282. *
  283. * @return self
  284. */
  285. public function setUriTemplate(UriTemplateInterface $uriTemplate)
  286. {
  287. $this->uriTemplate = $uriTemplate;
  288. return $this;
  289. }
  290. /**
  291. * Expand a URI template while merging client config settings into the template variables
  292. *
  293. * @param string $template Template to expand
  294. * @param array $variables Variables to inject
  295. *
  296. * @return string
  297. */
  298. protected function expandTemplate($template, array $variables = null)
  299. {
  300. $expansionVars = $this->getConfig()->toArray();
  301. if ($variables) {
  302. $expansionVars = $variables + $expansionVars;
  303. }
  304. return $this->getUriTemplate()->expand($template, $expansionVars);
  305. }
  306. /**
  307. * Get the URI template expander used by the client
  308. *
  309. * @return UriTemplateInterface
  310. */
  311. protected function getUriTemplate()
  312. {
  313. if (!$this->uriTemplate) {
  314. $this->uriTemplate = ParserRegistry::getInstance()->getParser('uri_template');
  315. }
  316. return $this->uriTemplate;
  317. }
  318. /**
  319. * Send multiple requests in parallel
  320. *
  321. * @param array $requests Array of RequestInterface objects
  322. *
  323. * @return array Returns an array of Response objects
  324. */
  325. protected function sendMultiple(array $requests)
  326. {
  327. $curlMulti = $this->getCurlMulti();
  328. foreach ($requests as $request) {
  329. $curlMulti->add($request);
  330. }
  331. $curlMulti->send();
  332. /** @var $request RequestInterface */
  333. $result = array();
  334. foreach ($requests as $request) {
  335. $result[] = $request->getResponse();
  336. }
  337. return $result;
  338. }
  339. /**
  340. * Prepare a request to be sent from the Client by adding client specific behaviors and properties to the request.
  341. *
  342. * @param RequestInterface $request Request to prepare for the client
  343. * @param array $options Options to apply to the request
  344. *
  345. * @return RequestInterface
  346. */
  347. protected function prepareRequest(RequestInterface $request, array $options = array())
  348. {
  349. $request->setClient($this)->setEventDispatcher(clone $this->getEventDispatcher());
  350. if ($curl = $this->config[self::CURL_OPTIONS]) {
  351. $request->getCurlOptions()->overwriteWith(CurlHandle::parseCurlConfig($curl));
  352. }
  353. if ($params = $this->config[self::REQUEST_PARAMS]) {
  354. Version::warn('request.params is deprecated. Use request.options to add default request options.');
  355. $request->getParams()->overwriteWith($params);
  356. }
  357. if ($this->userAgent && !$request->hasHeader('User-Agent')) {
  358. $request->setHeader('User-Agent', $this->userAgent);
  359. }
  360. if ($defaults = $this->config[self::REQUEST_OPTIONS]) {
  361. $this->requestFactory->applyOptions($request, $defaults, RequestFactoryInterface::OPTIONS_AS_DEFAULTS);
  362. }
  363. if ($options) {
  364. $this->requestFactory->applyOptions($request, $options);
  365. }
  366. $this->dispatch('client.create_request', array('client' => $this, 'request' => $request));
  367. return $request;
  368. }
  369. /**
  370. * Initializes SSL settings
  371. */
  372. protected function initSsl()
  373. {
  374. $authority = $this->config[self::SSL_CERT_AUTHORITY];
  375. if ($authority === 'system') {
  376. return;
  377. }
  378. if ($authority === null) {
  379. $authority = true;
  380. }
  381. if ($authority === true && substr(__FILE__, 0, 7) == 'phar://') {
  382. $authority = self::extractPharCacert(__DIR__ . '/Resources/cacert.pem');
  383. }
  384. $this->setSslVerification($authority);
  385. }
  386. /**
  387. * @deprecated
  388. */
  389. public function getDefaultHeaders()
  390. {
  391. Version::warn(__METHOD__ . ' is deprecated. Use the request.options array to retrieve default request options');
  392. return $this->defaultHeaders;
  393. }
  394. /**
  395. * @deprecated
  396. */
  397. public function setDefaultHeaders($headers)
  398. {
  399. Version::warn(__METHOD__ . ' is deprecated. Use the request.options array to specify default request options');
  400. if ($headers instanceof Collection) {
  401. $this->defaultHeaders = $headers;
  402. } elseif (is_array($headers)) {
  403. $this->defaultHeaders = new Collection($headers);
  404. } else {
  405. throw new InvalidArgumentException('Headers must be an array or Collection');
  406. }
  407. return $this;
  408. }
  409. /**
  410. * @deprecated
  411. */
  412. public function preparePharCacert($md5Check = true)
  413. {
  414. return sys_get_temp_dir() . '/guzzle-cacert.pem';
  415. }
  416. /**
  417. * Copies the phar cacert from a phar into the temp directory.
  418. *
  419. * @param string $pharCacertPath Path to the phar cacert. For example:
  420. * 'phar://aws.phar/Guzzle/Http/Resources/cacert.pem'
  421. *
  422. * @return string Returns the path to the extracted cacert file.
  423. * @throws \RuntimeException Throws if the phar cacert cannot be found or
  424. * the file cannot be copied to the temp dir.
  425. */
  426. public static function extractPharCacert($pharCacertPath)
  427. {
  428. // Copy the cacert.pem file from the phar if it is not in the temp
  429. // folder.
  430. $certFile = sys_get_temp_dir() . '/guzzle-cacert.pem';
  431. if (!file_exists($pharCacertPath)) {
  432. throw new \RuntimeException("Could not find $pharCacertPath");
  433. }
  434. if (!file_exists($certFile) ||
  435. filesize($certFile) != filesize($pharCacertPath)
  436. ) {
  437. if (!copy($pharCacertPath, $certFile)) {
  438. throw new \RuntimeException(
  439. "Could not copy {$pharCacertPath} to {$certFile}: "
  440. . var_export(error_get_last(), true)
  441. );
  442. }
  443. }
  444. return $certFile;
  445. }
  446. }