Completed
Pull Request — master (#32767)
by Thomas
17:30
created

Request::getRawPathInfo()   C

Complexity

Conditions 12
Paths 136

Size

Total Lines 41

Duplication

Lines 6
Ratio 14.63 %

Importance

Changes 0
Metric Value
cc 12
nc 136
nop 0
dl 6
loc 41
rs 6.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @author Bart Visscher <[email protected]>
4
 * @author Bernhard Posselt <[email protected]>
5
 * @author Joas Schilling <[email protected]>
6
 * @author Jörn Friedrich Dreyer <[email protected]>
7
 * @author Lukas Reschke <[email protected]>
8
 * @author Mitar <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Robin McCorkell <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 * @author Thomas Tanghus <[email protected]>
15
 * @author Vincent Petry <[email protected]>
16
 *
17
 * @copyright Copyright (c) 2018, ownCloud GmbH
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OC\AppFramework\Http;
35
36
use OC\Security\CSRF\CsrfToken;
37
use OC\Security\CSRF\CsrfTokenManager;
38
use OC\Security\TrustedDomainHelper;
39
use OCP\IConfig;
40
use OCP\IRequest;
41
use OCP\Security\ICrypto;
42
use OCP\Security\ISecureRandom;
43
44
/**
45
 * Class for accessing variables in the request.
46
 * This class provides an immutable object with request variables.
47
 *
48
 * @property mixed[] cookies
49
 * @property mixed[] env
50
 * @property mixed[] files
51
 * @property string method
52
 * @property mixed[] parameters
53
 * @property mixed[] server
54
 */
55
class Request implements \ArrayAccess, \Countable, IRequest {
56
	const USER_AGENT_IE = '/(MSIE)|(Trident)/';
57
	const USER_AGENT_IE_8 = '/MSIE 8.0/';
58
	// Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
59
	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.]+$/';
60
	// Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference
61
	const USER_AGENT_FIREFOX = '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/[0-9.]+$/';
62
	// Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
63
	const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+$/';
64
	// Safari User Agent from http://www.useragentstring.com/pages/Safari/
65
	const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/';
66
	// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
67
	const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#';
68
	const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#';
69
	const USER_AGENT_OWNCLOUD_IOS = '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/';
70
	const USER_AGENT_OWNCLOUD_ANDROID = '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/';
71
	const USER_AGENT_OWNCLOUD_DESKTOP = '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/';
72
	const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost)$/';
73
74
	protected $inputStream;
75
	protected $content;
76
	protected $items = [];
77
	protected $allowedKeys = [
78
		'get',
79
		'post',
80
		'files',
81
		'server',
82
		'env',
83
		'cookies',
84
		'urlParams',
85
		'parameters',
86
		'method',
87
		'requesttoken',
88
	];
89
	/** @var ISecureRandom */
90
	protected $secureRandom;
91
	/** @var IConfig */
92
	protected $config;
93
	/** @var string */
94
	protected $requestId = '';
95
	/** @var ICrypto */
96
	protected $crypto;
97
	/** @var CsrfTokenManager|null */
98
	protected $csrfTokenManager;
99
100
	/** @var bool */
101
	protected $contentDecoded = false;
102
103
	/**
104
	 * @param array $vars An associative array with the following optional values:
105
	 *        - array 'urlParams' the parameters which were matched from the URL
106
	 *        - array 'get' the $_GET array
107
	 *        - array|string 'post' the $_POST array or JSON string
108
	 *        - array 'files' the $_FILES array
109
	 *        - array 'server' the $_SERVER array
110
	 *        - array 'env' the $_ENV array
111
	 *        - array 'cookies' the $_COOKIE array
112
	 *        - string 'method' the request method (GET, POST etc)
113
	 *        - string|false 'requesttoken' the requesttoken or false when not available
114
	 * @param ISecureRandom $secureRandom
115
	 * @param IConfig $config
116
	 * @param CsrfTokenManager|null $csrfTokenManager
117
	 * @param string $stream
118
	 * @see http://www.php.net/manual/en/reserved.variables.php
119
	 */
120
	public function __construct(array $vars= [],
121
								ISecureRandom $secureRandom = null,
122
								IConfig $config = null,
123
								CsrfTokenManager $csrfTokenManager = null,
124
								$stream = 'php://input') {
125
		$this->inputStream = $stream;
126
		$this->items['params'] = [];
127
		$this->secureRandom = $secureRandom;
128
		$this->config = $config;
129
		$this->csrfTokenManager = $csrfTokenManager;
130
131
		if (!\array_key_exists('method', $vars)) {
132
			$vars['method'] = 'GET';
133
		}
134
135
		foreach ($this->allowedKeys as $name) {
136
			$this->items[$name] = isset($vars[$name])
137
				? $vars[$name]
138
				: [];
139
		}
140
141
		$this->items['parameters'] = \array_merge(
142
			$this->items['get'],
143
			$this->items['post'],
144
			$this->items['urlParams'],
145
			$this->items['params']
146
		);
147
	}
148
	/**
149
	 * @param array $parameters
150
	 */
151
	public function setUrlParameters(array $parameters) {
152
		$this->items['urlParams'] = $parameters;
153
		$this->items['parameters'] = \array_merge(
154
			$this->items['parameters'],
155
			$this->items['urlParams']
156
		);
157
	}
158
159
	/**
160
	 * Countable method
161
	 * @return int
162
	 */
163
	public function count() {
164
		return \count(\array_keys($this->items['parameters']));
165
	}
166
167
	/**
168
	* ArrayAccess methods
169
	*
170
	* Gives access to the combined GET, POST and urlParams arrays
171
	*
172
	* Examples:
173
	*
174
	* $var = $request['myvar'];
175
	*
176
	* or
177
	*
178
	* if(!isset($request['myvar']) {
179
	* 	// Do something
180
	* }
181
	*
182
	* $request['myvar'] = 'something'; // This throws an exception.
183
	*
184
	* @param string $offset The key to lookup
185
	* @return boolean
186
	*/
187
	public function offsetExists($offset) {
188
		return isset($this->items['parameters'][$offset]);
189
	}
190
191
	/**
192
	* @see offsetExists
193
	*/
194
	public function offsetGet($offset) {
195
		return isset($this->items['parameters'][$offset])
196
			? $this->items['parameters'][$offset]
197
			: null;
198
	}
199
200
	/**
201
	* @see offsetExists
202
	*/
203
	public function offsetSet($offset, $value) {
204
		throw new \RuntimeException('You cannot change the contents of the request object');
205
	}
206
207
	/**
208
	* @see offsetExists
209
	*/
210
	public function offsetUnset($offset) {
211
		throw new \RuntimeException('You cannot change the contents of the request object');
212
	}
213
214
	/**
215
	 * Magic property accessors
216
	 * @param string $name
217
	 * @param mixed $value
218
	 */
219
	public function __set($name, $value) {
220
		throw new \RuntimeException('You cannot change the contents of the request object');
221
	}
222
223
	/**
224
	* Access request variables by method and name.
225
	* Examples:
226
	*
227
	* $request->post['myvar']; // Only look for POST variables
228
	* $request->myvar; or $request->{'myvar'}; or $request->{$myvar}
229
	* Looks in the combined GET, POST and urlParams array.
230
	*
231
	* If you access e.g. ->post but the current HTTP request method
232
	* is GET a \LogicException will be thrown.
233
	*
234
	* @param string $name The key to look for.
235
	* @throws \LogicException
236
	* @return mixed|null
237
	*/
238
	public function __get($name) {
239
		switch ($name) {
240
			case 'put':
241
			case 'patch':
242
			case 'get':
243
			case 'post':
244
				if ($this->method !== \strtoupper($name)) {
245
					throw new \LogicException(\sprintf('%s cannot be accessed in a %s request.', $name, $this->method));
246
				}
247
				return $this->getContent();
248
			case 'files':
249
			case 'server':
250
			case 'env':
251
			case 'cookies':
252
			case 'urlParams':
253
			case 'method':
254
				return isset($this->items[$name])
255
					? $this->items[$name]
256
					: null;
257
			case 'parameters':
258
			case 'params':
259
				return $this->getContent();
260
			default:
261
				return isset($this[$name])
262
					? $this[$name]
263
					: null;
264
		}
265
	}
266
267
	/**
268
	 * @param string $name
269
	 * @return bool
270
	 */
271
	public function __isset($name) {
272
		if (\in_array($name, $this->allowedKeys, true)) {
273
			return true;
274
		}
275
		return isset($this->items['parameters'][$name]);
276
	}
277
278
	/**
279
	 * @param string $id
280
	 */
281
	public function __unset($id) {
282
		throw new \RuntimeException('You cannot change the contents of the request object');
283
	}
284
285
	/**
286
	 * Returns the value for a specific http header.
287
	 *
288
	 * This method returns null if the header did not exist.
289
	 *
290
	 * @param string $name
291
	 * @return string
292
	 */
293
	public function getHeader($name) {
294
		$name = \strtoupper(\str_replace(['-'], ['_'], $name));
295
		if (isset($this->server['HTTP_' . $name])) {
296
			return $this->server['HTTP_' . $name];
297
		}
298
299
		// There's a few headers that seem to end up in the top-level
300
		// server array.
301
		switch ($name) {
302
			case 'CONTENT_TYPE':
303
			case 'CONTENT_LENGTH':
304
				if (isset($this->server[$name])) {
305
					return $this->server[$name];
306
				}
307
				break;
308
309
		}
310
311
		return null;
312
	}
313
314
	/**
315
	 * Lets you access post and get parameters by the index
316
	 * In case of json requests the encoded json body is accessed
317
	 *
318
	 * @param string $key the key which you want to access in the URL Parameter
319
	 *                     placeholder, $_POST or $_GET array.
320
	 *                     The priority how they're returned is the following:
321
	 *                     1. URL parameters
322
	 *                     2. POST parameters
323
	 *                     3. GET parameters
324
	 * @param mixed $default If the key is not found, this value will be returned
325
	 * @return mixed the content of the array
326
	 */
327
	public function getParam($key, $default = null) {
328
		return isset($this->parameters[$key])
329
			? $this->parameters[$key]
330
			: $default;
331
	}
332
333
	/**
334
	 * Returns all params that were received, be it from the request
335
	 * (as GET or POST) or throuh the URL by the route
336
	 * @return array the array with all parameters
337
	 */
338
	public function getParams() {
339
		return $this->parameters;
340
	}
341
342
	/**
343
	 * Returns the method of the request
344
	 * @return string the method of the request (POST, GET, etc)
345
	 */
346
	public function getMethod() {
347
		return $this->method;
348
	}
349
350
	/**
351
	 * Shortcut for accessing an uploaded file through the $_FILES array
352
	 * @param string $key the key that will be taken from the $_FILES array
353
	 * @return array the file in the $_FILES element
354
	 */
355
	public function getUploadedFile($key) {
356
		return isset($this->files[$key]) ? $this->files[$key] : null;
357
	}
358
359
	/**
360
	 * Shortcut for getting env variables
361
	 * @param string $key the key that will be taken from the $_ENV array
362
	 * @return array the value in the $_ENV element
363
	 */
364
	public function getEnv($key) {
365
		return isset($this->env[$key]) ? $this->env[$key] : null;
366
	}
367
368
	/**
369
	 * Shortcut for getting cookie variables
370
	 * @param string $key the key that will be taken from the $_COOKIE array
371
	 * @return string the value in the $_COOKIE element
372
	 */
373
	public function getCookie($key) {
374
		return isset($this->cookies[$key]) ? $this->cookies[$key] : null;
375
	}
376
377
	/**
378
	 * Returns the request body content.
379
	 *
380
	 * If the HTTP request method is PUT and the body
381
	 * not application/x-www-form-urlencoded or application/json a stream
382
	 * resource is returned, otherwise an array.
383
	 *
384
	 * @return array|string|resource The request body content or a resource to read the body stream.
385
	 *
386
	 * @throws \LogicException
387
	 */
388
	protected function getContent() {
389
		// If the content can't be parsed into an array then return a stream resource.
390
		if ($this->method === 'PUT'
391
			&& \strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false
392
			&& \strpos($this->getHeader('Content-Type'), 'application/json') === false
393
		) {
394
			if ($this->content === false) {
395
				throw new \LogicException(
396
					'"put" can only be accessed once if not '
397
					. 'application/x-www-form-urlencoded or application/json.'
398
				);
399
			}
400
			$this->content = false;
401
			return \fopen($this->inputStream, 'rb');
402
		} else {
403
			$this->decodeContent();
404
			return $this->items['parameters'];
405
		}
406
	}
407
408
	/**
409
	 * Attempt to decode the content and populate parameters
410
	 */
411
	protected function decodeContent() {
412
		if ($this->contentDecoded) {
413
			return;
414
		}
415
		$params = [];
416
417
		// 'application/json' must be decoded manually.
418
		if (\strpos($this->getHeader('Content-Type'), 'application/json') !== false) {
419
			$params = \json_decode(\file_get_contents($this->inputStream), true);
420
			if (\is_array($params) && \count($params) > 0) {
421
				$this->items['params'] = $params;
422
				if ($this->method === 'POST') {
423
					$this->items['post'] = $params;
424
				}
425
			}
426
427
			// Handle application/x-www-form-urlencoded for methods other than GET
428
		// or post correctly
429
		} elseif ($this->method !== 'GET'
430
				&& $this->method !== 'POST'
431
				&& \strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) {
432
			\parse_str(\file_get_contents($this->inputStream), $params);
433
			if (\is_array($params)) {
434
				$this->items['params'] = $params;
435
			}
436
		}
437
438
		if (\is_array($params)) {
439
			$this->items['parameters'] = \array_merge($this->items['parameters'], $params);
440
		}
441
		$this->contentDecoded = true;
442
	}
443
444
	/**
445
	 * Checks if the CSRF check was correct
446
	 * @return bool true if CSRF check passed
447
	 */
448
	public function passesCSRFCheck() {
449
		if ($this->csrfTokenManager === null) {
450
			return false;
451
		}
452
453
		// Is CSRF protection handled outside of ownCloud?
454
		if ($this->config->getSystemValue('csrf.disabled', false)) {
455
			return true;
456
		}
457
458
		if (isset($this->items['get']['requesttoken'])) {
459
			$token = $this->items['get']['requesttoken'];
460
		} elseif (isset($this->items['post']['requesttoken'])) {
461
			$token = $this->items['post']['requesttoken'];
462
		} elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) {
463
			$token = $this->items['server']['HTTP_REQUESTTOKEN'];
464
		} else {
465
			//no token found.
466
			return false;
467
		}
468
		$token = new CsrfToken($token);
469
470
		return $this->csrfTokenManager->isTokenValid($token);
471
	}
472
473
	/**
474
	 * Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
475
	 * If an X-Request-ID header is sent by the client this value will be taken.
476
	 * If `mod_unique_id` is installed this value will be taken.
477
	 * @return string
478
	 */
479
	public function getId() {
480
		// allow clients to provide a request id
481
		if (isset($this->server['HTTP_X_REQUEST_ID'])) {
482
			$reqId = $this->server['HTTP_X_REQUEST_ID'];
483
			if (\strlen($reqId) > 19
484
				&& \strlen($reqId) < 200
485
				&& \preg_match('%^[a-zA-Z0-9-+/_=.:]+$%', $reqId)) {
486
				return $this->server['HTTP_X_REQUEST_ID'];
487
			} else {
488
				throw new \InvalidArgumentException('X-Request-ID must be 20-200 bytes of chars, numbers and -+/_=.:');
489
			}
490
		}
491
		if (isset($this->server['UNIQUE_ID'])) {
492
			return $this->server['UNIQUE_ID'];
493
		}
494
495
		if (empty($this->requestId)) {
496
			$validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
497
			$this->requestId = $this->secureRandom->generate(20, $validChars);
498
		}
499
500
		return $this->requestId;
501
	}
502
503
	/**
504
	 * Returns the remote address, if the connection came from a trusted proxy
505
	 * and `forwarded_for_headers` has been configured then the IP address
506
	 * specified in this header will be returned instead.
507
	 * Do always use this instead of $_SERVER['REMOTE_ADDR']
508
	 * @return string IP address
509
	 */
510
	public function getRemoteAddress() {
511
		$remoteAddress = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
512
		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
513
514
		if (\is_array($trustedProxies) && \in_array($remoteAddress, $trustedProxies)) {
515
			$forwardedForHeaders = $this->config->getSystemValue('forwarded_for_headers', [
516
				'HTTP_X_FORWARDED_FOR'
517
				// only have one default, so we cannot ship an insecure product out of the box
518
			]);
519
520
			foreach ($forwardedForHeaders as $header) {
521
				if (isset($this->server[$header])) {
522
					foreach (\explode(',', $this->server[$header]) as $IP) {
523
						$IP = \trim($IP);
524
						if (\filter_var($IP, FILTER_VALIDATE_IP) !== false) {
525
							return $IP;
526
						}
527
					}
528
				}
529
			}
530
		}
531
532
		return $remoteAddress;
533
	}
534
535
	/**
536
	 * Check overwrite condition
537
	 * @param string $type
538
	 * @return bool
539
	 */
540
	private function isOverwriteCondition($type = '') {
541
		$regex = '/' . $this->config->getSystemValue('overwritecondaddr', '')  . '/';
542
		$remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
543
		return $regex === '//' || \preg_match($regex, $remoteAddr) === 1
544
		|| $type !== 'protocol';
545
	}
546
547
	/**
548
	 * Returns the server protocol. It respects one or more reverse proxies servers
549
	 * and load balancers
550
	 * @return string Server protocol (http or https)
551
	 */
552
	public function getServerProtocol() {
553
		if ($this->config->getSystemValue('overwriteprotocol') !== ''
554
			&& $this->isOverwriteCondition('protocol')) {
555
			return $this->config->getSystemValue('overwriteprotocol');
556
		}
557
558
		if (isset($this->server['HTTP_X_FORWARDED_PROTO'])) {
559 View Code Duplication
			if (\strpos($this->server['HTTP_X_FORWARDED_PROTO'], ',') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
560
				$parts = \explode(',', $this->server['HTTP_X_FORWARDED_PROTO']);
561
				$proto = \strtolower(\trim($parts[0]));
562
			} else {
563
				$proto = \strtolower($this->server['HTTP_X_FORWARDED_PROTO']);
564
			}
565
566
			// Verify that the protocol is always HTTP or HTTPS
567
			// default to http if an invalid value is provided
568
			return $proto === 'https' ? 'https' : 'http';
569
		}
570
571
		if (isset($this->server['HTTPS'])
572
			&& $this->server['HTTPS'] !== null
573
			&& $this->server['HTTPS'] !== 'off'
574
			&& $this->server['HTTPS'] !== '') {
575
			return 'https';
576
		}
577
578
		return 'http';
579
	}
580
581
	/**
582
	 * Returns the used HTTP protocol.
583
	 *
584
	 * @return string HTTP protocol. HTTP/2, HTTP/1.1 or HTTP/1.0.
585
	 */
586 View Code Duplication
	public function getHttpProtocol() {
587
		$claimedProtocol = \strtoupper($this->server['SERVER_PROTOCOL']);
588
589
		$validProtocols = [
590
			'HTTP/1.0',
591
			'HTTP/1.1',
592
			'HTTP/2',
593
		];
594
595
		if (\in_array($claimedProtocol, $validProtocols, true)) {
596
			return $claimedProtocol;
597
		}
598
599
		return 'HTTP/1.1';
600
	}
601
602
	/**
603
	 * Returns the request uri, even if the website uses one or more
604
	 * reverse proxies
605
	 * @return string
606
	 */
607
	public function getRequestUri() {
608
		$uri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
609
		if ($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) {
610
			$uri = $this->getScriptName() . \substr($uri, \strlen($this->server['SCRIPT_NAME']));
611
		} else {
612
			$components = \parse_url($uri);
613
			if ($components === false) {
614
				// due to https://bugs.php.net/bug.php?id=70942 we have to add a
615
				// fake schema and host to successfully extract path, query and fragment
616
				$components = \parse_url("http://localhost$uri");
617
			}
618
			$uri = $components['path'];
619
			if (isset($components['query'])) {
620
				$uri .= '?'.$components['query'];
621
			}
622
			if (isset($components['fragment'])) {
623
				$uri .= '#'.$components['fragment'];
624
			}
625
		}
626
		return $uri;
627
	}
628
629
	/**
630
	 * Get raw PathInfo from request (not urldecoded)
631
	 * @throws \Exception
632
	 * @return string Path info
633
	 */
634
	public function getRawPathInfo() {
635
		$requestUri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
636
		// remove too many leading slashes - can be caused by reverse proxy configuration
637
		if (\strpos($requestUri, '/') === 0) {
638
			$requestUri = '/' . \ltrim($requestUri, '/');
639
		}
640
641
		$requestUri = \preg_replace('%/{2,}%', '/', $requestUri);
0 ignored issues
show
Bug Compatibility introduced by
The expression \preg_replace('%/{2,}%', '/', $requestUri); of type string|string[] adds the type string[] to the return on line 673 which is incompatible with the return type declared by the interface OCP\IRequest::getRawPathInfo of type string.
Loading history...
642
643
		// Remove the query string from REQUEST_URI
644
		if ($pos = \strpos($requestUri, '?')) {
645
			$requestUri = \substr($requestUri, 0, $pos);
646
		}
647
648
		$scriptName = $this->server['SCRIPT_NAME'];
649
		$scriptName = \preg_replace('%/{2,}%', '/', $scriptName);
650
		$pathInfo = $requestUri;
651
652
		// strip off the script name's dir and file name
653
		// FIXME: Sabre does not really belong here
654
		list($path, $name) = \Sabre\Uri\split($scriptName);
655
		if (!empty($path)) {
656
			if ($path === $pathInfo || \strpos($pathInfo, $path.'/') === 0) {
657
				$pathInfo = \substr($pathInfo, \strlen($path));
658
			} else {
659
				throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')");
660
			}
661
		}
662 View Code Duplication
		if (\strpos($pathInfo, "/$name") === 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
663
			$pathInfo = \substr($pathInfo, \strlen($name) + 1);
664
		}
665 View Code Duplication
		if (\is_string($name) && \strpos($pathInfo, $name) === 0) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
666
			$pathInfo = \substr($pathInfo, \strlen($name));
667
		}
668
669
		if ($pathInfo === false || $pathInfo === '/') {
670
			return '';
671
		}
672
673
		return $pathInfo;
674
	}
675
676
	/**
677
	 * Get PathInfo from request
678
	 * @throws \Exception
679
	 * @return string|false Path info or false when not found
680
	 */
681
	public function getPathInfo() {
682
		$pathInfo = isset($this->server['PATH_INFO']) ? $this->server['PATH_INFO'] : '';
683
		if ($pathInfo !== '') {
684
			return $pathInfo;
685
		}
686
687
		$pathInfo = $this->getRawPathInfo();
688
		// following is taken from \Sabre\HTTP\URLUtil::decodePathSegment
689
		$pathInfo = \rawurldecode($pathInfo);
690
		$encoding = \mb_detect_encoding($pathInfo, ['UTF-8', 'ISO-8859-1']);
691
692
		switch ($encoding) {
693
			case 'ISO-8859-1':
694
				$pathInfo = \utf8_encode($pathInfo);
695
		}
696
		// end copy
697
698
		return $pathInfo;
699
	}
700
701
	/**
702
	 * Returns the script name, even if the website uses one or more
703
	 * reverse proxies
704
	 * @return string the script name
705
	 */
706
	public function getScriptName() {
707
		$name = $this->server['SCRIPT_NAME'];
708
		$overwriteWebRoot =  $this->config->getSystemValue('overwritewebroot');
709
		if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) {
710
			// FIXME: This code is untestable due to __DIR__, also that hardcoded path is really dangerous
711
			$serverRoot = \str_replace('\\', '/', \substr(__DIR__, 0, -\strlen('lib/private/appframework/http/')));
712
			$suburi = \str_replace('\\', '/', \substr(\realpath($this->server['SCRIPT_FILENAME']), \strlen($serverRoot)));
713
			$name = '/' . \ltrim($overwriteWebRoot . $suburi, '/');
714
		}
715
		return $name;
716
	}
717
718
	/**
719
	 * Checks whether the user agent matches a given regex
720
	 * @param array $agent array of agent names
721
	 * @return bool true if at least one of the given agent matches, false otherwise
722
	 */
723
	public function isUserAgent(array $agent) {
724
		if (!isset($this->server['HTTP_USER_AGENT'])) {
725
			return false;
726
		}
727
		foreach ($agent as $regex) {
728
			if (\preg_match($regex, $this->server['HTTP_USER_AGENT'])) {
729
				return true;
730
			}
731
		}
732
		return false;
733
	}
734
735
	/**
736
	 * Returns the unverified server host from the headers without checking
737
	 * whether it is a trusted domain
738
	 * @return string Server host
739
	 */
740
	public function getInsecureServerHost() {
741
		$host = 'localhost';
742
		if (isset($this->server['HTTP_X_FORWARDED_HOST'])) {
743 View Code Duplication
			if (\strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
744
				$parts = \explode(',', $this->server['HTTP_X_FORWARDED_HOST']);
745
				$host = \trim(\current($parts));
746
			} else {
747
				$host = $this->server['HTTP_X_FORWARDED_HOST'];
748
			}
749
		} else {
750
			if (isset($this->server['HTTP_HOST'])) {
751
				$host = $this->server['HTTP_HOST'];
752
			} elseif (isset($this->server['SERVER_NAME'])) {
753
				$host = $this->server['SERVER_NAME'];
754
			}
755
		}
756
		return $host;
757
	}
758
759
	/**
760
	 * Returns the server host from the headers, or the first configured
761
	 * trusted domain if the host isn't in the trusted list
762
	 * @return string Server host
763
	 */
764
	public function getServerHost() {
765
		// overwritehost is always trusted
766
		$host = $this->getOverwriteHost();
767
		if ($host !== null) {
768
			return $host;
769
		}
770
771
		// get the host from the headers
772
		$host = $this->getInsecureServerHost();
773
774
		// Verify that the host is a trusted domain if the trusted domains
775
		// are defined
776
		// If no trusted domain is provided the first trusted domain is returned
777
		$trustedDomainHelper = new TrustedDomainHelper($this->config);
778
		if ($trustedDomainHelper->isTrustedDomain($host)) {
779
			return $host;
780
		} else {
781
			$trustedList = $this->config->getSystemValue('trusted_domains', []);
782
			if (!empty($trustedList)) {
783
				return $trustedList[0];
784
			} else {
785
				return '';
786
			}
787
		}
788
	}
789
790
	/**
791
	 * Returns the overwritehost setting from the config if set and
792
	 * if the overwrite condition is met
793
	 * @return string|null overwritehost value or null if not defined or the defined condition
794
	 * isn't met
795
	 */
796
	private function getOverwriteHost() {
797
		if ($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) {
798
			return $this->config->getSystemValue('overwritehost');
799
		}
800
		return null;
801
	}
802
}
803