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