Passed
Push — master ( e4948f...76bdce )
by John
09:45 queued 10s
created

Request::getHeader()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 11
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 20
rs 9.2222
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 *
6
 * @author Bart Visscher <[email protected]>
7
 * @author Bernhard Posselt <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author coderkun <[email protected]>
10
 * @author Joas Schilling <[email protected]>
11
 * @author Juan Pablo Villafáñez <[email protected]>
12
 * @author Jörn Friedrich Dreyer <[email protected]>
13
 * @author Lukas Reschke <[email protected]>
14
 * @author Mitar <[email protected]>
15
 * @author Morris Jobke <[email protected]>
16
 * @author Robin Appelman <[email protected]>
17
 * @author Robin McCorkell <[email protected]>
18
 * @author Roeland Jago Douma <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Thomas Tanghus <[email protected]>
21
 * @author Vincent Petry <[email protected]>
22
 *
23
 * @license AGPL-3.0
24
 *
25
 * This code is free software: you can redistribute it and/or modify
26
 * it under the terms of the GNU Affero General Public License, version 3,
27
 * as published by the Free Software Foundation.
28
 *
29
 * This program is distributed in the hope that it will be useful,
30
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
31
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
32
 * GNU Affero General Public License for more details.
33
 *
34
 * You should have received a copy of the GNU Affero General Public License, version 3,
35
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
36
 *
37
 */
38
39
namespace OC\AppFramework\Http;
40
41
use OC\Security\CSRF\CsrfToken;
42
use OC\Security\CSRF\CsrfTokenManager;
43
use OC\Security\TrustedDomainHelper;
44
use OCP\IConfig;
45
use OCP\IRequest;
46
use OCP\Security\ICrypto;
47
use OCP\Security\ISecureRandom;
48
49
/**
50
 * Class for accessing variables in the request.
51
 * This class provides an immutable object with request variables.
52
 *
53
 * @property mixed[] cookies
54
 * @property mixed[] env
55
 * @property mixed[] files
56
 * @property string method
57
 * @property mixed[] parameters
58
 * @property mixed[] server
59
 */
60
class Request implements \ArrayAccess, \Countable, IRequest {
61
62
	const USER_AGENT_IE = '/(MSIE)|(Trident)/';
63
	// Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
64
	const USER_AGENT_MS_EDGE = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+ Edge\/[0-9.]+$/';
65
	// Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference
66
	const USER_AGENT_FIREFOX = '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/[0-9.]+$/';
67
	// Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
68
	const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)( Ubuntu Chromium\/[0-9.]+|) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+$/';
69
	// Safari User Agent from http://www.useragentstring.com/pages/Safari/
70
	const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/';
71
	// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
72
	const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#';
73
	const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#';
74
	const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost|::1)$/';
75
76
	/**
77
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_IOS instead
78
	 */
79
	const USER_AGENT_OWNCLOUD_IOS = '/^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-iOS.*$/';
80
	/**
81
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_ANDROID instead
82
	 */
83
	const USER_AGENT_OWNCLOUD_ANDROID = '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/';
84
	/**
85
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_DESKTOP instead
86
	 */
87
	const USER_AGENT_OWNCLOUD_DESKTOP = '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/';
88
89
	protected $inputStream;
90
	protected $content;
91
	protected $items = [];
92
	protected $allowedKeys = [
93
		'get',
94
		'post',
95
		'files',
96
		'server',
97
		'env',
98
		'cookies',
99
		'urlParams',
100
		'parameters',
101
		'method',
102
		'requesttoken',
103
	];
104
	/** @var ISecureRandom */
105
	protected $secureRandom;
106
	/** @var IConfig */
107
	protected $config;
108
	/** @var string */
109
	protected $requestId = '';
110
	/** @var ICrypto */
111
	protected $crypto;
112
	/** @var CsrfTokenManager|null */
113
	protected $csrfTokenManager;
114
115
	/** @var bool */
116
	protected $contentDecoded = false;
117
118
	/**
119
	 * @param array $vars An associative array with the following optional values:
120
	 *        - array 'urlParams' the parameters which were matched from the URL
121
	 *        - array 'get' the $_GET array
122
	 *        - array|string 'post' the $_POST array or JSON string
123
	 *        - array 'files' the $_FILES array
124
	 *        - array 'server' the $_SERVER array
125
	 *        - array 'env' the $_ENV array
126
	 *        - array 'cookies' the $_COOKIE array
127
	 *        - string 'method' the request method (GET, POST etc)
128
	 *        - string|false 'requesttoken' the requesttoken or false when not available
129
	 * @param ISecureRandom $secureRandom
130
	 * @param IConfig $config
131
	 * @param CsrfTokenManager|null $csrfTokenManager
132
	 * @param string $stream
133
	 * @see http://www.php.net/manual/en/reserved.variables.php
134
	 */
135
	public function __construct(array $vars= [],
136
								ISecureRandom $secureRandom = null,
137
								IConfig $config,
138
								CsrfTokenManager $csrfTokenManager = null,
139
								string $stream = 'php://input') {
140
		$this->inputStream = $stream;
141
		$this->items['params'] = [];
142
		$this->secureRandom = $secureRandom;
143
		$this->config = $config;
144
		$this->csrfTokenManager = $csrfTokenManager;
145
146
		if(!array_key_exists('method', $vars)) {
147
			$vars['method'] = 'GET';
148
		}
149
150
		foreach($this->allowedKeys as $name) {
151
			$this->items[$name] = isset($vars[$name])
152
				? $vars[$name]
153
				: [];
154
		}
155
156
		$this->items['parameters'] = array_merge(
157
			$this->items['get'],
158
			$this->items['post'],
159
			$this->items['urlParams'],
160
			$this->items['params']
161
		);
162
163
	}
164
	/**
165
	 * @param array $parameters
166
	 */
167
	public function setUrlParameters(array $parameters) {
168
		$this->items['urlParams'] = $parameters;
169
		$this->items['parameters'] = array_merge(
170
			$this->items['parameters'],
171
			$this->items['urlParams']
172
		);
173
	}
174
175
	/**
176
	 * Countable method
177
	 * @return int
178
	 */
179
	public function count(): int {
180
		return \count($this->items['parameters']);
181
	}
182
183
	/**
184
	* ArrayAccess methods
185
	*
186
	* Gives access to the combined GET, POST and urlParams arrays
187
	*
188
	* Examples:
189
	*
190
	* $var = $request['myvar'];
191
	*
192
	* or
193
	*
194
	* if(!isset($request['myvar']) {
195
	* 	// Do something
196
	* }
197
	*
198
	* $request['myvar'] = 'something'; // This throws an exception.
199
	*
200
	* @param string $offset The key to lookup
201
	* @return boolean
202
	*/
203
	public function offsetExists($offset): bool {
204
		return isset($this->items['parameters'][$offset]);
205
	}
206
207
	/**
208
	 * @see offsetExists
209
	 * @param string $offset
210
	 * @return mixed
211
	 */
212
	public function offsetGet($offset) {
213
		return isset($this->items['parameters'][$offset])
214
			? $this->items['parameters'][$offset]
215
			: null;
216
	}
217
218
	/**
219
	 * @see offsetExists
220
	 * @param string $offset
221
	 * @param mixed $value
222
	 */
223
	public function offsetSet($offset, $value) {
224
		throw new \RuntimeException('You cannot change the contents of the request object');
225
	}
226
227
	/**
228
	 * @see offsetExists
229
	 * @param string $offset
230
	 */
231
	public function offsetUnset($offset) {
232
		throw new \RuntimeException('You cannot change the contents of the request object');
233
	}
234
235
	/**
236
	 * Magic property accessors
237
	 * @param string $name
238
	 * @param mixed $value
239
	 */
240
	public function __set($name, $value) {
241
		throw new \RuntimeException('You cannot change the contents of the request object');
242
	}
243
244
	/**
245
	* Access request variables by method and name.
246
	* Examples:
247
	*
248
	* $request->post['myvar']; // Only look for POST variables
249
	* $request->myvar; or $request->{'myvar'}; or $request->{$myvar}
250
	* Looks in the combined GET, POST and urlParams array.
251
	*
252
	* If you access e.g. ->post but the current HTTP request method
253
	* is GET a \LogicException will be thrown.
254
	*
255
	* @param string $name The key to look for.
256
	* @throws \LogicException
257
	* @return mixed|null
258
	*/
259
	public function __get($name) {
260
		switch($name) {
261
			case 'put':
262
			case 'patch':
263
			case 'get':
264
			case 'post':
265
				if($this->method !== strtoupper($name)) {
266
					throw new \LogicException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method));
267
				}
268
				return $this->getContent();
269
			case 'files':
270
			case 'server':
271
			case 'env':
272
			case 'cookies':
273
			case 'urlParams':
274
			case 'method':
275
				return isset($this->items[$name])
276
					? $this->items[$name]
277
					: null;
278
			case 'parameters':
279
			case 'params':
280
				return $this->getContent();
281
			default;
282
				return isset($this[$name])
283
					? $this[$name]
284
					: null;
285
		}
286
	}
287
288
	/**
289
	 * @param string $name
290
	 * @return bool
291
	 */
292
	public function __isset($name) {
293
		if (\in_array($name, $this->allowedKeys, true)) {
294
			return true;
295
		}
296
		return isset($this->items['parameters'][$name]);
297
	}
298
299
	/**
300
	 * @param string $id
301
	 */
302
	public function __unset($id) {
303
		throw new \RuntimeException('You cannot change the contents of the request object');
304
	}
305
306
	/**
307
	 * Returns the value for a specific http header.
308
	 *
309
	 * This method returns null if the header did not exist.
310
	 *
311
	 * @param string $name
312
	 * @return string
313
	 */
314
	public function getHeader(string $name): string {
315
316
		$name = strtoupper(str_replace('-', '_',$name));
317
		if (isset($this->server['HTTP_' . $name])) {
318
			return $this->server['HTTP_' . $name];
319
		}
320
321
		// There's a few headers that seem to end up in the top-level
322
		// server array.
323
		switch ($name) {
324
			case 'CONTENT_TYPE' :
325
			case 'CONTENT_LENGTH' :
326
			case 'REMOTE_ADDR':
327
				if (isset($this->server[$name])) {
328
					return $this->server[$name];
329
				}
330
				break;
331
		}
332
333
		return '';
334
	}
335
336
	/**
337
	 * Lets you access post and get parameters by the index
338
	 * In case of json requests the encoded json body is accessed
339
	 *
340
	 * @param string $key the key which you want to access in the URL Parameter
341
	 *                     placeholder, $_POST or $_GET array.
342
	 *                     The priority how they're returned is the following:
343
	 *                     1. URL parameters
344
	 *                     2. POST parameters
345
	 *                     3. GET parameters
346
	 * @param mixed $default If the key is not found, this value will be returned
347
	 * @return mixed the content of the array
348
	 */
349
	public function getParam(string $key, $default = null) {
350
		return isset($this->parameters[$key])
351
			? $this->parameters[$key]
352
			: $default;
353
	}
354
355
	/**
356
	 * Returns all params that were received, be it from the request
357
	 * (as GET or POST) or throuh the URL by the route
358
	 * @return array the array with all parameters
359
	 */
360
	public function getParams(): array {
361
		return is_array($this->parameters) ? $this->parameters : [];
0 ignored issues
show
introduced by
The condition is_array($this->parameters) is always true.
Loading history...
362
	}
363
364
	/**
365
	 * Returns the method of the request
366
	 * @return string the method of the request (POST, GET, etc)
367
	 */
