Curl.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. <?php
  2. /**
  3. * Yii2 cURL wrapper
  4. * With RESTful support.
  5. *
  6. * @category Web-yii2
  7. * @package yii2-curl
  8. * @author Nils Gajsek <info@linslin.org>
  9. * @copyright 2013-2017 Nils Gajsek <info@linslin.org>
  10. * @license http://opensource.org/licenses/MIT MIT Public
  11. * @version 1.3.0
  12. * @link http://www.linslin.org
  13. *
  14. */
  15. namespace linslin\yii2\curl;
  16. use Yii;
  17. /**
  18. * Class Curl
  19. * @package linslin\yii2\curl
  20. */
  21. class Curl
  22. {
  23. // ################################################ class vars // ################################################
  24. /**
  25. * @var string|boolean
  26. * Holds response data right after sending a request.
  27. */
  28. public $response = null;
  29. /**
  30. * @var null|integer
  31. * Error code holder: https://curl.haxx.se/libcurl/c/libcurl-errors.html
  32. */
  33. public $errorCode = null;
  34. /**
  35. * @var null|string
  36. * Error text holder: http://php.net/manual/en/function.curl-strerror.php
  37. */
  38. public $errorText = null;
  39. /**
  40. * @var integer HTTP-Status Code
  41. * This value will hold HTTP-Status Code. False if request was not successful.
  42. */
  43. public $responseCode = null;
  44. /**
  45. * @var string|null HTTP Response Charset
  46. * (taken from Content-type header)
  47. */
  48. public $responseCharset = null;
  49. /**
  50. * @var int HTTP Response Length
  51. * (taken from Content-length header, or strlen() of downloaded content)
  52. */
  53. public $responseLength = -1;
  54. /**
  55. * @var string|null HTTP Response Content Type
  56. * (taken from Content-type header)
  57. */
  58. public $responseType = null;
  59. /**
  60. * @var array|null HTTP Response headers
  61. * Lists response header in an array if CURLOPT_HEADER is set to true.
  62. */
  63. public $responseHeaders = null;
  64. /**
  65. * @var array HTTP-Status Code
  66. * Custom options holder
  67. */
  68. protected $_options = [];
  69. /**
  70. * @var array
  71. * Hold array of get params to send with the request
  72. */
  73. protected $_getParams = [];
  74. /**
  75. * @var array
  76. * Hold array of post params to send with the request
  77. */
  78. protected $_postParams = [];
  79. /**
  80. * @var resource|null
  81. * Holds cURL-Handler
  82. */
  83. public $curl = null;
  84. /**
  85. * @var string
  86. * hold base URL
  87. */
  88. protected $_baseUrl = '';
  89. /**
  90. * @var array default curl options
  91. * Default curl options
  92. */
  93. protected $_defaultOptions = [
  94. CURLOPT_USERAGENT => 'Yii2-Curl-Agent',
  95. CURLOPT_TIMEOUT => 30,
  96. CURLOPT_CONNECTTIMEOUT => 30,
  97. CURLOPT_RETURNTRANSFER => true,
  98. CURLOPT_HEADER => true,
  99. ];
  100. // ############################################### class methods // ##############################################
  101. /**
  102. * Start performing GET-HTTP-Request
  103. *
  104. * @param string $url
  105. * @param boolean $raw if response body contains JSON and should be decoded
  106. *
  107. * @return mixed
  108. * @throws \Exception
  109. */
  110. public function get($url, $raw = true)
  111. {
  112. $this->_baseUrl = $url;
  113. return $this->_httpRequest('GET', $raw);
  114. }
  115. /**
  116. * Start performing HEAD-HTTP-Request
  117. *
  118. * @param string $url
  119. *
  120. * @return mixed
  121. * @throws \Exception
  122. */
  123. public function head($url)
  124. {
  125. $this->_baseUrl = $url;
  126. return $this->_httpRequest('HEAD');
  127. }
  128. /**
  129. * Start performing POST-HTTP-Request
  130. *
  131. * @param string $url
  132. * @param boolean $raw if response body contains JSON and should be decoded
  133. *
  134. * @return mixed
  135. * @throws \Exception
  136. */
  137. public function post($url, $raw = true)
  138. {
  139. $this->_baseUrl = $url;
  140. return $this->_httpRequest('POST', $raw);
  141. }
  142. /**
  143. * Start performing PUT-HTTP-Request
  144. *
  145. * @param string $url
  146. * @param boolean $raw if response body contains JSON and should be decoded
  147. *
  148. * @return mixed
  149. * @throws \Exception
  150. */
  151. public function put($url, $raw = true)
  152. {
  153. $this->_baseUrl = $url;
  154. return $this->_httpRequest('PUT', $raw);
  155. }
  156. /**
  157. * Start performing PATCH-HTTP-Request
  158. *
  159. * @param string $url
  160. * @param bool $raw if response body contains JSON and should be decoded
  161. *
  162. * @return mixed
  163. * @throws \Exception
  164. */
  165. public function patch($url, $raw = true)
  166. {
  167. $this->_baseUrl = $url;
  168. $this->setHeaders([
  169. 'X-HTTP-Method-Override' => 'PATCH'
  170. ]);
  171. return $this->_httpRequest('PATCH',$raw);
  172. }
  173. /**
  174. * Start performing DELETE-HTTP-Request
  175. *
  176. * @param string $url
  177. * @param boolean $raw if response body contains JSON and should be decoded
  178. *
  179. * @return mixed
  180. * @throws \Exception
  181. */
  182. public function delete($url, $raw = true)
  183. {
  184. $this->_baseUrl = $url;
  185. return $this->_httpRequest('DELETE', $raw);
  186. }
  187. /**
  188. * Set curl option
  189. *
  190. * @param string $key
  191. * @param mixed $value
  192. *
  193. * @return $this
  194. */
  195. public function setOption($key, $value)
  196. {
  197. //set value
  198. if (array_key_exists($key, $this->_defaultOptions) && $key !== CURLOPT_WRITEFUNCTION) {
  199. $this->_defaultOptions[$key] = $value;
  200. } else {
  201. $this->_options[$key] = $value;
  202. }
  203. //return self
  204. return $this;
  205. }
  206. /**
  207. * Set get params
  208. *
  209. * @param array $params
  210. * @return $this
  211. */
  212. public function setGetParams($params)
  213. {
  214. if (is_array($params)) {
  215. foreach ($params as $key => $value) {
  216. $this->_getParams[$key] = $value;
  217. }
  218. }
  219. //return self
  220. return $this;
  221. }
  222. /**
  223. * Set get params
  224. *
  225. * @param array $params
  226. * @return $this
  227. */
  228. public function setPostParams($params)
  229. {
  230. if (is_array($params)) {
  231. $this->setOption(
  232. CURLOPT_POSTFIELDS,
  233. http_build_query($params)
  234. );
  235. }
  236. //return self
  237. return $this;
  238. }
  239. /**
  240. * Set raw post data allows you to post any data format.
  241. *
  242. * @param mixed $data
  243. * @return $this
  244. */
  245. public function setRawPostData($data)
  246. {
  247. $this->setOption(
  248. CURLOPT_POSTFIELDS,
  249. $data
  250. );
  251. //return self
  252. return $this;
  253. }
  254. /**
  255. * Set get params
  256. *
  257. * @param string $data
  258. * @return $this
  259. */
  260. public function setRequestBody($data)
  261. {
  262. if (is_string($data)) {
  263. $this->setOption(
  264. CURLOPT_POSTFIELDS,
  265. $data
  266. );
  267. }
  268. //return self
  269. return $this;
  270. }
  271. /**
  272. * Get URL - return URL parsed with given params
  273. *
  274. * @return string The full URL with parsed get params
  275. */
  276. public function getUrl()
  277. {
  278. if (Count($this->_getParams) > 0) {
  279. return $this->_baseUrl.'?'.http_build_query($this->_getParams);
  280. } else {
  281. return $this->_baseUrl;
  282. }
  283. }
  284. /**
  285. * Set curl options
  286. *
  287. * @param array $options
  288. *
  289. * @return $this
  290. */
  291. public function setOptions($options)
  292. {
  293. $this->_options = $options + $this->_options;
  294. return $this;
  295. }
  296. /**
  297. * Set multiple headers for request.
  298. *
  299. * @param array $headers
  300. *
  301. * @return $this
  302. */
  303. public function setHeaders($headers)
  304. {
  305. if (is_array($headers)) {
  306. //init
  307. $parsedHeader = [];
  308. //collect currently set headers
  309. foreach ($this->getRequestHeaders() as $header => $value) {
  310. array_push($parsedHeader, $header.':'.$value);
  311. }
  312. //parse header into right format key:value
  313. foreach ($headers as $header => $value) {
  314. array_push($parsedHeader, $header.':'.$value);
  315. }
  316. //set headers
  317. $this->setOption(
  318. CURLOPT_HTTPHEADER,
  319. $parsedHeader
  320. );
  321. }
  322. return $this;
  323. }
  324. /**
  325. * Set a single header for request.
  326. *
  327. * @param string $header
  328. * @param string $value
  329. *
  330. * @return $this
  331. */
  332. public function setHeader($header, $value)
  333. {
  334. //init
  335. $parsedHeader = [];
  336. //collect currently set headers
  337. foreach ($this->getRequestHeaders() as $headerToSet => $valueToSet) {
  338. array_push($parsedHeader, $headerToSet.':'.$valueToSet);
  339. }
  340. //add override new header
  341. if (strlen($header) > 0) {
  342. array_push($parsedHeader, $header.':'.$value);
  343. }
  344. //set headers
  345. $this->setOption(
  346. CURLOPT_HTTPHEADER,
  347. $parsedHeader
  348. );
  349. return $this;
  350. }
  351. /**
  352. * Unset a single header.
  353. *
  354. * @param string $header
  355. *
  356. * @return $this
  357. */
  358. public function unsetHeader($header)
  359. {
  360. //init
  361. $parsedHeader = [];
  362. //collect currently set headers and filter "unset" header param.
  363. foreach ($this->getRequestHeaders() as $headerToSet => $valueToSet) {
  364. if ($header !== $headerToSet) {
  365. array_push($parsedHeader, $headerToSet.':'.$valueToSet);
  366. }
  367. }
  368. //set headers
  369. $this->setOption(
  370. CURLOPT_HTTPHEADER,
  371. $parsedHeader
  372. );
  373. return $this;
  374. }
  375. /**
  376. * Get all request headers as key:value array
  377. *
  378. * @return array
  379. */
  380. public function getRequestHeaders()
  381. {
  382. //Init
  383. $requestHeaders = $this->getOption(CURLOPT_HTTPHEADER);
  384. $parsedRequestHeaders = [];
  385. if (is_array($requestHeaders)) {
  386. foreach ($requestHeaders as $headerValue) {
  387. list ($key, $value) = explode(':', $headerValue, 2);
  388. $parsedRequestHeaders[$key] = $value;
  389. }
  390. }
  391. return $parsedRequestHeaders;
  392. }
  393. /**
  394. * Get specific request header as key:value array
  395. *
  396. * @param string $headerKey
  397. *
  398. * @return string|null
  399. */
  400. public function getRequestHeader($headerKey)
  401. {
  402. //Init
  403. $parsedRequestHeaders = $this->getRequestHeaders();
  404. return isset($parsedRequestHeaders[$headerKey]) ? $parsedRequestHeaders[$headerKey] : null;
  405. }
  406. /**
  407. * Unset a single curl option
  408. *
  409. * @param string $key
  410. *
  411. * @return $this
  412. */
  413. public function unsetOption($key)
  414. {
  415. //reset a single option if its set already
  416. if (isset($this->_options[$key])) {
  417. unset($this->_options[$key]);
  418. }
  419. return $this;
  420. }
  421. /**
  422. * Unset all curl option, excluding default options.
  423. *
  424. * @return $this
  425. */
  426. public function unsetOptions()
  427. {
  428. //reset all options
  429. if (isset($this->_options)) {
  430. $this->_options = [];
  431. }
  432. return $this;
  433. }
  434. /**
  435. * Total reset of options, responses, etc.
  436. *
  437. * @return $this
  438. */
  439. public function reset()
  440. {
  441. if ($this->curl !== null) {
  442. curl_close($this->curl); //stop curl
  443. }
  444. //reset all options
  445. if (isset($this->_options)) {
  446. $this->_options = [];
  447. }
  448. //reset response & status params
  449. $this->curl = null;
  450. $this->errorCode = null;
  451. $this->response = null;
  452. $this->responseCode = null;
  453. $this->responseCharset = null;
  454. $this->responseLength = -1;
  455. $this->responseType = null;
  456. $this->errorText = null;
  457. $this->_postParams = [];
  458. $this->_getParams = [];
  459. return $this;
  460. }
  461. /**
  462. * Return a single option
  463. *
  464. * @param string|integer $key
  465. * @return mixed|boolean
  466. */
  467. public function getOption($key)
  468. {
  469. //get merged options depends on default and user options
  470. $mergesOptions = $this->getOptions();
  471. //return value or false if key is not set.
  472. return isset($mergesOptions[$key]) ? $mergesOptions[$key] : false;
  473. }
  474. /**
  475. * Return merged curl options and keep keys!
  476. *
  477. * @return array
  478. */
  479. public function getOptions()
  480. {
  481. return $this->_options + $this->_defaultOptions;
  482. }
  483. /**
  484. * Get curl info according to http://php.net/manual/de/function.curl-getinfo.php
  485. *
  486. * @param null $opt
  487. * @return array|mixed
  488. */
  489. public function getInfo($opt = null)
  490. {
  491. if ($this->curl !== null && $opt === null) {
  492. return curl_getinfo($this->curl);
  493. } elseif ($this->curl !== null && $opt !== null) {
  494. return curl_getinfo($this->curl, $opt);
  495. } else {
  496. return [];
  497. }
  498. }
  499. /**
  500. * Performs HTTP request
  501. *
  502. * @param string $method
  503. * @param boolean $raw if response body contains JSON and should be decoded -> helper.
  504. *
  505. * @throws \Exception if request failed
  506. *
  507. * @return mixed
  508. */
  509. protected function _httpRequest($method, $raw = false)
  510. {
  511. //set request type and writer function
  512. $this->setOption(CURLOPT_CUSTOMREQUEST, strtoupper($method));
  513. //check if method is head and set no body
  514. if ($method === 'HEAD') {
  515. $this->setOption(CURLOPT_NOBODY, true);
  516. $this->unsetOption(CURLOPT_WRITEFUNCTION);
  517. }
  518. //setup error reporting and profiling
  519. if (YII_DEBUG) {
  520. Yii::trace('Start sending cURL-Request: '.$this->getUrl().'\n', __METHOD__);
  521. Yii::beginProfile($method.' '.$this->_baseUrl.'#'.md5(serialize($this->getOption(CURLOPT_POSTFIELDS))), __METHOD__);
  522. }
  523. /**
  524. * proceed curl
  525. */
  526. $curlOptions = $this->getOptions();
  527. $this->curl = curl_init($this->getUrl());
  528. curl_setopt_array($this->curl, $curlOptions);
  529. $response = curl_exec($this->curl);
  530. //check if curl was successful
  531. if ($response === false) {
  532. //set error code
  533. $this->errorCode = curl_errno($this->curl);
  534. $this->errorText = curl_strerror($this->errorCode);
  535. switch ($this->errorCode) {
  536. // 7, 28 = timeout
  537. case 7:
  538. case 28:
  539. $this->responseCode = 'timeout';
  540. return false;
  541. break;
  542. default:
  543. return false;
  544. break;
  545. }
  546. }
  547. //extract header / body data if CURLOPT_HEADER are set to true
  548. if (isset($curlOptions[CURLOPT_HEADER]) && $curlOptions[CURLOPT_HEADER]) {
  549. $this->response = $this->_extractCurlBody($response);
  550. $this->responseHeaders = $this->_extractCurlHeaders($response);
  551. } else {
  552. $this->response = $response;
  553. }
  554. // Extract additional curl params
  555. $this->_extractAdditionalCurlParameter();
  556. //end yii debug profile
  557. if (YII_DEBUG) {
  558. Yii::endProfile($method.' '.$this->getUrl().'#'.md5(serialize($this->getOption(CURLOPT_POSTFIELDS))), __METHOD__);
  559. }
  560. //check responseCode and return data/status
  561. if ($this->getOption(CURLOPT_CUSTOMREQUEST) === 'HEAD') {
  562. return true;
  563. } else {
  564. $this->response = $raw ? $this->response : json_decode($this->response, true);
  565. return $this->response;
  566. }
  567. }
  568. /**
  569. * Extract additional curl params protected class helper
  570. */
  571. protected function _extractAdditionalCurlParameter ()
  572. {
  573. /**
  574. * retrieve response code
  575. */
  576. $this->responseCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE);
  577. /**
  578. * try extract response type & charset.
  579. */
  580. $this->responseType = curl_getinfo($this->curl, CURLINFO_CONTENT_TYPE);
  581. if (!is_null($this->responseType) && count(explode(';', $this->responseType)) > 1) {
  582. list($this->responseType, $possibleCharset) = explode(';', $this->responseType);
  583. //extract charset
  584. if (preg_match('~^charset=(.+?)$~', trim($possibleCharset), $matches) && isset($matches[1])) {
  585. $this->responseCharset = strtolower($matches[1]);
  586. }
  587. }
  588. /**
  589. * try extract response length
  590. */
  591. $this->responseLength = curl_getinfo($this->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
  592. if((int)$this->responseLength === -1) {
  593. $this->responseLength = strlen($this->response);
  594. }
  595. }
  596. /**
  597. * Extract body curl data from response
  598. *
  599. * @param string $response
  600. * @return string
  601. */
  602. protected function _extractCurlBody ($response)
  603. {
  604. return substr($response, $this->getInfo(CURLINFO_HEADER_SIZE));
  605. }
  606. /**
  607. * Extract header curl data from response
  608. *
  609. * @param string $response
  610. * @return array
  611. */
  612. protected function _extractCurlHeaders ($response)
  613. {
  614. //Init
  615. $headers = [];
  616. $headerText = substr($response, 0, strpos($response, "\r\n\r\n"));
  617. foreach (explode("\r\n", $headerText) as $i => $line) {
  618. if ($i === 0) {
  619. $headers['http_code'] = $line;
  620. } else {
  621. list ($key, $value) = explode(':', $line, 2);
  622. $headers[$key] = ltrim($value);
  623. }
  624. }
  625. return $headers;
  626. }
  627. }