1: <?php
2:
3: namespace Alpha\Util\Http;
4:
5: use Alpha\Exception\IllegalArguementException;
6: use Alpha\Util\Config\ConfigProvider;
7:
8: /**
9: * A class to encapsulate a HTTP request.
10: *
11: * @since 2.0
12: *
13: * @author John Collins <dev@alphaframework.org>
14: * @license http://www.opensource.org/licenses/bsd-license.php The BSD License
15: * @copyright Copyright (c) 2015, John Collins (founder of Alpha Framework).
16: * All rights reserved.
17: *
18: * <pre>
19: * Redistribution and use in source and binary forms, with or
20: * without modification, are permitted provided that the
21: * following conditions are met:
22: *
23: * * Redistributions of source code must retain the above
24: * copyright notice, this list of conditions and the
25: * following disclaimer.
26: * * Redistributions in binary form must reproduce the above
27: * copyright notice, this list of conditions and the
28: * following disclaimer in the documentation and/or other
29: * materials provided with the distribution.
30: * * Neither the name of the Alpha Framework nor the names
31: * of its contributors may be used to endorse or promote
32: * products derived from this software without specific
33: * prior written permission.
34: *
35: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
36: * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
37: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
38: * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
39: * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
40: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
41: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
42: * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
43: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
44: * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
45: * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
46: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
47: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
48: * </pre>
49: */
50: class Request
51: {
52: /**
53: * Array of supported HTTP methods.
54: *
55: * @var array
56: *
57: * @since 2.0
58: */
59: private $HTTPMethods = array('HEAD', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'TRACE');
60:
61: /**
62: * The HTTP method of this request (must be in HTTPMethods array).
63: *
64: * @var string
65: *
66: * @since 2.0
67: */
68: private $method;
69:
70: /**
71: * An associative array of HTTP headers on this request.
72: *
73: * @var array
74: *
75: * @since 2.0
76: */
77: private $headers;
78:
79: /**
80: * An associative array of HTTP cookies on this request.
81: *
82: * @var array
83: *
84: * @since 2.0
85: */
86: private $cookies;
87:
88: /**
89: * The HTTP params (form data and query string) on this request.
90: *
91: * @var array
92: *
93: * @since 2.0
94: */
95: private $params;
96:
97: /**
98: * An associative 3D array of uploaded files.
99: *
100: * @var array
101: *
102: * @since 2.0
103: */
104: private $files;
105:
106: /**
107: * The request body if one was provided.
108: *
109: * @var string
110: *
111: * @since 2.0
112: */
113: private $body;
114:
115: /**
116: * The host header provided on the request.
117: *
118: * @var string
119: *
120: * @since 2.0
121: */
122: private $host;
123:
124: /**
125: * The IP of the client making the request.
126: *
127: * @var string
128: *
129: * @since 2.0
130: */
131: private $IP;
132:
133: /**
134: * The URI requested.
135: *
136: * @var string
137: *
138: * @since 2.0
139: */
140: private $URI;
141:
142: /**
143: * The query string provided on the request (if any).
144: *
145: * @var string
146: *
147: * @since 2.0
148: */
149: private $queryString;
150:
151: /**
152: * Builds up the request based on available PHP super globals, in addition to
153: * any overrides provided (useful for testing).
154: *
155: * @param array $overrides Hash array of PHP super globals to override
156: *
157: * @throws Alpha\Exception\IllegalArguementException
158: *
159: * @since 2.0
160: */
161: public function __construct($overrides = array())
162: {
163: // set HTTP headers
164: if (isset($overrides['headers']) && is_array($overrides['headers'])) {
165: $this->headers = $overrides['headers'];
166: } else {
167: $this->headers = $this->getGlobalHeaders();
168: }
169:
170: // set HTTP method
171: if (isset($overrides['method']) && in_array($overrides['method'], $this->HTTPMethods)) {
172: $this->method = $overrides['method'];
173: } elseif (isset($_SERVER['REQUEST_METHOD']) && in_array($_SERVER['REQUEST_METHOD'], $this->HTTPMethods)) {
174: $this->method = $_SERVER['REQUEST_METHOD'];
175: }
176:
177: // allow the POST param _METHOD to override the HTTP method
178: if (isset($_POST['_METHOD']) && in_array($_POST['_METHOD'], $this->HTTPMethods)) {
179: $this->method = $_POST['_METHOD'];
180: }
181:
182: // allow the POST param X-HTTP-Method-Override to override the HTTP method
183: if (isset($this->headers['X-HTTP-Method-Override']) && in_array($this->headers['X-HTTP-Method-Override'], $this->HTTPMethods)) {
184: $this->method = $this->headers['X-HTTP-Method-Override'];
185: }
186:
187: if ($this->method == '') {
188: throw new IllegalArguementException('No valid HTTP method provided when creating new Request object');
189: }
190:
191: // set HTTP cookies
192: if (isset($overrides['cookies']) && is_array($overrides['cookies'])) {
193: $this->cookies = $overrides['cookies'];
194: } elseif (isset($_COOKIE)) {
195: $this->cookies = $_COOKIE;
196: } else {
197: $this->cookies = array();
198: }
199:
200: // set HTTP params
201: if (isset($overrides['params']) && is_array($overrides['params'])) {
202: $this->params = $overrides['params'];
203: } else {
204: $this->params = array();
205:
206: if (isset($_GET)) {
207: $this->params = array_merge($this->params, $_GET);
208: }
209:
210: if (isset($_POST)) {
211: $this->params = array_merge($this->params, $_POST);
212: }
213: }
214:
215: // set HTTP body
216: if (isset($overrides['body'])) {
217: $this->body = $overrides['body'];
218: } else {
219: $this->body = $this->getGlobalBody();
220: }
221:
222: // set HTTP host
223: if (isset($overrides['host'])) {
224: $this->host = $overrides['host'];
225: } elseif (isset($_SERVER['HTTP_HOST'])) {
226: $this->host = $_SERVER['HTTP_HOST'];
227: } else {
228: $this->host = 'localhost';
229: }
230:
231: // set IP of the client
232: if (isset($overrides['IP'])) {
233: $this->IP = $overrides['IP'];
234: } elseif (isset($_SERVER['REMOTE_ADDR'])) {
235: $this->IP = $_SERVER['REMOTE_ADDR'];
236: } else {
237: $this->IP = '127.0.0.1';
238: }
239:
240: // set requested URI
241: if (isset($overrides['URI'])) {
242: $this->URI = $overrides['URI'];
243: } elseif (isset($_SERVER['REQUEST_URI'])) {
244: $this->URI = $_SERVER['REQUEST_URI'];
245: }
246:
247: // set uploaded files (if any)
248: if (isset($overrides['files'])) {
249: $this->files = $overrides['files'];
250: } elseif (isset($_FILES)) {
251: $this->files = $_FILES;
252: }
253: }
254:
255: /**
256: * Get the HTTP method of this request.
257: *
258: * @return string
259: *
260: * @since 2.0
261: */
262: public function getMethod()
263: {
264: return $this->method;
265: }
266:
267: /**
268: * Set the HTTP method of this request.
269: *
270: * @param string $method
271: *
272: * @throws Alpha\Exception\IllegalArguementException
273: *
274: * @since 2.0
275: */
276: public function setMethod($method)
277: {
278: if (in_array($method, $this->HTTPMethods)) {
279: $this->method = $method;
280: } else {
281: throw new IllegalArguementException('The method provided ['.$method.'] is not valid!');
282: }
283: }
284:
285: /**
286: * Return all headers on this request.
287: *
288: * @return array
289: *
290: * @since 2.0
291: */
292: public function getHeaders()
293: {
294: return $this->headers;
295: }
296:
297: /**
298: * Get the header matching the key provided.
299: *
300: * @param string $key The key to search for
301: * @param mixed $default If key is not found, return this instead
302: *
303: * @return mixed
304: *
305: * @since 2.0
306: */
307: public function getHeader($key, $default = null)
308: {
309: if (array_key_exists($key, $this->headers)) {
310: return $this->headers[$key];
311: } else {
312: return $default;
313: }
314: }
315:
316: /**
317: * Tries to get the current HTTP request headers from supoer globals.
318: *
319: * @return array
320: *
321: * @since 2.0
322: */
323: private function getGlobalHeaders()
324: {
325: if (!function_exists('getallheaders')) {
326: $headers = array();
327: foreach ($_SERVER as $name => $value) {
328: if (substr($name, 0, 5) == 'HTTP_') {
329: $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
330: }
331: if ($name == 'CONTENT_TYPE') {
332: $headers['Content-Type'] = $value;
333: }
334: if ($name == 'CONTENT_LENGTH') {
335: $headers['Content-Length'] = $value;
336: }
337: }
338:
339: return $headers;
340: } else {
341: return getallheaders();
342: }
343: }
344:
345: /**
346: * Return all cookies on this request.
347: *
348: * @return array
349: *
350: * @since 2.0
351: */
352: public function getCookies()
353: {
354: return $this->cookies;
355: }
356:
357: /**
358: * Get the cookie matching the key provided.
359: *
360: * @param string $key The key to search for
361: * @param mixed $default If key is not found, return this instead
362: *
363: * @return mixed
364: *
365: * @since 2.0
366: */
367: public function getCookie($key, $default = null)
368: {
369: if (array_key_exists($key, $this->cookies)) {
370: return $this->cookies[$key];
371: } else {
372: return $default;
373: }
374: }
375:
376: /**
377: * Return all params on this request.
378: *
379: * @return array
380: *
381: * @since 2.0
382: */
383: public function getParams()
384: {
385: return $this->params;
386: }
387:
388: /**
389: * Get the param matching the key provided.
390: *
391: * @param string $key The key to search for
392: * @param mixed $default If key is not found, return this instead
393: *
394: * @return mixed
395: *
396: * @since 2.0
397: */
398: public function getParam($key, $default = null)
399: {
400: if (array_key_exists($key, $this->params)) {
401: return $this->params[$key];
402: } else {
403: return $default;
404: }
405: }
406:
407: /**
408: * Append the hash array provided to the params for this request.
409: *
410: * @param array A hash array of values to add to the request params
411: *
412: * @since 2.0
413: */
414: public function addParams($params)
415: {
416: if (is_array($params)) {
417: $this->params = array_merge($this->params, $params);
418: }
419: }
420:
421: /**
422: * Set the params array.
423: *
424: * @param array A hash array of values to set as the request params
425: *
426: * @since 2.0
427: */
428: public function setParams($params)
429: {
430: if (is_array($params)) {
431: $this->params = $params;
432: }
433: }
434:
435: /**
436: * Return all files on this request.
437: *
438: * @return array
439: *
440: * @since 2.0
441: */
442: public function getFiles()
443: {
444: return $this->files;
445: }
446:
447: /**
448: * Get the file matching the key provided.
449: *
450: * @param string $key The key to search for
451: * @param mixed $default If key is not found, return this instead
452: *
453: * @return mixed
454: *
455: * @since 2.0
456: */
457: public function getFile($key, $default = null)
458: {
459: if (array_key_exists($key, $this->files)) {
460: return $this->files[$key];
461: } else {
462: return $default;
463: }
464: }
465:
466: /**
467: * Get the request body if one was provided.
468: *
469: * @return string
470: *
471: * @since 2.0
472: */
473: public function getBody()
474: {
475: return $this->body;
476: }
477:
478: /**
479: * Attempts to get the raw body of the current request from super globals.
480: *
481: * @return string
482: *
483: * @since 2.0
484: */
485: private function getGlobalBody()
486: {
487: if (isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
488: return $GLOBALS['HTTP_RAW_POST_DATA'];
489: } else {
490: return file_get_contents('php://input');
491: }
492: }
493:
494: /**
495: * Get the Accept header of the request.
496: *
497: * @return string
498: *
499: * @since 2.0
500: */
501: public function getAccept()
502: {
503: return $this->getHeader('Accept');
504: }
505:
506: /**
507: * Get the Content-Type header of the request.
508: *
509: * @return string
510: *
511: * @since 2.0
512: */
513: public function getContentType()
514: {
515: return $this->getHeader('Content-Type');
516: }
517:
518: /**
519: * Get the Content-Length header of the request.
520: *
521: * @return string
522: *
523: * @since 2.0
524: */
525: public function getContentLength()
526: {
527: return $this->getHeader('Content-Length');
528: }
529:
530: /**
531: * Get the host name of the client that sent the request.
532: *
533: * @return string
534: *
535: * @since 2.0
536: */
537: public function getHost()
538: {
539: return $this->host;
540: }
541:
542: /**
543: * Get the URI that was requested.
544: *
545: * @return string
546: *
547: * @since 2.0
548: */
549: public function getURI()
550: {
551: return $this->URI;
552: }
553:
554: /**
555: * Get the URL that was requested.
556: *
557: * @return string
558: *
559: * @since 2.0
560: */
561: public function getURL()
562: {
563: $config = ConfigProvider::getInstance();
564:
565: return $config->get('app.url').$this->getURI();
566: }
567:
568: /**
569: * Get the IP address of the client that sent the request.
570: *
571: * @return string
572: *
573: * @since 2.0
574: */
575: public function getIP()
576: {
577: return $this->IP;
578: }
579:
580: /**
581: * Get the Referrer header of the request.
582: *
583: * @return string
584: *
585: * @since 2.0
586: */
587: public function getReferrer()
588: {
589: return $this->getHeader('Referrer');
590: }
591:
592: /**
593: * Get the User-Agent header of the request.
594: *
595: * @return string
596: *
597: * @since 2.0
598: */
599: public function getUserAgent()
600: {
601: return $this->getHeader('User-Agent');
602: }
603:
604: /**
605: * Get the query string provided on the request.
606: *
607: * @return string
608: *
609: * @since 2.0
610: */
611: public function getQueryString()
612: {
613: return $this->queryString;
614: }
615:
616: /**
617: * Parses the route provided to extract matching params of the route from this request's URI.
618: *
619: * @param string $route The route with parameter names, e.g. /user/{username}
620: * @param array $defaultParams Optional hash array of default request param values to use if they are missing from URI
621: *
622: * @since 2.0
623: */
624: public function parseParamsFromRoute($route, $defaultParams = array())
625: {
626: // if the URI has a query-string, we will ignore it for now
627: if (mb_strpos($this->URI, '?') !== false) {
628: $URI = mb_substr($this->URI, 0, mb_strpos($this->URI, '?'));
629:
630: // let's take this opportunity to pass query string params to $this->params
631: $queryString = mb_substr($this->URI, (mb_strpos($this->URI, '?') + 1));
632: $this->queryString = $queryString;
633: parse_str($queryString, $this->params);
634: } else {
635: $URI = $this->URI;
636: }
637:
638: $paramNames = explode('/', $route);
639: $paramValues = explode('/', $URI);
640:
641: for ($i = 0; $i < count($paramNames); ++$i) {
642: $name = $paramNames[$i];
643:
644: if (!isset($this->params[trim($name, '{}')])) {
645: if (isset($paramValues[$i]) && substr($name, 0, 1) == '{' && substr($name, strlen($name) - 1, 1) == '}') {
646: $this->params[trim($name, '{}')] = $paramValues[$i];
647: }
648: if (!isset($paramValues[$i]) && isset($defaultParams[trim($name, '{}')])) {
649: $this->params[trim($name, '{}')] = $defaultParams[trim($name, '{}')];
650: }
651: }
652: }
653: }
654:
655: /**
656: * Checks to see if the request contains a secure/encrypted token.
657: *
658: * @return bool
659: *
660: * @since 2.0
661: */
662: public function isSecureURI()
663: {
664: if (isset($this->params['act']) && mb_strpos($this->URI, '/tk/') !== false) {
665: return true;
666: } else {
667: return false;
668: }
669: }
670: }
671: