Passed
Push — master ( 2d5047...0999f0 )
by Morris
15:20 queued 03:08
created

Request::getHeader()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 8
nop 1
dl 0
loc 24
rs 8.8333
c 0
b 0
f 0
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
				if (isset($this->server[$name])) {
327
					return $this->server[$name];
328
				}
329
				break;
330
			case 'REMOTE_ADDR' :
331
				if (isset($this->server[$name])) {
332
					return $this->server[$name];
333
				}
334
				break;
335
		}
336
337
		return '';
338
	}
339
340
	/**
341
	 * Lets you access post and get parameters by the index
342
	 * In case of json requests the encoded json body is accessed
343
	 *
344
	 * @param string $key the key which you want to access in the URL Parameter
345
	 *                     placeholder, $_POST or $_GET array.
346
	 *                     The priority how they're returned is the following:
347
	 *                     1. URL parameters
348
	 *                     2. POST parameters
349
	 *                     3. GET parameters
350
	 * @param mixed $default If the key is not found, this value will be returned
351
	 * @return mixed the content of the array
352
	 */
353
	public function getParam(string $key, $default = null) {
354
		return isset($this->parameters[$key])
355
			? $this->parameters[$key]
356
			: $default;
357
	}
358
359
	/**
360
	 * Returns all params that were received, be it from the request
361
	 * (as GET or POST) or throuh the URL by the route
362
	 * @return array the array with all parameters
363
	 */
364
	public function getParams(): array {
365
		return is_array($this->parameters) ? $this->parameters : [];
0 ignored issues
show
introduced by
The condition is_array($this->parameters) is always true.
Loading history...
366
	}
367
368
	/**
369
	 * Returns the method of the request
370
	 * @return string the method of the request (POST, GET, etc)
371
	 */
372
	public function getMethod(): string {
373
		return $this->method;
374
	}
375
376
	/**
377
	 * Shortcut for accessing an uploaded file through the $_FILES array
378
	 * @param string $key the key that will be taken from the $_FILES array
379
	 * @return array the file in the $_FILES element
380
	 */
381
	public function getUploadedFile(string $key) {
382
		return isset($this->files[$key]) ? $this->files[$key] : null;
383
	}
384
385
	/**
386
	 * Shortcut for getting env variables
387
	 * @param string $key the key that will be taken from the $_ENV array
388
	 * @return array the value in the $_ENV element
389
	 */
390
	public function getEnv(string $key) {
391
		return isset($this->env[$key]) ? $this->env[$key] : null;
392
	}
393
394
	/**
395
	 * Shortcut for getting cookie variables
396
	 * @param string $key the key that will be taken from the $_COOKIE array
397
	 * @return string the value in the $_COOKIE element
398
	 */
399
	public function getCookie(string $key) {
400
		return isset($this->cookies[$key]) ? $this->cookies[$key] : null;
401
	}
402
403
	/**
404
	 * Returns the request body content.
405
	 *
406
	 * If the HTTP request method is PUT and the body
407
	 * not application/x-www-form-urlencoded or application/json a stream
408
	 * resource is returned, otherwise an array.
409
	 *
410
	 * @return array|string|resource The request body content or a resource to read the body stream.
411
	 *
412
	 * @throws \LogicException
413
	 */
414
	protected function getContent() {
415
		// If the content can't be parsed into an array then return a stream resource.
416
		if ($this->method === 'PUT'
417
			&& $this->getHeader('Content-Length') !== '0'
418
			&& $this->getHeader('Content-Length') !== ''
419
			&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false
420
			&& strpos($this->getHeader('Content-Type'), 'application/json') === false
421
		) {
422
			if ($this->content === false) {
423
				throw new \LogicException(
424
					'"put" can only be accessed once if not '
425
					. 'application/x-www-form-urlencoded or application/json.'
426
				);
427
			}
428
			$this->content = false;
429
			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...
430
		} else {
431
			$this->decodeContent();
432
			return $this->items['parameters'];
433
		}
434
	}
435
436
	/**
437
	 * Attempt to decode the content and populate parameters
438
	 */
439
	protected function decodeContent() {
440
		if ($this->contentDecoded) {
441
			return;
442
		}
443
		$params = [];
444
445
		// 'application/json' must be decoded manually.
446
		if (strpos($this->getHeader('Content-Type'), 'application/json') !== false) {
447
			$params = json_decode(file_get_contents($this->inputStream), true);
448
			if($params !== null && \count($params) > 0) {
449
				$this->items['params'] = $params;
450
				if($this->method === 'POST') {
451
					$this->items['post'] = $params;
452
				}
453
			}
454
455
		// Handle application/x-www-form-urlencoded for methods other than GET
456
		// or post correctly
457
		} elseif($this->method !== 'GET'
458
				&& $this->method !== 'POST'
459
				&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) {
460
461
			parse_str(file_get_contents($this->inputStream), $params);
462
			if(\is_array($params)) {
463
				$this->items['params'] = $params;
464
			}
465
		}
466
467
		if (\is_array($params)) {
468
			$this->items['parameters'] = array_merge($this->items['parameters'], $params);
469
		}
470
		$this->contentDecoded = true;
471
	}
472
473
474
	/**
475
	 * Checks if the CSRF check was correct
476
	 * @return bool true if CSRF check passed
477
	 */
478
	public function passesCSRFCheck(): bool {
479
		if($this->csrfTokenManager === null) {
480
			return false;
481
		}
482
483
		if(!$this->passesStrictCookieCheck()) {
484
			return false;
485
		}
486
487
		if (isset($this->items['get']['requesttoken'])) {
488
			$token = $this->items['get']['requesttoken'];
489
		} elseif (isset($this->items['post']['requesttoken'])) {
490
			$token = $this->items['post']['requesttoken'];
491
		} elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) {
492
			$token = $this->items['server']['HTTP_REQUESTTOKEN'];
493
		} else {
494
			//no token found.
495
			return false;
496
		}
497
		$token = new CsrfToken($token);
498
499
		return $this->csrfTokenManager->isTokenValid($token);
500
	}
501
502
	/**
503
	 * Whether the cookie checks are required
504
	 *
505
	 * @return bool
506
	 */
507
	private function cookieCheckRequired(): bool {
508
		if ($this->getHeader('OCS-APIREQUEST')) {
509
			return false;
510
		}
511
		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...
512
			return false;
513
		}
514
515
		return true;
516
	}
517
518
	/**
519
	 * Wrapper around session_get_cookie_params
520
	 *
521
	 * @return array
522
	 */
523
	public function getCookieParams(): array {
524
		return session_get_cookie_params();
525
	}
526
527
	/**
528
	 * Appends the __Host- prefix to the cookie if applicable
529
	 *
530
	 * @param string $name
531
	 * @return string
532
	 */
533
	protected function getProtectedCookieName(string $name): string {
534
		$cookieParams = $this->getCookieParams();
535
		$prefix = '';
536
		if($cookieParams['secure'] === true && $cookieParams['path'] === '/') {
537
			$prefix = '__Host-';
538
		}
539
540
		return $prefix.$name;
541
	}
542
543
	/**
544
	 * Checks if the strict cookie has been sent with the request if the request
545
	 * is including any cookies.
546
	 *
547
	 * @return bool
548
	 * @since 9.1.0
549
	 */
550
	public function passesStrictCookieCheck(): bool {
551
		if(!$this->cookieCheckRequired()) {
552
			return true;
553
		}
554
555
		$cookieName = $this->getProtectedCookieName('nc_sameSiteCookiestrict');
556
		if($this->getCookie($cookieName) === 'true'
557
			&& $this->passesLaxCookieCheck()) {
558
			return true;
559
		}
560
		return false;
561
	}
562
563
	/**
564
	 * Checks if the lax cookie has been sent with the request if the request
565
	 * is including any cookies.
566
	 *
567
	 * @return bool
568
	 * @since 9.1.0
569
	 */
570
	public function passesLaxCookieCheck(): bool {
571
		if(!$this->cookieCheckRequired()) {
572
			return true;
573
		}
574
575
		$cookieName = $this->getProtectedCookieName('nc_sameSiteCookielax');
576
		if($this->getCookie($cookieName) === 'true') {
577
			return true;
578
		}
579
		return false;
580
	}
581
582
583
	/**
584
	 * Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
585
	 * If `mod_unique_id` is installed this value will be taken.
586
	 * @return string
587
	 */
588
	public function getId(): string {
589
		if(isset($this->server['UNIQUE_ID'])) {
590
			return $this->server['UNIQUE_ID'];
591
		}
592
593
		if(empty($this->requestId)) {
594
			$validChars = ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS;
595
			$this->requestId = $this->secureRandom->generate(20, $validChars);
596
		}
597
598
		return $this->requestId;
599
	}
600
601
	/**
602
	 * Returns the remote address, if the connection came from a trusted proxy
603
	 * and `forwarded_for_headers` has been configured then the IP address
604
	 * specified in this header will be returned instead.
605
	 * Do always use this instead of $_SERVER['REMOTE_ADDR']
606
	 * @return string IP address
607
	 */
