1<?php
2namespace go\core\http;
3
4use Exception;
5use go\core\Singleton;
6use go\core\util\StringUtil;
7use stdClass;
8
9
10/**
11 * The HTTP request class.
12 *
13 * <p>Example:</p>
14 * ```````````````````````````````````````````````````````````````````````````
15 * $var = IFW::app()->request()->queryParams['someVar'];
16 *
17 * //Get the JSON or XML data
18 * $var = IFW::app()->request()->payload['somevar'];
19 * ```````````````````````````````````````````````````````````````````````````
20 *
21 *
22 * @copyright (c) 2014, Intermesh BV http://www.intermesh.nl
23 * @author Merijn Schering <mschering@intermesh.nl>
24 * @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3
25 */
26class Request extends Singleton{
27
28	/**
29	 * The body of the request. Only JSON is supported at the moment.
30	 *
31	 * @var mixed[]
32	 */
33	private $body;
34
35
36	/**
37	 * The request headers
38	 *
39	 * @var string[]
40	 */
41	private $headers;
42
43	/**
44	 * Get all query parameters of this request
45	 *
46	 * @return array ['paramName' => 'value']
47	 */
48	public function getQueryParams() {
49		return $_GET;
50	}
51
52
53	/**
54	 * Get a query parameter by name
55	 *
56	 * @return string|bool false if not set.
57	 */
58	public function getQueryParam($name) {
59		return $_GET[$name] ?? false;
60	}
61
62  /**
63   * Get the values of the Accept header in lower case
64   *
65   * @param string[]
66   * @return array
67   */
68	public function getAccept() {
69
70		if(empty($_SERVER['HTTP_ACCEPT'])) {
71			return [];
72		}
73
74		$accept = explode(',', strtolower($_SERVER['HTTP_ACCEPT']));
75		return array_map('trim', $accept);
76	}
77
78	/**
79	 * Get the accepted languages sent by the request in lower case
80	 *
81	 * @return string[]
82	 */
83	public function getAcceptLanguages() {
84		if(empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
85			return [];
86		}
87
88		$accept = explode(',', strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE']));
89		return array_map('trim', $accept);
90	}
91
92	/**
93	 * Get the request headers as a key value array. The header names are in lower case.
94	 *
95	 * Example:
96	 *
97	 * ```````````````````````````````````````````````````````````````````````````
98	 * [
99	 * 'accept' => 'application/json',
100	 * 'accept-language' => 'en-us'
101	 * ]
102	 * ```````````````````````````````````````````````````````````````````````````
103	 *
104	 * @return array
105	 */
106	public function getHeaders() {
107
108		if (!isset($this->headers)) {
109			if(!function_exists('apache_request_headers'))
110			{
111				$this->headers = $this->getNonApacheHeaders();
112			} else{
113				$this->headers = array_change_key_case(apache_request_headers(),CASE_LOWER);
114			}
115		}
116		return $this->headers;
117	}
118
119	private function getNonApacheHeaders() {
120		$headers = array();
121		$copy_server = array(
122				'CONTENT_TYPE' => 'content-type',
123				'CONTENT_LENGTH' => 'content-length',
124				'CONTENT_MD5' => 'content-md5',
125		);
126		foreach ($_SERVER as $key => $value) {
127			if (substr($key, 0, 5) === 'HTTP_') {
128				$key = substr($key, 5);
129				if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) {
130					$key = str_replace(' ', '-', strtolower(str_replace('_', ' ', $key)));
131					$headers[$key] = $value;
132				}
133			} elseif (isset($copy_server[$key])) {
134				$headers[$copy_server[$key]] = $value;
135			}
136		}
137		if (!isset($headers['authorization'])) {
138			if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
139				$headers['authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
140			} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
141				$basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '';
142				$headers['authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass);
143			} elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
144				$headers['authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
145			}
146		}
147		return $headers;
148	}
149
150	/**
151	 * Get request header value
152	 *
153	 * @param string $name
154	 * @param string
155	 */
156	public function getHeader($name) {
157		$name = strtolower($name);
158		$headers = $this->getHeaders();
159		return isset($headers[$name]) ? $headers[$name] : null;
160	}
161
162	/**
163	 * Get the request payload
164	 *
165	 * The data send in the body of the request.
166	 * We support:
167	 *
168	 * #application/x-www-form-urlencoded
169	 * #multipart/form-data
170	 * #application/json
171	 * #application/xml or text/xml
172	 *
173	 * @return stdClass
174	 */
175	public function getBody() {
176		if (!isset($this->body)) {
177			//If it's a form post (application/x-www-form-urlencoded or multipart/form-data) with HTML then PHP already built the data
178			if(!empty($_POST)) {
179				$this->body = $_POST;
180			}else if($this->isJson())
181			{
182				if(empty($this->getRawBody())) {
183					$this->body = [];
184				}else {
185					$this->body = json_decode($this->getRawBody(), true);
186
187					// Check if the post is filled with an array. Otherwise make it an empty array.
188					if(!isset($this->body)){
189						throw new Exception("JSON decoding error: '".json_last_error_msg()."'.\n\nJSON data from client: \n\n".var_export($this->getRawBody(), true));
190					}
191				}
192			}
193		}
194
195		return $this->body;
196	}
197
198  /**
199   * Get raw request body as string.
200   *
201   * @return string
202   */
203	public function getRawBody() {
204		if(!isset($this->rawBody)) {
205			$this->rawBody = file_get_contents('php://input');
206		}
207
208		return $this->rawBody;
209	}
210
211	/**
212	 * Get's the content type header
213	 *
214	 * @preturn string
215	 */
216	public function getContentType() {
217		return isset($_SERVER["CONTENT_TYPE"]) ? $_SERVER["CONTENT_TYPE"] : '';
218	}
219
220	/**
221	 * Get the request method in upper case
222	 *
223	 * @return string PUT, POST, DELETE, GET, PATCH, HEAD
224	 */
225	public function getMethod() {
226		return strtoupper($_SERVER['REQUEST_METHOD']);
227	}
228
229	/**
230	 * Check if the request posted a JSON body
231	 *
232	 * @return boolean
233	 */
234	public function isJson() {
235		return strpos($this->getContentType(), 'application/json') !== false;
236	}
237
238	/**
239	 * Check if the request posted a JSON body
240	 *
241	 * @return boolean
242	 */
243	private function isXml() {
244		return strpos($this->getContentType(), '/xml') !== false;
245	}
246
247	/**
248	 * Check if this request SSL secured
249	 *
250	 * @return boolean
251	 */
252	public function isHttps() {
253		if(!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') {
254			return true;
255		}
256
257		if(!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') {
258			return true;
259		}
260
261		return false;
262	}
263
264	private $host;
265	/**
266	 * Get the host name of the request.
267	 *
268	 * @param bool $stripPort remove :1234 from result
269	 * @return string eg. localhost:6480
270	 */
271	public function getHost($stripPort = true) {
272
273		if(!isset($this->host)) {
274			$possibleHostSources = array('HTTP_X_FORWARDED_HOST', 'HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR');
275			$sourceTransformations = array(
276				"HTTP_X_FORWARDED_HOST" => function ($value) {
277					$elements = explode(',', $value);
278					return trim(end($elements));
279				}
280			);
281			$this->host = '';
282			foreach ($possibleHostSources as $source) {
283				if (!empty($this->host)) break;
284				if (empty($_SERVER[$source])) continue;
285				$this->host = $_SERVER[$source];
286				if (array_key_exists($source, $sourceTransformations)) {
287					$this->host = $sourceTransformations[$source]($this->host);
288				}
289			}
290
291			$this->host = trim($this->host);
292		}
293
294    // Remove port number from host
295		if($stripPort) {
296			return preg_replace('/:\d+$/', '', $this->host);
297		}
298
299    return $this->host;
300	}
301
302//	/**
303//	 * Get port number of request
304//	 *
305//	 * @return int| false
306//	 */
307//	public function getPort() {
308//		$possibleHostSources = array('HTTP_X_FORWARDED_HOST', 'HTTP_HOST');
309//		$host = '';
310//		foreach ($possibleHostSources as $source)
311//		{
312//			if (!empty($host)) break;
313//			if (empty($_SERVER[$source])) continue;
314//			$host = $_SERVER[$source];
315//			if (array_key_exists($source, $sourceTransformations))
316//			{
317//				$host = $sourceTransformations[$source]($host);
318//			}
319//		}
320//
321//		$pos = strpos($host, ':');
322//
323//		if($pos === false) {
324//			return false;
325//		}
326//
327//		return (int) substr($host, $pos);
328//
329//	}
330
331  /**
332   * Get the IP address of the user's client
333   *
334   * @return string
335   */
336	public function getRemoteIpAddress() {
337    if(!empty($_SERVER['HTTP_CLIENT_IP'])){
338      //ip from share internet
339      return $_SERVER['HTTP_CLIENT_IP'];
340    }elseif(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])){
341      //ip pass from proxy
342      return $_SERVER['HTTP_X_FORWARDED_FOR'];
343    }else{
344      return $_SERVER['REMOTE_ADDR'] ?? null;
345    }
346  }
347
348  /**
349   * Check if this request is an XMLHttpRequest
350   *    *
351   * @return boolean
352   */
353  public function isXHR() {
354    return isset($_SERVER["HTTP_X_REQUESTED_WITH"]) && $_SERVER["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest";
355  }
356
357
358  /**
359   * Decode a HTTP header to UTF-8
360   *
361   * @link https://tools.ietf.org/html/rfc5987
362   * @param $string
363   * @return bool|string
364   */
365  public static function headerDecode($string) {
366      $pos = strpos($string, "''");
367      if($pos == false || $pos > 64) {
368        return false;
369      }
370			//eg. iso-8859-1''%66%6F%73%73%2D%69%74%2D%73%6D%61%6C%6C%2E%67%69%66
371			$charset = substr($string, 0, $pos);
372
373			$string = rawurldecode(substr($string, $pos + 2));
374
375			return StringUtil::cleanUtf8($string, $charset);
376  }
377}
378