Curl.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  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. * Start performing OPTIONS-HTTP-Request
  189. *
  190. * @param string $url
  191. * @param bool $raw if response body contains JSON and should be decoded
  192. *
  193. * @return mixed
  194. * @throws \Exception
  195. */
  196. public function options($url, $raw = true)
  197. {
  198. $this->_baseUrl = $url;
  199. return $this->_httpRequest('OPTIONS', $raw);
  200. }
  201. /**
  202. * Set curl option
  203. *
  204. * @param string $key
  205. * @param mixed $value
  206. *
  207. * @return $this
  208. */
  209. public function setOption($key, $value)
  210. {
  211. //set value
  212. if (array_key_exists($key, $this->_defaultOptions) && $key !== CURLOPT_WRITEFUNCTION) {
  213. $this->_defaultOptions[$key] = $value;
  214. } else {
  215. $this->_options[$key] = $value;
  216. }
  217. //return self
  218. return $this;
  219. }
  220. /**
  221. * Set get params
  222. *
  223. * @param array $params
  224. * @return $this
  225. */
  226. public function setGetParams($params)
  227. {
  228. if (is_array($params)) {
  229. foreach ($params as $key => $value) {
  230. $this->_getParams[$key] = $value;
  231. }
  232. }
  233. //return self
  234. return $this;
  235. }
  236. /**
  237. * Set get params
  238. *
  239. * @param array $params
  240. * @return $this
  241. */
  242. public function setPostParams($params)
  243. {
  244. if (is_array($params)) {
  245. $this->setOption(
  246. CURLOPT_POSTFIELDS,
  247. http_build_query($params)
  248. );
  249. }
  250. //return self
  251. return $this;
  252. }
  253. /**
  254. * Set raw post data allows you to post any data format.
  255. *
  256. * @param mixed $data
  257. * @return $this
  258. */
  259. public function setRawPostData($data)
  260. {
  261. $this->setOption(
  262. CURLOPT_POSTFIELDS,
  263. $data
  264. );
  265. //return self
  266. return $this;
  267. }
  268. /**
  269. * Set get params
  270. *
  271. * @param string $data
  272. * @return $this
  273. */
  274. public function setRequestBody($data)
  275. {
  276. if (is_string($data)) {
  277. $this->setOption(
  278. CURLOPT_POSTFIELDS,
  279. $data
  280. );
  281. }
  282. //return self
  283. return $this;
  284. }
  285. /**
  286. * Get URL - return URL parsed with given params
  287. *
  288. * @return string The full URL with parsed get params
  289. */
  290. public function getUrl()
  291. {
  292. if (Count($this->_getParams) > 0) {
  293. return $this->_baseUrl.'?'.http_build_query($this->_getParams);
  294. } else {
  295. return $this->_baseUrl;
  296. }
  297. }
  298. /**
  299. * Set curl options
  300. *
  301. * @param array $options
  302. *
  303. * @return $this
  304. */
  305. public function setOptions($options)
  306. {
  307. $this->_options = $options + $this->_options;
  308. return $this;
  309. }
  310. /**
  311. * Set multiple headers for request.
  312. *
  313. * @param array $headers
  314. *
  315. * @return $this
  316. */
  317. public function setHeaders($headers)
  318. {
  319. if (is_array($headers)) {
  320. //init
  321. $parsedHeader = [];
  322. //collect currently set headers
  323. foreach ($this->getRequestHeaders() as $header => $value) {
  324. array_push($parsedHeader, $header.':'.$value);
  325. }
  326. //parse header into right format key:value
  327. foreach ($headers as $header => $value) {
  328. array_push($parsedHeader, $header.':'.$value);
  329. }
  330. //set headers
  331. $this->setOption(
  332. CURLOPT_HTTPHEADER,
  333. $parsedHeader
  334. );
  335. }
  336. return $this;
  337. }
  338. /**
  339. * Set a single header for request.
  340. *
  341. * @param string $header
  342. * @param string $value
  343. *
  344. * @return $this
  345. */
  346. public function setHeader($header, $value)
  347. {
  348. //init
  349. $parsedHeader = [];
  350. //collect currently set headers
  351. foreach ($this->getRequestHeaders() as $headerToSet => $valueToSet) {
  352. array_push($parsedHeader, $headerToSet.':'.$valueToSet);
  353. }
  354. //add override new header
  355. if (strlen($header) > 0) {
  356. array_push($parsedHeader, $header.':'.$value);
  357. }
  358. //set headers
  359. $this->setOption(
  360. CURLOPT_HTTPHEADER,
  361. $parsedHeader
  362. );
  363. return $this;
  364. }
  365. /**
  366. * Unset a single header.
  367. *
  368. * @param string $header
  369. *
  370. * @return $this
  371. */
  372. public function unsetHeader($header)
  373. {
  374. //init
  375. $parsedHeader = [];
  376. //collect currently set headers and filter "unset" header param.
  377. foreach ($this->getRequestHeaders() as $headerToSet => $valueToSet) {
  378. if ($header !== $headerToSet) {
  379. array_push($parsedHeader, $headerToSet.':'.$valueToSet);
  380. }
  381. }
  382. //set headers
  383. $this->setOption(
  384. CURLOPT_HTTPHEADER,
  385. $parsedHeader
  386. );
  387. return $this;
  388. }
  389. /**
  390. * Get all request headers as key:value array
  391. *
  392. * @return array
  393. */
  394. public function getRequestHeaders()
  395. {
  396. //Init
  397. $requestHeaders = $this->getOption(CURLOPT_HTTPHEADER);
  398. $parsedRequestHeaders = [];
  399. if (is_array($requestHeaders)) {
  400. foreach ($requestHeaders as $headerValue) {
  401. list ($key, $value) = explode(':', $headerValue, 2);
  402. $parsedRequestHeaders[$key] = $value;
  403. }
  404. }
  405. return $parsedRequestHeaders;
  406. }
  407. /**
  408. * Get specific request header as key:value array
  409. *
  410. * @param string $headerKey
  411. *
  412. * @return string|null
  413. */
  414. public function getRequestHeader($headerKey)
  415. {
  416. //Init
  417. $parsedRequestHeaders = $this->getRequestHeaders();
  418. return isset($parsedRequestHeaders[$headerKey]) ? $parsedRequestHeaders[$headerKey] : null;
  419. }
  420. /**
  421. * Unset a single curl option
  422. *
  423. * @param string $key
  424. *
  425. * @return $this
  426. */
  427. public function unsetOption($key)
  428. {
  429. //reset a single option if its set already
  430. if (isset($this->_options[$key])) {
  431. unset($this->_options[$key]);
  432. }
  433. return $this;
  434. }
  435. /**
  436. * Unset all curl option, excluding default options.
  437. *
  438. * @return $this
  439. */
  440. public function unsetOptions()
  441. {
  442. //reset all options
  443. if (isset($this->_options)) {
  444. $this->_options = [];
  445. }
  446. return $this;
  447. }
  448. /**
  449. * Total reset of options, responses, etc.
  450. *
  451. * @return $this
  452. */
  453. public function reset()
  454. {
  455. if ($this->curl !== null) {
  456. curl_close($this->curl); //stop curl
  457. }
  458. //reset all options
  459. if (isset($this->_options)) {
  460. $this->_options = [];
  461. }
  462. //reset response & status params
  463. $this->curl = null;
  464. $this->errorCode = null;
  465. $this->response = null;
  466. $this->responseCode = null;
  467. $this->responseCharset = null;
  468. $this->responseLength = -1;
  469. $this->responseType = null;
  470. $this->errorText = null;
  471. $this->_postParams = [];
  472. $this->_getParams = [];
  473. return $this;
  474. }
  475. /**
  476. * Return a single option
  477. *
  478. * @param string|integer $key
  479. * @return mixed|boolean
  480. */
  481. public function getOption($key)
  482. {
  483. //get merged options depends on default and user options
  484. $mergesOptions = $this->getOptions();
  485. //return value or false if key is not set.
  486. return isset($mergesOptions[$key]) ? $mergesOptions[$key] : false;
  487. }
  488. /**
  489. * Return merged curl options and keep keys!
  490. *
  491. * @return array
  492. */
  493. public function getOptions()
  494. {
  495. return $this->_options + $this->_defaultOptions;
  496. }
  497. /**
  498. * Get curl info according to http://php.net/manual/de/function.curl-getinfo.php
  499. *
  500. * @param null $opt
  501. * @return array|mixed
  502. */
  503. public function getInfo($opt = null)
  504. {
  505. if ($this->curl !== null && $opt === null) {
  506. return curl_getinfo($this->curl);
  507. } elseif ($this->curl !== null && $opt !== null) {
  508. return curl_getinfo($this->curl, $opt);
  509. } else {
  510. return [];
  511. }
  512. }
  513. /**
  514. * Performs HTTP request
  515. *
  516. * @param string $method
  517. * @param boolean $raw if response body contains JSON and should be decoded -> helper.
  518. *
  519. * @throws \Exception if request failed
  520. *
  521. * @return mixed
  522. */
  523. protected function _httpRequest($method, $raw = false)
  524. {
  525. //set request type and writer function
  526. $this->setOption(CURLOPT_CUSTOMREQUEST, strtoupper($method));
  527. //check if method is head and set no body
  528. if ($method === 'HEAD') {
  529. $this->setOption(CURLOPT_NOBODY, true);
  530. $this->unsetOption(CURLOPT_WRITEFUNCTION);
  531. }
  532. //setup error reporting and profiling
  533. if (defined('YII_DEBUG') && YII_DEBUG) {
  534. Yii::debug('Start sending cURL-Request: '.$this->getUrl().'\n', __METHOD__);
  535. Yii::beginProfile($method.' '.$this->_baseUrl.'#'.md5(serialize($this->_getDebugData())), __METHOD__);
  536. }
  537. /**
  538. * proceed curl
  539. */
  540. $curlOptions = $this->getOptions();
  541. $this->curl = curl_init($this->getUrl());
  542. curl_setopt_array($this->curl, $curlOptions);
  543. $response = curl_exec($this->curl);
  544. //check if curl was successful
  545. if ($response === false) {
  546. //set error code
  547. $this->errorCode = curl_errno($this->curl);
  548. $this->errorText = curl_strerror($this->errorCode);
  549. switch ($this->errorCode) {
  550. // 7, 28 = timeout
  551. case 7:
  552. case 28:
  553. $this->responseCode = 'timeout';
  554. return false;
  555. break;
  556. default:
  557. return false;
  558. break;
  559. }
  560. }
  561. //extract header / body data if CURLOPT_HEADER are set to true
  562. if (isset($curlOptions[CURLOPT_HEADER]) && $curlOptions[CURLOPT_HEADER]) {
  563. $this->response = $this->_extractCurlBody($response);
  564. $this->responseHeaders = $this->_extractCurlHeaders($response);
  565. } else {
  566. $this->response = $response;
  567. }
  568. // Extract additional curl params
  569. $this->_extractAdditionalCurlParameter();
  570. //end yii debug profile
  571. if (defined('YII_DEBUG') && YII_DEBUG) {
  572. Yii::debug('End cURL-Request: '.$this->response, __METHOD__);
  573. Yii::endProfile($method.' '.$this->getUrl().'#'.md5(serialize($this->_getDebugData())), __METHOD__);
  574. }
  575. //check responseCode and return data/status
  576. if ($this->getOption(CURLOPT_CUSTOMREQUEST) === 'HEAD') {
  577. return true;
  578. } else {
  579. $this->response = $raw ? $this->response : json_decode($this->response, true);
  580. return $this->response;
  581. }
  582. }
  583. /**
  584. * Extract additional curl params protected class helper
  585. */
  586. protected function _extractAdditionalCurlParameter ()
  587. {
  588. /**
  589. * retrieve response code
  590. */
  591. $this->responseCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE);
  592. /**
  593. * try extract response type & charset.
  594. */
  595. $this->responseType = curl_getinfo($this->curl, CURLINFO_CONTENT_TYPE);
  596. if (!is_null($this->responseType) && count(explode(';', $this->responseType)) > 1) {
  597. list($this->responseType, $possibleCharset) = explode(';', $this->responseType);
  598. //extract charset
  599. if (preg_match('~^charset=(.+?)$~', trim($possibleCharset), $matches) && isset($matches[1])) {
  600. $this->responseCharset = strtolower($matches[1]);
  601. }
  602. }
  603. /**
  604. * try extract response length
  605. */
  606. $this->responseLength = curl_getinfo($this->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
  607. if((int)$this->responseLength === -1) {
  608. $this->responseLength = strlen($this->response);
  609. }
  610. }
  611. /**
  612. * Extract body curl data from response
  613. *
  614. * @param string $response
  615. * @return string
  616. */
  617. protected function _extractCurlBody ($response)
  618. {
  619. return substr($response, $this->getInfo(CURLINFO_HEADER_SIZE));
  620. }
  621. /**
  622. * Extract header curl data from response
  623. *
  624. * @param string $response
  625. * @return array
  626. */
  627. protected function _extractCurlHeaders ($response)
  628. {
  629. //Init
  630. $headers = [];
  631. $headerText = substr($response, 0, strpos($response, "\r\n\r\n"));
  632. foreach (explode("\r\n", $headerText) as $i => $line) {
  633. if ($i === 0) {
  634. $headers['http_code'] = $line;
  635. } else {
  636. list ($key, $value) = explode(':', $line, 2);
  637. $headers[$key] = ltrim($value);
  638. }
  639. }
  640. return $headers;
  641. }
  642. /**
  643. * Collects debug data for serialize
  644. * @return array|bool|mixed
  645. */
  646. private function _getDebugData () {
  647. $data = [];
  648. if (is_array($this->getOption(CURLOPT_POSTFIELDS))) {
  649. foreach ($this->getOption(CURLOPT_POSTFIELDS) as $key => $debugItem) {
  650. if (is_array($debugItem)) {
  651. $data[$key] = $debugItem;
  652. } else if ($debugItem instanceof \CURLFile) {
  653. $data[$key] = [
  654. 'name' => $debugItem->name,
  655. 'mime' => $debugItem->mime,
  656. 'postname' => $debugItem->postname,
  657. ];
  658. } // more to come?
  659. }
  660. } else {
  661. $data = $this->getOption(CURLOPT_POSTFIELDS);
  662. }
  663. return $data;
  664. }
  665. }