608
	public function getRemoteAddress(): string {
609
		$remoteAddress = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
610
		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
611
612
		if(\is_array($trustedProxies) && \in_array($remoteAddress, $trustedProxies)) {
613
			$forwardedForHeaders = $this->config->getSystemValue('forwarded_for_headers', [
614
				'HTTP_X_FORWARDED_FOR'
615
				// only have one default, so we cannot ship an insecure product out of the box
616
			]);
617
618
			foreach($forwardedForHeaders as $header) {
619
				if(isset($this->server[$header])) {
620
					foreach(explode(',', $this->server[$header]) as $IP) {
621
						$IP = trim($IP);
622
						if (filter_var($IP, FILTER_VALIDATE_IP) !== false) {
623
							return $IP;
624
						}
625
					}
626
				}
627
			}
628
		}
629
630
		return $remoteAddress;
631
	}
632
633
	/**
634
	 * Check overwrite condition
635
	 * @param string $type
636
	 * @return bool
637
	 */
638
	private function isOverwriteCondition(string $type = ''): bool {
639
		$regex = '/' . $this->config->getSystemValue('overwritecondaddr', '')  . '/';
640
		$remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
641
		return $regex === '//' || preg_match($regex, $remoteAddr) === 1
642
		|| $type !== 'protocol';
643
	}
644
645
	/**
646
	 * Returns the server protocol. It respects one or more reverse proxies servers
647
	 * and load balancers
648
	 * @return string Server protocol (http or https)
649
	 */
650
	public function getServerProtocol(): string {
651
		if($this->config->getSystemValue('overwriteprotocol') !== ''
652
			&& $this->isOverwriteCondition('protocol')) {
653
			return $this->config->getSystemValue('overwriteprotocol');
654
		}
655
656
		if (isset($this->server['HTTP_X_FORWARDED_PROTO'])) {
657
			if (strpos($this->server['HTTP_X_FORWARDED_PROTO'], ',') !== false) {
658
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_PROTO']);
659
				$proto = strtolower(trim($parts[0]));
660
			} else {
661
				$proto = strtolower($this->server['HTTP_X_FORWARDED_PROTO']);
662
			}
663
664
			// Verify that the protocol is always HTTP or HTTPS
665
			// default to http if an invalid value is provided
666
			return $proto === 'https' ? 'https' : 'http';
667
		}
668
669
		if (isset($this->server['HTTPS'])
670
			&& $this->server['HTTPS'] !== null
671
			&& $this->server['HTTPS'] !== 'off'
672
			&& $this->server['HTTPS'] !== '') {
673
			return 'https';
674
		}
675
676
		return 'http';
677
	}
678
679
	/**
680
	 * Returns the used HTTP protocol.
681
	 *
682
	 * @return string HTTP protocol. HTTP/2, HTTP/1.1 or HTTP/1.0.
683
	 */
684
	public function getHttpProtocol(): string {
685
		$claimedProtocol = $this->server['SERVER_PROTOCOL'];
686
687
		if (\is_string($claimedProtocol)) {
688
			$claimedProtocol = strtoupper($claimedProtocol);
689
		}
690
691
		$validProtocols = [
692
			'HTTP/1.0',
693
			'HTTP/1.1',
694
			'HTTP/2',
695
		];
696
697
		if(\in_array($claimedProtocol, $validProtocols, true)) {
698
			return $claimedProtocol;
699
		}
700
701
		return 'HTTP/1.1';
702
	}
703
704
	/**
705
	 * Returns the request uri, even if the website uses one or more
706
	 * reverse proxies
707
	 * @return string
708
	 */
709
	public function getRequestUri(): string {
710
		$uri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
711
		if($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) {
712
			$uri = $this->getScriptName() . substr($uri, \strlen($this->server['SCRIPT_NAME']));
713
		}
714
		return $uri;
715
	}
716
717
	/**
718
	 * Get raw PathInfo from request (not urldecoded)
719
	 * @throws \Exception
720
	 * @return string Path info
721
	 */
722
	public function getRawPathInfo(): string {
723
		$requestUri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
724
		// remove too many leading slashes - can be caused by reverse proxy configuration
725
		if (strpos($requestUri, '/') === 0) {
726
			$requestUri = '/' . ltrim($requestUri, '/');
727
		}
728
729
		$requestUri = preg_replace('%/{2,}%', '/', $requestUri);
730
731
		// Remove the query string from REQUEST_URI
732
		if ($pos = strpos($requestUri, '?')) {
733
			$requestUri = substr($requestUri, 0, $pos);
734
		}
735
736
		$scriptName = $this->server['SCRIPT_NAME'];
737
		$pathInfo = $requestUri;
738
739
		// strip off the script name's dir and file name
740
		// FIXME: Sabre does not really belong here
741
		list($path, $name) = \Sabre\Uri\split($scriptName);
742
		if (!empty($path)) {
743
			if($path === $pathInfo || strpos($pathInfo, $path.'/') === 0) {
744
				$pathInfo = substr($pathInfo, \strlen($path));
745
			} else {
746
				throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')");
747
			}
748
		}
749
		if ($name === null) {
750
			$name = '';
751
		}
752
753
		if (strpos($pathInfo, '/'.$name) === 0) {
754
			$pathInfo = substr($pathInfo, \strlen($name) + 1);
755
		}
756
		if ($name !== '' && strpos($pathInfo, $name) === 0) {
757
			$pathInfo = substr($pathInfo, \strlen($name));
758
		}
759
		if($pathInfo === false || $pathInfo === '/'){
760
			return '';
761
		} else {
762
			return $pathInfo;
763
		}
764
	}