368
	public function getMethod(): string {
369
		return $this->method;
370
	}
371
372
	/**
373
	 * Shortcut for accessing an uploaded file through the $_FILES array
374
	 * @param string $key the key that will be taken from the $_FILES array
375
	 * @return array the file in the $_FILES element
376
	 */
377
	public function getUploadedFile(string $key) {
378
		return isset($this->files[$key]) ? $this->files[$key] : null;
379
	}
380
381
	/**
382
	 * Shortcut for getting env variables
383
	 * @param string $key the key that will be taken from the $_ENV array
384
	 * @return array the value in the $_ENV element
385
	 */
386
	public function getEnv(string $key) {
387
		return isset($this->env[$key]) ? $this->env[$key] : null;
388
	}
389
390
	/**
391
	 * Shortcut for getting cookie variables
392
	 * @param string $key the key that will be taken from the $_COOKIE array
393
	 * @return string the value in the $_COOKIE element
394
	 */
395
	public function getCookie(string $key) {
396
		return isset($this->cookies[$key]) ? $this->cookies[$key] : null;
397
	}
398
399
	/**
400
	 * Returns the request body content.
401
	 *
402
	 * If the HTTP request method is PUT and the body
403
	 * not application/x-www-form-urlencoded or application/json a stream
404
	 * resource is returned, otherwise an array.
405
	 *
406
	 * @return array|string|resource The request body content or a resource to read the body stream.
407
	 *
408
	 * @throws \LogicException
409
	 */
410
	protected function getContent() {
411
		// If the content can't be parsed into an array then return a stream resource.
412
		if ($this->method === 'PUT'
413
			&& $this->getHeader('Content-Length') !== '0'
414
			&& $this->getHeader('Content-Length') !== ''
415
			&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false
416
			&& strpos($this->getHeader('Content-Type'), 'application/json') === false
417
		) {
418
			if ($this->content === false) {
419
				throw new \LogicException(
420
					'"put" can only be accessed once if not '
421
					. 'application/x-www-form-urlencoded or application/json.'
422
				);
423
			}
424
			$this->content = false;
425
			return fopen($this->inputStream, 'rb');
0 ignored issues
show
Bug Best Practice introduced by
The expression return fopen($this->inputStream, 'rb') could also return false which is incompatible with the documented return type array|resource|string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
426
		} else {
427
			$this->decodeContent();
428
			return $this->items['parameters'];
429
		}
430
	}
431
432
	/**
433
	 * Attempt to decode the content and populate parameters
434
	 */
435
	protected function decodeContent() {
436
		if ($this->contentDecoded) {
437
			return;
438
		}
439
		$params = [];
440
441
		// 'application/json' must be decoded manually.
442
		if (strpos($this->getHeader('Content-Type'), 'application/json') !== false) {
443
			$params = json_decode(file_get_contents($this->inputStream), true);
444
			if($params !== null && \count($params) > 0) {
445
				$this->items['params'] = $params;
446
				if($this->method === 'POST') {
447
					$this->items['post'] = $params;
448
				}
449
			}
450
451
		// Handle application/x-www-form-urlencoded for methods other than GET
452
		// or post correctly
453
		} elseif($this->method !== 'GET'
454
				&& $this->method !== 'POST'
455
				&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) {
456
457
			parse_str(file_get_contents($this->inputStream), $params);
458
			if(\is_array($params)) {
459
				$this->items['params'] = $params;
460
			}
461
		}
462
463
		if (\is_array($params)) {
464
			$this->items['parameters'] = array_merge($this->items['parameters'], $params);
465
		}
466
		$this->contentDecoded = true;
467
	}
468
469
470
	/**
471
	 * Checks if the CSRF check was correct
472
	 * @return bool true if CSRF check passed
473
	 */
474
	public function passesCSRFCheck(): bool {
475
		if($this->csrfTokenManager === null) {
476
			return false;
477
		}
478
479
		if(!$this->passesStrictCookieCheck()) {
480
			return false;
481
		}
482
483
		if (isset($this->items['get']['requesttoken'])) {
484
			$token = $this->items['get']['requesttoken'];
485
		} elseif (isset($this->items['post']['requesttoken'])) {
486
			$token = $this->items['post']['requesttoken'];
487
		} elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) {
488
			$token = $this->items['server']['HTTP_REQUESTTOKEN'];
489
		} else {
490
			//no token found.
491
			return false;
492
		}
493
		$token = new CsrfToken($token);
494
495
		return $this->csrfTokenManager->isTokenValid($token);
496
	}
497
498
	/**
499
	 * Whether the cookie checks are required
500
	 *
501
	 * @return bool
502
	 */
503
	private function cookieCheckRequired(): bool {
504
		if ($this->getHeader('OCS-APIREQUEST')) {
505
			return false;
506
		}
507
		if($this->getCookie(session_name()) === null && $this->getCookie('nc_token') === null) {
0 ignored issues
show
introduced by
The condition $this->getCookie(session_name()) === null is always false.
Loading history...
508
			return false;
509
		}
510
511
		return true;
512
	}
513
514
	/**
515
	 * Wrapper around session_get_cookie_params
516
	 *
517
	 * @return array
518
	 */
519
	public function getCookieParams(): array {
520
		return session_get_cookie_params();
521
	}
522
523
	/**
524
	 * Appends the __Host- prefix to the cookie if applicable
525
	 *
526
	 * @param string $name
527
	 * @return string
528
	 */
529
	protected function getProtectedCookieName(string $name): string {
530
		$cookieParams = $this->getCookieParams();
531
		$prefix = '';
532
		if($cookieParams['secure'] === true && $cookieParams['path'] === '/') {
533
			$prefix = '__Host-';
534
		}
535
536
		return $prefix.$name;
537
	}
538
539
	/**
540
	 * Checks if the strict cookie has been sent with the request if the request
541
	 * is including any cookies.
542
	 *
543
	 * @return bool
544
	 * @since 9.1.0
545
	 */
546
	public function passesStrictCookieCheck(): bool {
547
		if(!$this->cookieCheckRequired()) {
548
			return true;
549
		}
550
551
		$cookieName = $this->getProtectedCookieName('nc_sameSiteCookiestrict');
552
		if($this->getCookie($cookieName) === 'true'
553
			&& $this->passesLaxCookieCheck()) {
554
			return true;
555
		}
556
		return false;
557
	}
558
559
	/**
560
	 * Checks if the lax cookie has been sent with the request if the request
561
	 * is including any cookies.
562
	 *
563
	 * @return bool
564
	 * @since 9.1.0
565
	 */
566
	public function passesLaxCookieCheck(): bool {
567
		if(!$this->cookieCheckRequired()) {
568
			return true;
569
		}
570
571
		$cookieName = $this->getProtectedCookieName('nc_sameSiteCookielax');
572
		if($this->getCookie($cookieName) === 'true') {
573
			return true;
574
		}
575
		return false;
576
	}
577
578
579
	/**
580
	 * Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
581
	 * If `mod_unique_id` is installed this value will be taken.
582
	 * @return string
583
	 */
584
	public function getId(): string {
585
		if(isset($this->server['UNIQUE_ID'])) {
586
			return $this->server['UNIQUE_ID'];
587
		}
588
589
		if(empty($this->requestId)) {
590
			$validChars = ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS;
591
			$this->requestId = $this->secureRandom->generate(20, $validChars);
592
		}
593
594
		return $this->requestId;
595
	}
596
597
	/**
598
	 * Checks if given $remoteAddress matches given $trustedProxy.
599
	 * If $trustedProxy is an IPv4 IP range given in CIDR notation, true will be returned if
600
	 * $remoteAddress is an IPv4 address within that IP range.
601
	 * Otherwise $remoteAddress will be compared to $trustedProxy literally and the result
602
	 * will be returned.
603
	 * @return boolean true if $remoteAddress matches $trustedProxy, false otherwise
604
	 */
605
	protected function matchesTrustedProxy($trustedProxy, $remoteAddress) {
606
		$cidrre = '/^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\/([0-9]{1,2})$/';
607
608
		if (preg_match($cidrre, $trustedProxy, $match)) {
609
			$net = $match[1];
610
			$shiftbits = min(32, max(0, 32 - intval($match[2])));
611
			$netnum = ip2long($net) >> $shiftbits;
612
			$ipnum = ip2long($remoteAddress) >> $shiftbits;
613
614
			return $ipnum === $netnum;
615
		}
616
617
		return $trustedProxy === $remoteAddress;
618
	}
619
620
	/**
621
	 * Checks if given $remoteAddress matches any entry in the given array $trustedProxies.
622
	 * For details regarding what "match" means, refer to `matchesTrustedProxy`.
623
	 * @return boolean true if $remoteAddress matches any entry in $trustedProxies, false otherwise
624
	 */
625
	protected function isTrustedProxy($trustedProxies, $remoteAddress) {
626
		foreach ($trustedProxies as $tp) {
627
			if ($this->matchesTrustedProxy($tp, $remoteAddress)) {
628
				return true;
629
			}
630
		}
631
632
		return false;
633
	}
634
635
	/**
636
	 * Returns the remote address, if the connection came from a trusted proxy
637
	 * and `forwarded_for_headers` has been configured then the IP address
638
	 * specified in this header will be returned instead.
639
	 * Do always use this instead of $_SERVER['REMOTE_ADDR']
640
	 * @return string IP address
641
	 */
642
	public function getRemoteAddress(): string {
643
		$remoteAddress = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
644
		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
645
646
		if(\is_array($trustedProxies) && $this->isTrustedProxy($trustedProxies, $remoteAddress)) {
647
			$forwardedForHeaders = $this->config->getSystemValue('forwarded_for_headers', [
648
				'HTTP_X_FORWARDED_FOR'
649
				// only have one default, so we cannot ship an insecure product out of the box
650
			]);
651
652
			foreach($forwardedForHeaders as $header) {
653
				if(isset($this->server[$header])) {
654
					foreach(explode(',', $this->server[$header]) as $IP) {
655
						$IP = trim($IP);
656
						if (filter_var($IP, FILTER_VALIDATE_IP) !== false) {
657
							return $IP;
658
						}
659
					}
660
				}
661
			}
662
		}
663
664
		return $remoteAddress;
665
	}
666
667
	/**
668
	 * Check overwrite condition
669
	 * @param string $type
670
	 * @return bool
671
	 */
672
	private function isOverwriteCondition(string $type = ''): bool {
673
		$regex = '/' . $this->config->getSystemValue('overwritecondaddr', '')  . '/';
674
		$remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
675
		return $regex === '//' || preg_match($regex, $remoteAddr) === 1
676
		|| $type !== 'protocol';
677
	}
678
679
	/**
680
	 * Returns the server protocol. It respects one or more reverse proxies servers
681
	 * and load balancers
682
	 * @return string Server protocol (http or https)
683
	 */
684
	public function getServerProtocol(): string {
685
		if($this->config->getSystemValue('overwriteprotocol') !== ''
686
			&& $this->isOverwriteCondition('protocol')) {
687
			return $this->config->getSystemValue('overwriteprotocol');
688
		}
689
690
		if ($this->fromTrustedProxy() && isset($this->server['HTTP_X_FORWARDED_PROTO'])) {
691
			if (strpos($this->server['HTTP_X_FORWARDED_PROTO'], ',') !== false) {
692
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_PROTO']);
693
				$proto = strtolower(trim($parts[0]));
694
			} else {
695
				$proto = strtolower($this->server['HTTP_X_FORWARDED_PROTO']);
696
			}
697
698
			// Verify that the protocol is always HTTP or HTTPS
699
			// default to http if an invalid value is provided
700
			return $proto === 'https' ? 'https' : 'http';
701
		}
702
703
		if (isset($this->server['HTTPS'])
704
			&& $this->server['HTTPS'] !== null
705
			&& $this->server['HTTPS'] !== 'off'
706
			&& $this->server['HTTPS'] !== '') {
707
			return 'https';
708
		}
709
710
		return 'http';
711
	}
712
713
	/**
714
	 * Returns the used HTTP protocol.
715
	 *
716
	 * @return string HTTP protocol. HTTP/2, HTTP/1.1 or HTTP/1.0.
717
	 */
718
	public function getHttpProtocol(): string {
719
		$claimedProtocol = $this->server['SERVER_PROTOCOL'];
720
721
		if (\is_string($claimedProtocol)) {
722
			$claimedProtocol = strtoupper($claimedProtocol);
723
		}
724
725
		$validProtocols = [
726
			'HTTP/1.0',
727
			'HTTP/1.1',
728
			'HTTP/2',
729
		];
730
731
		if(\in_array($claimedProtocol, $validProtocols, true)) {
732
			return $claimedProtocol;
733
		}
734
735
		return 'HTTP/1.1';
736
	}
737
738
	/**
739
	 * Returns the request uri, even if the website uses one or more
740
	 * reverse proxies
741
	 * @return string
742
	 */
743
	public function getRequestUri(): string {
744
		$uri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
745
		if($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) {
746
			$uri = $this->getScriptName() . substr($uri, \strlen($this->server['SCRIPT_NAME']));
747
		}
748
		return $uri;
749
	}
750
751
	/**
752
	 * Get raw PathInfo from request (not urldecoded)
753
	 * @throws \Exception
754
	 * @return string Path info
755
	 */
756
	public function getRawPathInfo(): string {
757
		$requestUri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
758
		// remove too many slashes - can be caused by reverse proxy configuration
759
		$requestUri = preg_replace('%/{2,}%', '/', $requestUri);
760
761
		// Remove the query string from REQUEST_URI
762
		if ($pos = strpos($requestUri, '?')) {
763
			$requestUri = substr($requestUri, 0, $pos);
764
		}
765
766
		$scriptName = $this->server['SCRIPT_NAME'];
767
		$pathInfo = $requestUri;
768
769
		// strip off the script name's dir and file name
770
		// FIXME: Sabre does not really belong here
771
		list($path, $name) = \Sabre\Uri\split($scriptName);
772
		if (!empty($path)) {
773
			if($path === $pathInfo || strpos($pathInfo, $path.'/') === 0) {
774
				$pathInfo = substr($pathInfo, \strlen($path));
775
			} else {
776
				throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')");
777
			}
778
		}
779
		if ($name === null) {
780
			$name = '';
781
		}
782
783
		if (strpos($pathInfo, '/'.$name) === 0) {
784
			$pathInfo = substr($pathInfo, \strlen($name) + 1);
785
		}
786
		if ($name !== '' && strpos($pathInfo, $name) === 0) {
787
			$pathInfo = substr($pathInfo, \strlen($name));
788
		}
789
		if($pathInfo === false || $pathInfo === '/'){
790
			return '';
791
		} else {
792
			return $pathInfo;
793
		}
794
	}
795
796
	/**
797
	 * Get PathInfo from request
798
	 * @throws \Exception
799
	 * @return string|false Path info or false when not found
800
	 */
801
	public function getPathInfo() {
802
		$pathInfo = $this->getRawPathInfo();
803
		// following is taken from \Sabre\HTTP\URLUtil::decodePathSegment
804
		$pathInfo = rawurldecode($pathInfo);
805
		$encoding = mb_detect_encoding($pathInfo, ['UTF-8', 'ISO-8859-1']);
806
807
		switch($encoding) {
808
			case 'ISO-8859-1' :
809
				$pathInfo = utf8_encode($pathInfo);
810
		}
811
		// end copy
812
813
		return $pathInfo;
814
	}
