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 = Set::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 = Set::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'] = $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 = Set::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 = Set::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 = Set::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 = Set::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 = Set::merge($this->config, $config);
642: $this->config = Set::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: if (is_array($query)) {
772: return $query;
773: }
774: $parsedQuery = array();
775:
776: if (is_string($query) && !empty($query)) {
777: $query = preg_replace('/^\?/', '', $query);
778: $items = explode('&', $query);
779:
780: foreach ($items as $item) {
781: if (strpos($item, '=') !== false) {
782: list($key, $value) = explode('=', $item, 2);
783: } else {
784: $key = $item;
785: $value = null;
786: }
787:
788: $key = urldecode($key);
789: $value = urldecode($value);
790:
791: if (preg_match_all('/\[([^\[\]]*)\]/iUs', $key, $matches)) {
792: $subKeys = $matches[1];
793: $rootKey = substr($key, 0, strpos($key, '['));
794: if (!empty($rootKey)) {
795: array_unshift($subKeys, $rootKey);
796: }
797: $queryNode =& $parsedQuery;
798:
799: foreach ($subKeys as $subKey) {
800: if (!is_array($queryNode)) {
801: $queryNode = array();
802: }
803:
804: if ($subKey === '') {
805: $queryNode[] = array();
806: end($queryNode);
807: $subKey = key($queryNode);
808: }
809: $queryNode =& $queryNode[$subKey];
810: }
811: $queryNode = $value;
812: continue;
813: }
814: if (!isset($parsedQuery[$key])) {
815: $parsedQuery[$key] = $value;
816: } else {
817: $parsedQuery[$key] = (array)$parsedQuery[$key];
818: $parsedQuery[$key][] = $value;
819: }
820: }
821: }
822: return $parsedQuery;
823: }
824:
825: 826: 827: 828: 829: 830: 831: 832:
833: protected function _buildRequestLine($request = array(), $versionToken = 'HTTP/1.1') {
834: $asteriskMethods = array('OPTIONS');
835:
836: if (is_string($request)) {
837: $isValid = preg_match("/(.+) (.+) (.+)\r\n/U", $request, $match);
838: if (!$this->quirksMode && (!$isValid || ($match[2] == '*' && !in_array($match[3], $asteriskMethods)))) {
839: throw new SocketException(__d('cake_dev', 'HttpSocket::_buildRequestLine - Passed an invalid request line string. Activate quirks mode to do this.'));
840: }
841: return $request;
842: } elseif (!is_array($request)) {
843: return false;
844: } elseif (!array_key_exists('uri', $request)) {
845: return false;
846: }
847:
848: $request['uri'] = $this->_parseUri($request['uri']);
849: $request = array_merge(array('method' => 'GET'), $request);
850: if (!empty($this->_proxy['host'])) {
851: $request['uri'] = $this->_buildUri($request['uri'], '%scheme://%host:%port/%path?%query');
852: } else {
853: $request['uri'] = $this->_buildUri($request['uri'], '/%path?%query');
854: }
855:
856: if (!$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) {
857: 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)));
858: }
859: return $request['method'] . ' ' . $request['uri'] . ' ' . $versionToken . "\r\n";
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: }
981: