1: <?php
2:
3: namespace Wei;
4:
5: 6: 7: 8: 9:
10: class Http extends Base implements \ArrayAccess, \Countable, \IteratorAggregate
11: {
12: 13: 14: 15: 16:
17: protected $url;
18:
19: 20: 21: 22: 23: 24: 25: 26:
27: protected $method = 'GET';
28:
29: 30: 31: 32: 33:
34: protected $contentType;
35:
36: 37: 38: 39: 40:
41: protected $cookies = array();
42:
43: 44: 45: 46: 47:
48: protected $data = array();
49:
50: 51: 52: 53: 54:
55: protected $files = array();
56:
57: 58: 59: 60: 61: 62:
63: protected $global = false;
64:
65: 66: 67: 68: 69:
70: protected $headers = array();
71:
72: 73: 74: 75: 76: 77: 78: 79:
80: protected $header = false;
81:
82: 83: 84: 85: 86:
87: protected $ip;
88:
89: 90: 91: 92: 93:
94: protected $timeout;
95:
96: 97: 98: 99: 100: 101: 102: 103:
104: protected $dataType = 'text';
105:
106: 107: 108: 109: 110: 111: 112:
113: protected $referer;
114:
115: 116: 117: 118: 119:
120: protected $userAgent;
121:
122: 123: 124: 125: 126: 127: 128:
129: protected $throwException = true;
130:
131: 132: 133: 134: 135:
136: protected $beforeSend;
137:
138: 139: 140: 141: 142:
143: protected $success;
144:
145: 146: 147: 148: 149: 150: 151:
152: protected $error;
153:
154: 155: 156: 157: 158:
159: protected $complete;
160:
161: 162: 163: 164: 165:
166: protected $curlOptions = array();
167:
168: 169: 170: 171: 172:
173: protected $defaultCurlOptions = array(
174: CURLOPT_RETURNTRANSFER => true,
175: CURLOPT_FOLLOWLOCATION => true,
176: );
177:
178: 179: 180: 181: 182:
183: protected $result;
184:
185: 186: 187: 188: 189:
190: protected $responseText;
191:
192: 193: 194: 195: 196:
197: protected $response;
198:
199: 200: 201: 202: 203:
204: protected $responseHeader;
205:
206: 207: 208: 209: 210:
211: protected $responseHeaders;
212:
213: 214: 215: 216: 217:
218: protected $responseCookies;
219:
220: 221: 222: 223: 224:
225: protected $ch;
226:
227: 228: 229: 230: 231:
232: protected $errorStatus = '';
233:
234: 235: 236: 237: 238:
239: protected $errorException;
240:
241: 242: 243: 244: 245:
246: private $defaultOptions;
247:
248: 249: 250: 251: 252:
253: public function __construct(array $options = array())
254: {
255:
256: if (isset($options['global']) && true == $options['global']) {
257: $options += (array)$options['wei']->getConfig('http');
258: }
259: parent::__construct($options);
260: $this->defaultOptions = $options;
261: }
262:
263: 264: 265: 266: 267: 268: 269:
270: public function __invoke($url = null, array $options = array())
271: {
272:
273: if (is_array($url)) {
274: $options = $url;
275: } else {
276: $options['url'] = $url;
277: }
278: $options = $options + $this->defaultOptions;
279:
280: $http = new self($options);
281: $http->execute();
282:
283: return $http;
284: }
285:
286: 287: 288:
289: public function execute()
290: {
291:
292: $ch = $this->ch = curl_init();
293: curl_setopt_array($ch, $this->prepareCurlOptions());
294: $this->beforeSend && call_user_func($this->beforeSend, $this, $ch);
295:
296:
297: $response = curl_exec($ch);
298:
299:
300: $this->handleResponse($response);
301: $this->complete && call_user_func($this->complete, $this, $ch);
302:
303: if ($this->throwException && $this->errorException) {
304: throw $this->errorException;
305: }
306: }
307:
308: 309: 310: 311: 312:
313: protected function prepareCurlOptions()
314: {
315: $opts = array();
316: $url = $this->url;
317:
318:
319: if ($this->ip) {
320: $host = parse_url($url, PHP_URL_HOST);
321: $url = substr_replace($url, $this->ip, strpos($url, $host), strlen($host));
322: $this->headers['Host'] = $host;
323: }
324:
325: switch ($this->method) {
326: case 'GET' :
327: $postData = false;
328: break;
329:
330: case 'POST' :
331: $postData = true;
332: $opts[CURLOPT_POST] = 1;
333: break;
334:
335: case 'DELETE':
336: case 'PUT':
337: case 'PATCH':
338: $postData = true;
339: $opts[CURLOPT_CUSTOMREQUEST] = $this->method;
340: break;
341:
342: default:
343: $postData = false;
344: $opts[CURLOPT_CUSTOMREQUEST] = $this->method;
345: }
346:
347: if ($this->data) {
348: $data = is_string($this->data) ? $this->data : http_build_query($this->data);
349: if ($postData) {
350: $opts[CURLOPT_POSTFIELDS] = $data;
351: } else {
352: if (false === strpos($url, '?')) {
353: $url .= '?' . $data;
354: } else {
355: $url .= '&' . $data;
356: }
357: }
358: }
359:
360: if ($this->timeout > 0) {
361: $opts[CURLOPT_TIMEOUT_MS] = $this->timeout;
362: }
363:
364: if ($this->referer) {
365:
366: if (true === $this->referer) {
367: $opts[CURLOPT_REFERER] = $this->url;
368: } else {
369: $opts[CURLOPT_REFERER] = $this->referer;
370: }
371: }
372:
373: if ($this->userAgent) {
374: $opts[CURLOPT_USERAGENT] = $this->userAgent;
375: }
376:
377: if ($this->cookies) {
378: $cookies = array();
379: foreach ($this->cookies as $key => $value) {
380: $cookies[] = $key . '=' . urlencode($value);
381: }
382: $opts[CURLOPT_COOKIE] = implode('; ', $cookies);
383: }
384:
385: if ($this->contentType) {
386: $this->headers['Content-Type'] = $this->contentType;
387: }
388:
389:
390: if ($this->headers) {
391: $headers = array();
392: foreach ($this->headers as $key => $value) {
393: $headers[] = $key . ': ' . $value;
394: }
395: $opts[CURLOPT_HTTPHEADER] = $headers;
396: }
397:
398: $opts[CURLOPT_HEADER] = $this->header;
399: $opts[CURLOPT_URL] = $url;
400:
401: $this->curlOptions += $opts + $this->defaultCurlOptions;
402: return $this->curlOptions;
403: }
404:
405: 406: 407: 408: 409:
410: protected function handleResponse($response)
411: {
412: $ch = $this->ch;
413:
414: if (false !== $response) {
415: $curlInfo = curl_getinfo($ch);
416:
417:
418: if ($this->getCurlOption(CURLOPT_HEADER)) {
419:
420:
421: if (false !== stripos($response, "HTTP/1.1 200 Connection established\r\n\r\n")) {
422: $response = str_ireplace("HTTP/1.1 200 Connection established\r\n\r\n", '', $response);
423: }
424:
425: $this->responseHeader = trim(substr($response, 0, $curlInfo['header_size']));
426: $this->responseText = substr($response, $curlInfo['header_size']);
427: } else {
428: $this->responseText = $response;
429: }
430:
431: $statusCode = $curlInfo['http_code'];
432: $isSuccess = $statusCode >= 200 && $statusCode < 300 || $statusCode === 304;
433: if ($isSuccess) {
434: $this->response = $this->parseResponse($this->responseText, $exception);
435: if (!$exception) {
436: $this->result = true;
437: $this->success && call_user_func($this->success, $this->response, $this);
438: } else {
439: $this->triggerError('parser', $exception);
440: }
441: } else {
442: if ($this->responseHeader) {
443: preg_match('/[\d]{3} (.+?)\r/', $this->responseHeader, $matches);
444: $statusText = $matches[1];
445: } else {
446: $statusText = 'HTTP request error';
447: }
448: $exception = new \ErrorException($statusText, $statusCode);
449: $this->triggerError('http', $exception);
450: }
451: } else {
452: $exception = new \ErrorException(curl_error($ch), curl_errno($ch));
453: $this->triggerError('curl', $exception);
454: }
455: }
456:
457: 458: 459: 460: 461: 462:
463: protected function triggerError($status, \ErrorException $exception)
464: {
465: $this->result = false;
466: $this->errorStatus = $status;
467: $this->errorException = $exception;
468: $this->error && call_user_func($this->error, $this, $status, $exception);
469: }
470:
471: 472: 473: 474: 475: 476: 477:
478: protected function parseResponse($data, &$exception)
479: {
480: switch ($this->dataType) {
481: case 'json' :
482: case 'jsonObject' :
483: $data = json_decode($data, $this->dataType === 'json');
484: if (null === $data && json_last_error() != JSON_ERROR_NONE) {
485: $exception = new \ErrorException('JSON parsing error', json_last_error());
486: }
487: break;
488:
489: case 'xml' :
490: case 'serialize' :
491: $methods = array(
492: 'xml' => 'simplexml_load_string',
493: 'serialize' => 'unserialize',
494: );
495: $data = @$methods[$this->dataType]($data);
496: if (false === $data && $e = error_get_last()) {
497: $exception = new \ErrorException($e['message'], $e['type'], 0, $e['file'], $e['line']);
498: }
499: break;
500:
501: case 'query' :
502:
503: parse_str($data, $data);
504: break;
505:
506: case 'text':
507: default :
508: break;
509: }
510: return $data;
511: }
512:
513: 514: 515: 516: 517: 518: 519:
520: public function setCurlOption($option, $value)
521: {
522: $this->curlOptions[$option] = $value;
523: return $this;
524: }
525:
526: 527: 528: 529: 530: 531:
532: public function getCurlOption($option)
533: {
534: return isset($this->curlOptions[$option]) ? $this->curlOptions[$option] : null;
535: }
536:
537: 538: 539: 540: 541:
542: public function getResponseText()
543: {
544: return $this->responseText;
545: }
546:
547: 548: 549: 550: 551:
552: public function getResponse()
553: {
554: return $this->response;
555: }
556:
557: 558: 559: 560: 561:
562: public function getUrl()
563: {
564: return $this->url;
565: }
566:
567: 568: 569: 570: 571:
572: public function getMethod()
573: {
574: return $this->method;
575: }
576:
577: 578: 579: 580: 581:
582: public function getIp()
583: {
584: return $this->ip;
585: }
586:
587: 588: 589: 590: 591:
592: public function getData()
593: {
594: return $this->data;
595: }
596:
597: 598: 599: 600: 601: 602: 603:
604: public function getResponseHeader($name = null, $first = true)
605: {
606:
607: if (is_null($name)) {
608: return $this->responseHeader;
609: }
610:
611: $name = strtoupper($name);
612: $headers = $this->getResponseHeaders();
613:
614: if (!isset($headers[$name])) {
615: return $first ? null : array();
616: } else {
617: return $first ? current($headers[$name]) : $headers[$name];
618: }
619: }
620:
621: 622: 623: 624: 625:
626: public function getResponseHeaders()
627: {
628: if (!is_array($this->responseHeaders)) {
629: $this->responseHeaders = array();
630: foreach (explode("\n", $this->responseHeader) as $line) {
631: $line = explode(':', $line, 2);
632: $name = strtoupper($line[0]);
633: $value = isset($line[1]) ? trim($line[1]) : null;
634: $this->responseHeaders[$name][] = $value;
635: }
636: }
637: return $this->responseHeaders;
638: }
639:
640: 641: 642: 643: 644:
645: public function getResponseCookies()
646: {
647: if (!is_array($this->responseCookies)) {
648: $cookies = $this->getResponseHeader('SET-COOKIE', false);
649: $this->responseCookies = array();
650: foreach ($cookies as $cookie) {
651: $this->responseCookies += $this->parseCookie($cookie);
652: }
653: }
654: return $this->responseCookies;
655: }
656:
657: 658: 659: 660: 661: 662:
663: public function getResponseCookie($name)
664: {
665: $cookies = $this->getResponseCookies();
666: return isset($cookies[$name]) ? $cookies[$name] : null;
667: }
668:
669: 670: 671: 672: 673: 674:
675: protected function parseCookie($header)
676: {
677: $elements = explode(';', $header);
678: $cookies = array();
679:
680: $currentName = null;
681: foreach ($elements as $element) {
682: $pieces = explode('=', trim($element), 2);
683: if (!isset($pieces[1])) {
684: continue;
685: }
686: list($name, $value) = $pieces;
687:
688: if (strtolower($name) == 'expires' && strtotime($value) < time()) {
689:
690: unset($cookies[$currentName]);
691: } elseif (in_array(strtolower($name), array('domain', 'path', 'comment', 'expires', 'secure', 'max-age'))) {
692:
693: continue;
694: } else {
695: $cookies[$name] = trim(urldecode($value));
696: $currentName = $name;
697: }
698: }
699: return $cookies;
700: }
701:
702: 703: 704: 705: 706:
707: public function isSuccess()
708: {
709: return $this->result;
710: }
711:
712: 713: 714: 715: 716: 717:
718: public function setMethod($method)
719: {
720: $this->method = strtoupper($method);
721: return $this;
722: }
723:
724: 725: 726: 727: 728: 729: 730: 731:
732: public function get($url, $data = array(), $dataType = null)
733: {
734: return $this->processMethod($url, $data, $dataType, 'GET');
735: }
736:
737: 738: 739: 740: 741: 742: 743:
744: public function getJson($url, $data = array())
745: {
746: return $this->processMethod($url, $data, 'json', 'GET');
747: }
748:
749: 750: 751: 752: 753: 754: 755:
756: public function getJsonObject($url, $data = array())
757: {
758: return $this->processMethod($url, $data, 'jsonObject', 'GET');
759: }
760:
761: 762: 763: 764: 765: 766: 767: 768:
769: public function post($url, $data = array(), $dataType = null)
770: {
771: return $this->processMethod($url, $data, $dataType, 'POST');
772: }
773:
774: 775: 776: 777: 778: 779: 780:
781: public function postJson($url, $data = array())
782: {
783: return $this->processMethod($url, $data, 'json', 'POST');
784: }
785:
786: 787: 788: 789: 790: 791: 792: 793:
794: public function put($url, $data = array(), $dataType = null)
795: {
796: return $this->processMethod($url, $data, $dataType, 'PUT');
797: }
798:
799: 800: 801: 802: 803: 804: 805: 806:
807: public function delete($url, $data = array(), $dataType = null)
808: {
809: return $this->processMethod($url, $data, $dataType, 'DELETE');
810: }
811:
812: 813: 814: 815: 816: 817: 818: 819:
820: public function patch($url, $data = array(), $dataType = null)
821: {
822: return $this->processMethod($url, $data, $dataType, 'PATCH');
823: }
824:
825: public function upload($url, $data = array(), $dataType = null)
826: {
827: return $this->processMethod($url, $data, $dataType, 'POST');
828: }
829:
830: 831: 832: 833: 834: 835: 836: 837: 838:
839: protected function processMethod($url, $data, $dataType, $method)
840: {
841: return $this->__invoke(array(
842: 'url' => $url,
843: 'method' => $method,
844: 'dataType' => $dataType,
845: 'data' => $data
846: ));
847: }
848:
849: 850: 851: 852: 853:
854: public function getErrorStatus()
855: {
856: return $this->errorStatus;
857: }
858:
859: 860: 861: 862: 863:
864: public function getErrorException()
865: {
866: return $this->errorException;
867: }
868:
869: 870: 871: 872: 873: 874:
875: public function offsetExists($offset)
876: {
877: return array_key_exists($offset, $this->response);
878: }
879:
880: 881: 882: 883: 884: 885:
886: public function offsetGet($offset)
887: {
888: return isset($this->response[$offset]) ? $this->response[$offset] : null;
889: }
890:
891: 892: 893: 894: 895: 896:
897: public function offsetSet($offset, $value)
898: {
899: $this->response[$offset] = $value;
900: }
901:
902: 903: 904: 905: 906:
907: public function offsetUnset($offset)
908: {
909: unset($this->response[$offset]);
910: }
911:
912: 913: 914: 915: 916:
917: public function count()
918: {
919: return count($this->response);
920: }
921:
922: 923: 924: 925: 926:
927: public function getIterator()
928: {
929: return new \ArrayIterator($this->response);
930: }
931:
932: 933: 934: 935: 936:
937: public function __toString()
938: {
939: return (string)$this->responseText;
940: }
941:
942: 943: 944:
945: public function getCurlInfo()
946: {
947: return curl_getinfo($this->ch);
948: }
949:
950: 951: 952:
953: public function __destruct()
954: {
955: if ($this->ch) {
956: curl_close($this->ch);
957: unset($this->ch);
958: }
959: }
960: }