815
816
	/**
817
	 * Returns the script name, even if the website uses one or more
818
	 * reverse proxies
819
	 * @return string the script name
820
	 */
821
	public function getScriptName(): string {
822
		$name = $this->server['SCRIPT_NAME'];
823
		$overwriteWebRoot =  $this->config->getSystemValue('overwritewebroot');
824
		if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) {
825
			// FIXME: This code is untestable due to __DIR__, also that hardcoded path is really dangerous
826
			$serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -\strlen('lib/private/appframework/http/')));
827
			$suburi = str_replace('\\', '/', substr(realpath($this->server['SCRIPT_FILENAME']), \strlen($serverRoot)));
828
			$name = '/' . ltrim($overwriteWebRoot . $suburi, '/');
829
		}
830
		return $name;
831
	}
832
833
	/**
834
	 * Checks whether the user agent matches a given regex
835
	 * @param array $agent array of agent names
836
	 * @return bool true if at least one of the given agent matches, false otherwise
837
	 */
838
	public function isUserAgent(array $agent): bool {
839
		if (!isset($this->server['HTTP_USER_AGENT'])) {
840
			return false;
841
		}
842
		foreach ($agent as $regex) {
843
			if (preg_match($regex, $this->server['HTTP_USER_AGENT'])) {
844
				return true;
845
			}
846
		}
847
		return false;
848
	}
849
850
	/**
851
	 * Returns the unverified server host from the headers without checking
852
	 * whether it is a trusted domain
853
	 * @return string Server host
854
	 */
855
	public function getInsecureServerHost(): string {
856
		$host = 'localhost';
857
		if ($this->fromTrustedProxy() && isset($this->server['HTTP_X_FORWARDED_HOST'])) {
858
			if (strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) {
859
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_HOST']);
860
				$host = trim(current($parts));
861
			} else {
862
				$host = $this->server['HTTP_X_FORWARDED_HOST'];
863
			}
864
		} else {
865
			if (isset($this->server['HTTP_HOST'])) {
866
				$host = $this->server['HTTP_HOST'];
867
			} else if (isset($this->server['SERVER_NAME'])) {
868
				$host = $this->server['SERVER_NAME'];
869
			}
870
		}
871
		return $host;
872
	}
873
874
875
	/**
876
	 * Returns the server host from the headers, or the first configured
877
	 * trusted domain if the host isn't in the trusted list
878
	 * @return string Server host
879
	 */
880
	public function getServerHost(): string {
881
		// overwritehost is always trusted
882
		$host = $this->getOverwriteHost();
883
		if ($host !== null) {
884
			return $host;
885
		}
886
887
		// get the host from the headers
888
		$host = $this->getInsecureServerHost();
889
890
		// Verify that the host is a trusted domain if the trusted domains
891
		// are defined
892
		// If no trusted domain is provided the first trusted domain is returned
893
		$trustedDomainHelper = new TrustedDomainHelper($this->config);
894
		if ($trustedDomainHelper->isTrustedDomain($host)) {
895
			return $host;
896
		} else {
897
			$trustedList = $this->config->getSystemValue('trusted_domains', []);
898
			if(!empty($trustedList)) {
899
				return $trustedList[0];
900
			} else {
901
				return '';
902
			}
903
		}
904
	}
905
906
	/**
907
	 * Returns the overwritehost setting from the config if set and
908
	 * if the overwrite condition is met
909
	 * @return string|null overwritehost value or null if not defined or the defined condition
910
	 * isn't met
911
	 */
912
	private function getOverwriteHost() {
913
		if($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) {
914
			return $this->config->getSystemValue('overwritehost');
915
		}
916
		return null;
917
	}
918
919
	private function fromTrustedProxy(): bool {
920
		$remoteAddress = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
921
		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
922
923
		return \is_array($trustedProxies) && $this->isTrustedProxy($trustedProxies, $remoteAddress);
924
	}
925
}
926