765
766
	/**
767
	 * Get PathInfo from request
768
	 * @throws \Exception
769
	 * @return string|false Path info or false when not found
770
	 */
771
	public function getPathInfo() {
772
		$pathInfo = $this->getRawPathInfo();
773
		// following is taken from \Sabre\HTTP\URLUtil::decodePathSegment
774
		$pathInfo = rawurldecode($pathInfo);
775
		$encoding = mb_detect_encoding($pathInfo, ['UTF-8', 'ISO-8859-1']);
776
777
		switch($encoding) {
778
			case 'ISO-8859-1' :
779
				$pathInfo = utf8_encode($pathInfo);
780
		}
781
		// end copy
782
783
		return $pathInfo;
784
	}
785
786
	/**
787
	 * Returns the script name, even if the website uses one or more
788
	 * reverse proxies
789
	 * @return string the script name
790
	 */
791
	public function getScriptName(): string {
792
		$name = $this->server['SCRIPT_NAME'];
793
		$overwriteWebRoot =  $this->config->getSystemValue('overwritewebroot');
794
		if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) {
795
			// FIXME: This code is untestable due to __DIR__, also that hardcoded path is really dangerous
796
			$serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -\strlen('lib/private/appframework/http/')));
797
			$suburi = str_replace('\\', '/', substr(realpath($this->server['SCRIPT_FILENAME']), \strlen($serverRoot)));
798
			$name = '/' . ltrim($overwriteWebRoot . $suburi, '/');
799
		}
800
		return $name;
801
	}
802
803
	/**
804
	 * Checks whether the user agent matches a given regex
805
	 * @param array $agent array of agent names
806
	 * @return bool true if at least one of the given agent matches, false otherwise
807
	 */
808
	public function isUserAgent(array $agent): bool {
809
		if (!isset($this->server['HTTP_USER_AGENT'])) {
810
			return false;
811
		}
812
		foreach ($agent as $regex) {
813
			if (preg_match($regex, $this->server['HTTP_USER_AGENT'])) {
814
				return true;
815
			}
816
		}
817
		return false;
818
	}
819
820
	/**
821
	 * Returns the unverified server host from the headers without checking
822
	 * whether it is a trusted domain
823
	 * @return string Server host
824
	 */
825
	public function getInsecureServerHost(): string {
826
		$host = 'localhost';
827
		if (isset($this->server['HTTP_X_FORWARDED_HOST'])) {
828
			if (strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) {
829
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_HOST']);
830
				$host = trim(current($parts));
831
			} else {
832
				$host = $this->server['HTTP_X_FORWARDED_HOST'];
833
			}
834
		} else {
835
			if (isset($this->server['HTTP_HOST'])) {
836
				$host = $this->server['HTTP_HOST'];
837
			} else if (isset($this->server['SERVER_NAME'])) {
838
				$host = $this->server['SERVER_NAME'];
839
			}
840
		}
841
		return $host;
842
	}
843
844
845
	/**
846
	 * Returns the server host from the headers, or the first configured
847
	 * trusted domain if the host isn't in the trusted list
848
	 * @return string Server host
849
	 */
850
	public function getServerHost(): string {
851
		// overwritehost is always trusted
852
		$host = $this->getOverwriteHost();
853
		if ($host !== null) {
854
			return $host;
855
		}
856
857
		// get the host from the headers
858
		$host = $this->getInsecureServerHost();
859
860
		// Verify that the host is a trusted domain if the trusted domains
861
		// are defined
862
		// If no trusted domain is provided the first trusted domain is returned
863
		$trustedDomainHelper = new TrustedDomainHelper($this->config);
864
		if ($trustedDomainHelper->isTrustedDomain($host)) {
865
			return $host;
866
		} else {
867
			$trustedList = $this->config->getSystemValue('trusted_domains', []);
868
			if(!empty($trustedList)) {
869
				return $trustedList[0];
870
			} else {
871
				return '';
872
			}
873
		}
874
	}
875
876
	/**
877
	 * Returns the overwritehost setting from the config if set and
878
	 * if the overwrite condition is met
879
	 * @return string|null overwritehost value or null if not defined or the defined condition
880
	 * isn't met
881
	 */
882
	private function getOverwriteHost() {
883
		if($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) {
884
			return $this->config->getSystemValue('overwritehost');
885
		}
886
		return null;
887
	}
888
889
}
890