1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18:
19: App::uses('CakeSocket', 'Network');
20: App::uses('Router', 'Routing');
21:
22: 23: 24: 25: 26: 27: 28: 29:
30: class HttpSocket extends CakeSocket {
31:
32: 33: 34: 35: 36: 37: 38:
39: public $quirksMode = false;
40:
41: 42: 43: 44: 45:
46: public $request = array(
47: 'method' => 'GET',
48: 'uri' => array(
49: 'scheme' => 'http',
50: 'host' => null,
51: 'port' => 80,
52: 'user' => null,
53: 'pass' => null,
54: 'path' => null,
55: 'query' => null,
56: 'fragment' => null
57: ),
58: 'version' => '1.1',
59: 'body' => '',
60: 'line' => null,
61: 'header' => array(
62: 'Connection' => 'close',
63: 'User-Agent' => 'CakePHP'
64: ),
65: 'raw' => null,
66: 'cookies' => array()
67: );
68:
69: 70: 71: 72: 73:
74: public $response = null;
75:
76: 77: 78: 79: 80:
81: public $responseClass = 'HttpResponse';
82:
83: 84: 85: 86: 87:
88: public $config = array(
89: 'persistent' => false,
90: 'host' => 'localhost',
91: 'protocol' => 'tcp',
92: 'port' => 80,
93: 'timeout' => 30,
94: 'request' => array(
95: 'uri' => array(
96: 'scheme' => array('http', 'https'),
97: 'host' => 'localhost',
98: 'port' => array(80, 443)
99: ),
100: 'cookies' => array()
101: )
102: );
103:
104: 105: 106: 107: 108:
109: protected $_auth = array();
110:
111: 112: 113: 114: 115:
116: protected $_proxy = array();
117:
118: 119: 120: 121: 122:
123: protected $_contentResource = null;
124:
125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145:
146: public function __construct($config = array()) {
147: if (is_string($config)) {
148: $this->_configUri($config);
149: } elseif (is_array($config)) {
150: if (isset($config['request']['uri']) && is_string($config['request']['uri'])) {
151: $this->_configUri($config['request']['uri']);
152: unset($config['request']['uri']);
153: }
154: $this->config = Set::merge($this->config, $config);
155: }
156: parent::__construct($this->config);
157: }
158:
159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189:
190: public function configAuth($method, $user = null, $pass = null) {
191: if (empty($method)) {
192: $this->_auth = array();
193: return;
194: }
195: if (is_array($user)) {
196: $this->_auth = array($method => $user);
197: return;
198: }
199: $this->_auth = array($method => compact('user', 'pass'));
200: }
201:
202: 203: 204: 205: 206: 207: 208: 209: 210: 211:
212: public function configProxy($host, $port = 3128, $method = null, $user = null, $pass = null) {
213: if (empty($host)) {
214: $this->_proxy = array();
215: return;
216: }
217: if (is_array($host)) {
218: $this->_proxy = $host + array('host' => null);
219: return;
220: }
221: $this->_proxy = compact('host', 'port', 'method', 'user', 'pass');
222: }
223:
224: 225: 226: 227: 228: 229: 230:
231: public function setContentResource($resource) {
232: if ($resource === false) {
233: $this->_contentResource = null;
234: return;
235: }
236: if (!is_resource($resource)) {
237: throw new SocketException(__d('cake_dev', 'Invalid resource.'));
238: }
239: $this->_contentResource = $resource;
240: }
241:
242: 243: 244: 245: 246: 247: 248: 249:
250: public function request($request = array()) {
251: $this->reset(false);
252:
253: if (is_string($request)) {
254: $request = array('uri' => $request);
255: } elseif (!is_array($request)) {
256: return false;
257: }
258:
259: if (!isset($request['uri'])) {
260: $request['uri'] = null;
261: }
262: $uri = $this->_parseUri($request['uri']);
263: if (!isset($uri['host'])) {
264: $host = $this->config['host'];
265: }
266: if (isset($request['host'])) {
267: $host = $request['host'];
268: unset($request['host']);
269: }
270: $request['uri'] = $this->url($request['uri']);
271: $request['uri'] = $this->_parseUri($request['uri'], true);
272: $this->request = Set::merge($this->request, array_diff_key($this->config['request'], array('cookies' => true)), $request);
273:
274: $this->_configUri($this->request['uri']);
275:
276: $Host = $this->request['uri']['host'];
277: if (!empty($this->config['request']['cookies'][$Host])) {
278: if (!isset($this->request['cookies'])) {
279: $this->request['cookies'] = array();
280: }
281: if (!isset($request['cookies'])) {
282: $request['cookies'] = array();
283: }
284: $this->request['cookies'] = array_merge($this->request['cookies'], $this->config['request']['cookies'][$Host], $request['cookies']);
285: }
286:
287: if (isset($host)) {
288: $this->config['host'] = $host;
289: }
290: $this->_setProxy();
291: $this->request['proxy'] = $this->_proxy;
292:
293: $cookies = null;
294:
295: if (is_array($this->request['header'])) {
296: if (!empty($this->request['cookies'])) {
297: $cookies = $this->buildCookies($this->request['cookies']);
298: }
299: $scheme = '';
300: $port = 0;
301: if (isset($this->request['uri']['scheme'])) {
302: $scheme = $this->request['uri']['scheme'];
303: }
304: if (isset($this->request['uri']['port'])) {
305: $port = $this->request['uri']['port'];
306: }
307: if (
308: ($scheme === 'http' && $port != 80) ||
309: ($scheme === 'https' && $port != 443) ||
310: ($port != 80 && $port != 443)
311: ) {
312: $Host .= ':' . $port;
313: }
314: $this->request['header'] = array_merge(compact('Host'), $this->request['header']);
315: }
316:
317: if (isset($this->request['uri']['user'], $this->request['uri']['pass'])) {
318: $this->configAuth('Basic', $this->request['uri']['user'], $this->request['uri']['pass']);
319: }
320: $this->_setAuth();
321: $this->request['auth'] = $this->_auth;
322:
323: if (is_array($this->request['body'])) {
324: $this->request['body'] = $this->_httpSerialize($this->request['body']);
325: }
326:
327: if (!empty($this->request['body']) && !isset($this->request['header']['Content-Type'])) {
328: $this->request['header']['Content-Type'] = 'application/x-www-form-urlencoded';
329: }
330:
331: if (!empty($this->request['body']) && !isset($this->request['header']['Content-Length'])) {
332: $this->request['header']['Content-Length'] = strlen($this->request['body']);
333: }
334:
335: $connectionType = null;
336: if (isset($this->request['header']['Connection'])) {
337: $connectionType = $this->request['header']['Connection'];
338: }
339: $this->request['header'] = $this->_buildHeader($this->request['header']) . $cookies;
340:
341: if (empty($this->request['line'])) {
342: $this->request['line'] = $this->_buildRequestLine($this->request);
343: }
344:
345: if ($this->quirksMode === false && $this->request['line'] === false) {
346: return false;
347: }
348:
349: $this->request['raw'] = '';
350: if ($this->request['line'] !== false) {
351: $this->request['raw'] = $this->request['line'];
352: }
353:
354: if ($this->request['header'] !== false) {
355: $this->request['raw'] .= $this->request['header'];
356: }
357:
358: $this->request['raw'] .= "\r\n";
359: $this->request['raw'] .= $this->request['body'];
360: $this->write($this->request['raw']);
361:
362: $response = null;
363: $inHeader = true;
364: while ($data = $this->read()) {
365: if ($this->_contentResource) {
366: if ($inHeader) {
367: $response .= $data;
368: $pos = strpos($response, "\r\n\r\n");
369: if ($pos !== false) {
370: $pos += 4;
371: $data = substr($response, $pos);
372: fwrite($this->_contentResource, $data);
373:
374: $response = substr($response, 0, $pos);
375: $inHeader = false;
376: }
377: } else {
378: fwrite($this->_contentResource, $data);
379: fflush($this->_contentResource);
380: }
381: } else {
382: $response .= $data;
383: }
384: }
385:
386: if ($connectionType === 'close') {
387: $this->disconnect();
388: }
389:
390: list($plugin, $responseClass) = pluginSplit($this->responseClass, true);
391: App::uses($this->responseClass, $plugin . 'Network/Http');
392: if (!class_exists($responseClass)) {
393: throw new SocketException(__d('cake_dev', 'Class %s not found.', $this->responseClass));
394: }
395: $responseClass = $this->responseClass;
396: $this->response = new $responseClass($response);
397: if (!empty($this->response->cookies)) {
398: if (!isset($this->config['request']['cookies'][$Host])) {
399: $this->config['request']['cookies'][$Host] = array();
400: }
401: $this->config['request']['cookies'][$Host] = array_merge($this->config['request']['cookies'][$Host], $this->response->cookies);
402: }
403:
404: return $this->response;
405: }
406:
407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429:
430: public function get($uri = null, $query = array(), $request = array()) {
431: if (!empty($query)) {
432: $uri = $this->_parseUri($uri, $this->config['request']['uri']);
433: if (isset($uri['query'])) {
434: $uri['query'] = array_merge($uri['query'], $query);
435: } else {
436: $uri['query'] = $query;
437: }
438: $uri = $this->_buildUri($uri);
439: }
440:
441: $request = Set::merge(array('method' => 'GET', 'uri' => $uri), $request);
442: return $this->request($request);
443: }
444:
445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461:
462: public function post($uri = null, $data = array(), $request = array()) {
463: $request = Set::merge(array('method' => 'POST', 'uri' => $uri, 'body' => $data), $request);
464: return $this->request($request);
465: }
466:
467: 468: 469: 470: 471: 472: 473: 474:
475: public function put($uri = null, $data = array(), $request = array()) {
476: $request = Set::merge(array('method' => 'PUT', 'uri' => $uri, 'body' => $data), $request);
477: return $this->request($request);
478: }
479:
480: 481: 482: 483: 484: 485: 486: 487:
488: public function delete($uri = null, $data = array(), $request = array()) {
489: $request = Set::merge(array('method' => 'DELETE', 'uri' => $uri, 'body' => $data), $request);
490: return $this->request($request);
491: }
492:
493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 517: 518: 519:
520: public function url($url = null, $uriTemplate = null) {
521: if (is_null($url)) {
522: $url = '/';
523: }
524: if (is_string($url)) {
525: $scheme = $this->config['request']['uri']['scheme'];
526: if (is_array($scheme)) {
527: $scheme = $scheme[0];
528: }
529: $port = $this->config['request']['uri']['port'];
530: if (is_array($port)) {
531: $port = $port[0];
532: }
533: if ($url{0} == '/') {
534: $url = $this->config['request']['uri']['host'] . ':' . $port . $url;
535: }
536: if (!preg_match('/^.+:\/\/|\*|^\//', $url)) {
537: $url = $scheme . '://' . $url;
538: }
539: } elseif (!is_array($url) && !empty($url)) {
540: return false;
541: }
542:
543: $base = array_merge($this->config['request']['uri'], array('scheme' => array('http', 'https'), 'port' => array(80, 443)));
544: $url = $this->_parseUri($url, $base);
545:
546: if (empty($url)) {
547: $url = $this->config['request']['uri'];
548: }
549:
550: if (!empty($uriTemplate)) {
551: return $this->_buildUri($url, $uriTemplate);
552: }
553: return $this->_buildUri($url);
554: }
555:
556: 557: 558: 559: 560: 561:
562: protected function _setAuth() {
563: if (empty($this->_auth)) {
564: return;
565: }
566: $method = key($this->_auth);
567: list($plugin, $authClass) = pluginSplit($method, true);
568: $authClass = Inflector::camelize($authClass) . 'Authentication';
569: App::uses($authClass, $plugin . 'Network/Http');
570:
571: if (!class_exists($authClass)) {
572: throw new SocketException(__d('cake_dev', 'Unknown authentication method.'));
573: }
574: if (!method_exists($authClass, 'authentication')) {
575: throw new SocketException(sprintf(__d('cake_dev', 'The %s do not support authentication.'), $authClass));
576: }
577: call_user_func_array("$authClass::authentication", array($this, &$this->_auth[$method]));
578: }
579:
580: 581: 582: 583: 584: 585:
586: protected function _setProxy() {
587: if (empty($this->_proxy) || !isset($this->_proxy['host'], $this->_proxy['port'])) {
588: return;
589: }
590: $this->config['host'] = $this->_proxy['host'];
591: $this->config['port'] = $this->_proxy['port'];
592:
593: if (empty($this->_proxy['method']) || !isset($this->_proxy['user'], $this->_proxy['pass'])) {
594: return;
595: }
596: list($plugin, $authClass) = pluginSplit($this->_proxy['method'], true);
597: $authClass = Inflector::camelize($authClass) . 'Authentication';
598: App::uses($authClass, $plugin. 'Network/Http');
599:
600: if (!class_exists($authClass)) {
601: throw new SocketException(__d('cake_dev', 'Unknown authentication method for proxy.'));
602: }
603: if (!method_exists($authClass, 'proxyAuthentication')) {
604: throw new SocketException(sprintf(__d('cake_dev', 'The %s do not support proxy authentication.'), $authClass));
605: }
606: call_user_func_array("$authClass::proxyAuthentication", array($this, &$this->_proxy));
607: }
608:
609: 610: 611: 612: 613: 614:
615: protected function _configUri($uri = null) {
616: if (empty($uri)) {
617: return false;
618: }
619:
620: if (is_array($uri)) {
621: $uri = $this->_parseUri($uri);
622: } else {
623: $uri = $this->_parseUri($uri, true);
624: }
625:
626: if (!isset($uri['host'])) {
627: return false;
628: }
629: $config = array(
630: 'request' => array(
631: 'uri' => array_intersect_key($uri, $this->config['request']['uri'])
632: )
633: );
634: $this->config = Set::merge($this->config, $config);
635: $this->config = Set::merge($this->config, array_intersect_key($this->config['request']['uri'], $this->config));
636: return true;
637: }
638:
639: 640: 641: 642: 643: 644: 645:
646: protected function _buildUri($uri = array(), $uriTemplate = '%scheme://%user:%pass@%host:%port/%path?%query#%fragment') {
647: if (is_string($uri)) {
648: $uri = array('host' => $uri);
649: }
650: $uri = $this->_parseUri($uri, true);
651:
652: if (!is_array($uri) || empty($uri)) {
653: return false;
654: }
655:
656: $uri['path'] = preg_replace('/^\//', null, $uri['path']);
657: $uri['query'] = $this->_httpSerialize($uri['query']);
658: $uri['query'] = rtrim($uri['query'], '=');
659: $stripIfEmpty = array(
660: 'query' => '?%query',
661: 'fragment' => '#%fragment',
662: 'user' => '%user:%pass@',
663: 'host' => '%host:%port/'
664: );
665:
666: foreach ($stripIfEmpty as $key => $strip) {
667: if (empty($uri[$key])) {
668: $uriTemplate = str_replace($strip, null, $uriTemplate);
669: }
670: }
671:
672: $defaultPorts = array('http' => 80, 'https' => 443);
673: if (array_key_exists($uri['scheme'], $defaultPorts) && $defaultPorts[$uri['scheme']] == $uri['port']) {
674: $uriTemplate = str_replace(':%port', null, $uriTemplate);
675: }
676: foreach ($uri as $property => $value) {
677: $uriTemplate = str_replace('%' . $property, $value, $uriTemplate);
678: }
679:
680: if ($uriTemplate === '/*') {
681: $uriTemplate = '*';
682: }
683: return $uriTemplate;
684: }
685:
686: 687: 688: 689: 690: 691: 692: 693:
694: protected function _parseUri($uri = null, $base = array()) {
695: $uriBase = array(
696: 'scheme' => array('http', 'https'),
697: 'host' => null,
698: 'port' => array(80, 443),
699: 'user' => null,
700: 'pass' => null,
701: 'path' => '/',
702: 'query' => null,
703: 'fragment' => null
704: );
705:
706: if (is_string($uri)) {
707: $uri = parse_url($uri);
708: }
709: if (!is_array($uri) || empty($uri)) {
710: return false;
711: }
712: if ($base === true) {
713: $base = $uriBase;
714: }
715:
716: if (isset($base['port'], $base['scheme']) && is_array($base['port']) && is_array($base['scheme'])) {
717: if (isset($uri['scheme']) && !isset($uri['port'])) {
718: $base['port'] = $base['port'][array_search($uri['scheme'], $base['scheme'])];
719: } elseif (isset($uri['port']) && !isset($uri['scheme'])) {
720: $base['scheme'] = $base['scheme'][array_search($uri['port'], $base['port'])];
721: }
722: }
723:
724: if (is_array($base) && !empty($base)) {
725: $uri = array_merge($base, $uri);
726: }
727:
728: if (isset($uri['scheme']) && is_array($uri['scheme'])) {
729: $uri['scheme'] = array_shift($uri['scheme']);
730: }
731: if (isset($uri['port']) && is_array($uri['port'])) {
732: $uri['port'] = array_shift($uri['port']);
733: }
734:
735: if (array_key_exists('query', $uri)) {
736: $uri['query'] = $this->_parseQuery($uri['query']);
737: }
738:
739: if (!array_intersect_key($uriBase, $uri)) {
740: return false;
741: }
742: return $uri;
743: }
744:
745: 746: 747: 748: 749: 750: 751: 752: 753: 754: 755: 756: 757: 758:
759: protected function _parseQuery($query) {
760: if (is_array($query)) {
761: return $query;
762: }
763: $parsedQuery = array();
764:
765: if (is_string($query) && !empty($query)) {
766: $query = preg_replace('/^\?/', '', $query);
767: $items = explode('&', $query);
768:
769: foreach ($items as $item) {
770: if (strpos($item, '=') !== false) {
771: list($key, $value) = explode('=', $item, 2);
772: } else {
773: $key = $item;
774: $value = null;
775: }
776:
777: $key = urldecode($key);
778: $value = urldecode($value);
779:
780: if (preg_match_all('/\[([^\[\]]*)\]/iUs', $key, $matches)) {
781: $subKeys = $matches[1];
782: $rootKey = substr($key, 0, strpos($key, '['));
783: if (!empty($rootKey)) {
784: array_unshift($subKeys, $rootKey);
785: }
786: $queryNode =& $parsedQuery;
787:
788: foreach ($subKeys as $subKey) {
789: if (!is_array($queryNode)) {
790: $queryNode = array();
791: }
792:
793: if ($subKey === '') {
794: $queryNode[] = array();
795: end($queryNode);
796: $subKey = key($queryNode);
797: }
798: $queryNode =& $queryNode[$subKey];
799: }
800: $queryNode = $value;
801: } else {
802: $parsedQuery[$key] = $value;
803: }
804: }
805: }
806: return $parsedQuery;
807: }
808:
809: 810: 811: 812: 813: 814: 815: 816:
817: protected function _buildRequestLine($request = array(), $versionToken = 'HTTP/1.1') {
818: $asteriskMethods = array('OPTIONS');
819:
820: if (is_string($request)) {
821: $isValid = preg_match("/(.+) (.+) (.+)\r\n/U", $request, $match);
822: if (!$this->quirksMode && (!$isValid || ($match[2] == '*' && !in_array($match[3], $asteriskMethods)))) {
823: throw new SocketException(__d('cake_dev', 'HttpSocket::_buildRequestLine - Passed an invalid request line string. Activate quirks mode to do this.'));
824: }
825: return $request;
826: } elseif (!is_array($request)) {
827: return false;
828: } elseif (!array_key_exists('uri', $request)) {
829: return false;
830: }
831:
832: $request['uri'] = $this->_parseUri($request['uri']);
833: $request = array_merge(array('method' => 'GET'), $request);
834: if (!empty($this->_proxy['host'])) {
835: $request['uri'] = $this->_buildUri($request['uri'], '%scheme://%host:%port/%path?%query');
836: } else {
837: $request['uri'] = $this->_buildUri($request['uri'], '/%path?%query');
838: }
839:
840: if (!$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) {
841: throw new SocketException(__d('cake_dev', 'HttpSocket::_buildRequestLine - The "*" asterisk character is only allowed for the following methods: %s. Activate quirks mode to work outside of HTTP/1.1 specs.', implode(',', $asteriskMethods)));
842: }
843: return $request['method'] . ' ' . $request['uri'] . ' ' . $versionToken . "\r\n";
844: }
845:
846: 847: 848: 849: 850: 851:
852: protected function _httpSerialize($data = array()) {
853: if (is_string($data)) {
854: return $data;
855: }
856: if (empty($data) || !is_array($data)) {
857: return false;
858: }
859: return substr(Router::queryString($data), 1);
860: }
861:
862: 863: 864: 865: 866: 867: 868:
869: protected function _buildHeader($header, $mode = 'standard') {
870: if (is_string($header)) {
871: return $header;
872: } elseif (!is_array($header)) {
873: return false;
874: }
875:
876: $fieldsInHeader = array();
877: foreach ($header as $key => $value) {
878: $lowKey = strtolower($key);
879: if (array_key_exists($lowKey, $fieldsInHeader)) {
880: $header[$fieldsInHeader[$lowKey]] = $value;
881: unset($header[$key]);
882: } else {
883: $fieldsInHeader[$lowKey] = $key;
884: }
885: }
886:
887: $returnHeader = '';
888: foreach ($header as $field => $contents) {
889: if (is_array($contents) && $mode == 'standard') {
890: $contents = implode(',', $contents);
891: }
892: foreach ((array)$contents as $content) {
893: $contents = preg_replace("/\r\n(?![\t ])/", "\r\n ", $content);
894: $field = $this->_escapeToken($field);
895:
896: $returnHeader .= $field . ': ' . $contents . "\r\n";
897: }
898: }
899: return $returnHeader;
900: }
901:
902: 903: 904: 905: 906: 907: 908:
909: public function buildCookies($cookies) {
910: $header = array();
911: foreach ($cookies as $name => $cookie) {
912: $header[] = $name . '=' . $this->_escapeToken($cookie['value'], array(';'));
913: }
914: return $this->_buildHeader(array('Cookie' => implode('; ', $header)), 'pragmatic');
915: }
916:
917: 918: 919: 920: 921: 922: 923: 924:
925: protected function _escapeToken($token, $chars = null) {
926: $regex = '/([' . implode('', $this->_tokenEscapeChars(true, $chars)) . '])/';
927: $token = preg_replace($regex, '"\\1"', $token);
928: return $token;
929: }
930:
931: 932: 933: 934: 935: 936: 937: 938:
939: protected function _tokenEscapeChars($hex = true, $chars = null) {
940: if (!empty($chars)) {
941: $escape = $chars;
942: } else {
943: $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
944: for ($i = 0; $i <= 31; $i++) {
945: $escape[] = chr($i);
946: }
947: $escape[] = chr(127);
948: }
949:
950: if ($hex == false) {
951: return $escape;
952: }
953: foreach ($escape as $key => $char) {
954: $escape[$key] = '\\x' . str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
955: }
956: return $escape;
957: }
958:
959: 960: 961: 962: 963: 964: 965:
966: public function reset($full = true) {
967: static $initalState = array();
968: if (empty($initalState)) {
969: $initalState = get_class_vars(__CLASS__);
970: }
971: if (!$full) {
972: $this->request = $initalState['request'];
973: $this->response = $initalState['response'];
974: return true;
975: }
976: parent::reset($initalState);
977: return true;
978: }
979: }
980: