Completed
Push — stable9 ( 485cb1...e094cf )
by Lukas
26:41 queued 26:23
created

lib/private/appframework/http/request.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bart Visscher <[email protected]>
6
 * @author Bernhard Posselt <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Mitar <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Robin McCorkell <[email protected]>
14
 * @author Roeland Jago Douma <[email protected]>
15
 * @author Thomas Müller <[email protected]>
16
 * @author Thomas Tanghus <[email protected]>
17
 * @author Vincent Petry <[email protected]>
18
 *
19
 * @license AGPL-3.0
20
 *
21
 * This code is free software: you can redistribute it and/or modify
22
 * it under the terms of the GNU Affero General Public License, version 3,
23
 * as published by the Free Software Foundation.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License, version 3,
31
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
32
 *
33
 */
34
35
namespace OC\AppFramework\Http;
36
37
use OC\Security\CSRF\CsrfToken;
38
use OC\Security\CSRF\CsrfTokenManager;
39
use OC\Security\TrustedDomainHelper;
40
use OCP\IConfig;
41
use OCP\IRequest;
42
use OCP\Security\ICrypto;
43
use OCP\Security\ISecureRandom;
44
45
/**
46
 * Class for accessing variables in the request.
47
 * This class provides an immutable object with request variables.
48
 */
49
class Request implements \ArrayAccess, \Countable, IRequest {
50
51
	const USER_AGENT_IE = '/(MSIE)|(Trident)/';
52
	const USER_AGENT_IE_8 = '/MSIE 8.0/';
53
	// Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
54
	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.]+$/';
55
	// Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference
56
	const USER_AGENT_FIREFOX = '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/[0-9.]+$/';
57
	// Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
58
	const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+$/';
59
	// Safari User Agent from http://www.useragentstring.com/pages/Safari/
60
	const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/';
61
	// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
62
	const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#';
63
	const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#';
64
	const USER_AGENT_OWNCLOUD_IOS = '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/';
65
	const USER_AGENT_OWNCLOUD_ANDROID = '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/';
66
	const USER_AGENT_OWNCLOUD_DESKTOP = '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/';
67
	const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost)$/';
68
69
	protected $inputStream;
70
	protected $content;
71
	protected $items = array();
72
	protected $allowedKeys = array(
73
		'get',
74
		'post',
75
		'files',
76
		'server',
77
		'env',
78
		'cookies',
79
		'urlParams',
80
		'parameters',
81
		'method',
82
		'requesttoken',
83
	);
84
	/** @var ISecureRandom */
85
	protected $secureRandom;
86
	/** @var IConfig */
87
	protected $config;
88
	/** @var string */
89
	protected $requestId = '';
90
	/** @var ICrypto */
91
	protected $crypto;
92
	/** @var CsrfTokenManager|null */
93
	protected $csrfTokenManager;
94
95
	/** @var bool */
96
	protected $contentDecoded = false;
97
98
	/**
99
	 * @param array $vars An associative array with the following optional values:
100
	 *        - array 'urlParams' the parameters which were matched from the URL
101
	 *        - array 'get' the $_GET array
102
	 *        - array|string 'post' the $_POST array or JSON string
103
	 *        - array 'files' the $_FILES array
104
	 *        - array 'server' the $_SERVER array
105
	 *        - array 'env' the $_ENV array
106
	 *        - array 'cookies' the $_COOKIE array
107
	 *        - string 'method' the request method (GET, POST etc)
108
	 *        - string|false 'requesttoken' the requesttoken or false when not available
109
	 * @param ISecureRandom $secureRandom
110
	 * @param IConfig $config
111
	 * @param CsrfTokenManager|null $csrfTokenManager
112
	 * @param string $stream
113
	 * @see http://www.php.net/manual/en/reserved.variables.php
114
	 */
115
	public function __construct(array $vars=array(),
116
								ISecureRandom $secureRandom = null,
117
								IConfig $config,
118
								CsrfTokenManager $csrfTokenManager = null,
119
								$stream = 'php://input') {
120
		$this->inputStream = $stream;
121
		$this->items['params'] = array();
122
		$this->secureRandom = $secureRandom;
123
		$this->config = $config;
124
		$this->csrfTokenManager = $csrfTokenManager;
125
126
		if(!array_key_exists('method', $vars)) {
127
			$vars['method'] = 'GET';
128
		}
129
130
		foreach($this->allowedKeys as $name) {
131
			$this->items[$name] = isset($vars[$name])
132
				? $vars[$name]
133
				: array();
134
		}
135
136
		$this->items['parameters'] = array_merge(
137
			$this->items['get'],
138
			$this->items['post'],
139
			$this->items['urlParams'],
140
			$this->items['params']
141
		);
142
143
	}
144
	/**
145
	 * @param array $parameters
146
	 */
147
	public function setUrlParameters(array $parameters) {
148
		$this->items['urlParams'] = $parameters;
149
		$this->items['parameters'] = array_merge(
150
			$this->items['parameters'],
151
			$this->items['urlParams']
152
		);
153
	}
154
155
	/**
156
	 * Countable method
157
	 * @return int
158
	 */
159
	public function count() {
160
		return count(array_keys($this->items['parameters']));
161
	}
162
163
	/**
164
	* ArrayAccess methods
165
	*
166
	* Gives access to the combined GET, POST and urlParams arrays
167
	*
168
	* Examples:
169
	*
170
	* $var = $request['myvar'];
171
	*
172
	* or
173
	*
174
	* if(!isset($request['myvar']) {
175
	* 	// Do something
176
	* }
177
	*
178
	* $request['myvar'] = 'something'; // This throws an exception.
179
	*
180
	* @param string $offset The key to lookup
181
	* @return boolean
182
	*/
183
	public function offsetExists($offset) {
184
		return isset($this->items['parameters'][$offset]);
185
	}
186
187
	/**
188
	* @see offsetExists
189
	*/
190
	public function offsetGet($offset) {
191
		return isset($this->items['parameters'][$offset])
192
			? $this->items['parameters'][$offset]
193
			: null;
194
	}
195
196
	/**
197
	* @see offsetExists
198
	*/
199
	public function offsetSet($offset, $value) {
200
		throw new \RuntimeException('You cannot change the contents of the request object');
201
	}
202
203
	/**
204
	* @see offsetExists
205
	*/
206
	public function offsetUnset($offset) {
207
		throw new \RuntimeException('You cannot change the contents of the request object');
208
	}
209
210
	/**
211
	 * Magic property accessors
212
	 * @param string $name
213
	 * @param mixed $value
214
	 */
215
	public function __set($name, $value) {
216
		throw new \RuntimeException('You cannot change the contents of the request object');
217
	}
218
219
	/**
220
	* Access request variables by method and name.
221
	* Examples:
222
	*
223
	* $request->post['myvar']; // Only look for POST variables
224
	* $request->myvar; or $request->{'myvar'}; or $request->{$myvar}
225
	* Looks in the combined GET, POST and urlParams array.
226
	*
227
	* If you access e.g. ->post but the current HTTP request method
228
	* is GET a \LogicException will be thrown.
229
	*
230
	* @param string $name The key to look for.
231
	* @throws \LogicException
232
	* @return mixed|null
233
	*/
234
	public function __get($name) {
235
		switch($name) {
236
			case 'put':
237
			case 'patch':
238
			case 'get':
239
			case 'post':
240
				if($this->method !== strtoupper($name)) {
241
					throw new \LogicException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method));
242
				}
243
				return $this->getContent();
244
			case 'files':
245
			case 'server':
246
			case 'env':
247
			case 'cookies':
248
			case 'urlParams':
249
			case 'method':
250
				return isset($this->items[$name])
251
					? $this->items[$name]
252
					: null;
253
			case 'parameters':
254
			case 'params':
255
				return $this->getContent();
256
			default;
257
				return isset($this[$name])
258
					? $this[$name]
259
					: null;
260
		}
261
	}
262
263
	/**
264
	 * @param string $name
265
	 * @return bool
266
	 */
267
	public function __isset($name) {
268
		if (in_array($name, $this->allowedKeys, true)) {
269
			return true;
270
		}
271
		return isset($this->items['parameters'][$name]);
272
	}
273
274
	/**
275
	 * @param string $id
276
	 */
277
	public function __unset($id) {
278
		throw new \RunTimeException('You cannot change the contents of the request object');
279
	}
280
281
	/**
282
	 * Returns the value for a specific http header.
283
	 *
284
	 * This method returns null if the header did not exist.
285
	 *
286
	 * @param string $name
287
	 * @return string
288
	 */
289
	public function getHeader($name) {
290
291
		$name = strtoupper(str_replace(array('-'),array('_'),$name));
292
		if (isset($this->server['HTTP_' . $name])) {
293
			return $this->server['HTTP_' . $name];
294
		}
295
296
		// There's a few headers that seem to end up in the top-level
297
		// server array.
298
		switch($name) {
299
			case 'CONTENT_TYPE' :
300
			case 'CONTENT_LENGTH' :
301
				if (isset($this->server[$name])) {
302
					return $this->server[$name];
303
				}
304
				break;
305
306
		}
307
308
		return null;
309
	}
310
311
	/**
312
	 * Lets you access post and get parameters by the index
313
	 * In case of json requests the encoded json body is accessed
314
	 *
315
	 * @param string $key the key which you want to access in the URL Parameter
316
	 *                     placeholder, $_POST or $_GET array.
317
	 *                     The priority how they're returned is the following:
318
	 *                     1. URL parameters
319
	 *                     2. POST parameters
320
	 *                     3. GET parameters
321
	 * @param mixed $default If the key is not found, this value will be returned
322
	 * @return mixed the content of the array
323
	 */
324
	public function getParam($key, $default = null) {
325
		return isset($this->parameters[$key])
326
			? $this->parameters[$key]
327
			: $default;
328
	}
329
330
	/**
331
	 * Returns all params that were received, be it from the request
332
	 * (as GET or POST) or throuh the URL by the route
333
	 * @return array the array with all parameters
334
	 */
335
	public function getParams() {
336
		return $this->parameters;
337
	}
338
339
	/**
340
	 * Returns the method of the request
341
	 * @return string the method of the request (POST, GET, etc)
342
	 */
343
	public function getMethod() {
344
		return $this->method;
345
	}
346
347
	/**
348
	 * Shortcut for accessing an uploaded file through the $_FILES array
349
	 * @param string $key the key that will be taken from the $_FILES array
350
	 * @return array the file in the $_FILES element
351
	 */
352
	public function getUploadedFile($key) {
353
		return isset($this->files[$key]) ? $this->files[$key] : null;
354
	}
355
356
	/**
357
	 * Shortcut for getting env variables
358
	 * @param string $key the key that will be taken from the $_ENV array
359
	 * @return array the value in the $_ENV element
360
	 */
361
	public function getEnv($key) {
362
		return isset($this->env[$key]) ? $this->env[$key] : null;
363
	}
364
365
	/**
366
	 * Shortcut for getting cookie variables
367
	 * @param string $key the key that will be taken from the $_COOKIE array
368
	 * @return array the value in the $_COOKIE element
369
	 */
370
	public function getCookie($key) {
371
		return isset($this->cookies[$key]) ? $this->cookies[$key] : null;
372
	}
373
374
	/**
375
	 * Returns the request body content.
376
	 *
377
	 * If the HTTP request method is PUT and the body
378
	 * not application/x-www-form-urlencoded or application/json a stream
379
	 * resource is returned, otherwise an array.
380
	 *
381
	 * @return array|string|resource The request body content or a resource to read the body stream.
382
	 *
383
	 * @throws \LogicException
384
	 */
385
	protected function getContent() {
386
		// If the content can't be parsed into an array then return a stream resource.
387
		if ($this->method === 'PUT'
388
			&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false
389
			&& strpos($this->getHeader('Content-Type'), 'application/json') === false
390
		) {
391
			if ($this->content === false) {
392
				throw new \LogicException(
393
					'"put" can only be accessed once if not '
394
					. 'application/x-www-form-urlencoded or application/json.'
395
				);
396
			}
397
			$this->content = false;
398
			return fopen($this->inputStream, 'rb');
399
		} else {
400
			$this->decodeContent();
401
			return $this->items['parameters'];
402
		}
403
	}
404
405
	/**
406
	 * Attempt to decode the content and populate parameters
407
	 */
408
	protected function decodeContent() {
409
		if ($this->contentDecoded) {
410
			return;
411
		}
412
		$params = [];
413
414
		// 'application/json' must be decoded manually.
415
		if (strpos($this->getHeader('Content-Type'), 'application/json') !== false) {
416
			$params = json_decode(file_get_contents($this->inputStream), true);
417
			if(count($params) > 0) {
418
				$this->items['params'] = $params;
419
				if($this->method === 'POST') {
420
					$this->items['post'] = $params;
421
				}
422
			}
423
424
		// Handle application/x-www-form-urlencoded for methods other than GET
425
		// or post correctly
426
		} elseif($this->method !== 'GET'
427
				&& $this->method !== 'POST'
428
				&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) {
429
430
			parse_str(file_get_contents($this->inputStream), $params);
431
			if(is_array($params)) {
432
				$this->items['params'] = $params;
433
			}
434
		}
435
436
		if (is_array($params)) {
437
			$this->items['parameters'] = array_merge($this->items['parameters'], $params);
438
		}
439
		$this->contentDecoded = true;
440
	}
441
442
443
	/**
444
	 * Checks if the CSRF check was correct
445
	 * @return bool true if CSRF check passed
446
	 */
447
	public function passesCSRFCheck() {
448
		if($this->csrfTokenManager === null) {
449
			return false;
450
		}
451
452
		if(!$this->passesStrictCookieCheck()) {
453
			return false;
454
		}
455
456
		if (isset($this->items['get']['requesttoken'])) {
457
			$token = $this->items['get']['requesttoken'];
458
		} elseif (isset($this->items['post']['requesttoken'])) {
459
			$token = $this->items['post']['requesttoken'];
460
		} elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) {
461
			$token = $this->items['server']['HTTP_REQUESTTOKEN'];
462
		} else {
463
			//no token found.
464
			return false;
465
		}
466
		$token = new CsrfToken($token);
467
468
		return $this->csrfTokenManager->isTokenValid($token);
469
	}
470
471
	/**
472
	 * Checks if the strict cookie has been sent with the request if the request
473
	 * is including any cookies.
474
	 *
475
	 * @return bool
476
	 * @since 9.1.0
477
	 */
478
	public function passesStrictCookieCheck() {
479
		if(count($this->cookies) === 0) {
480
			return true;
481
		}
482
		if($this->getCookie('nc_sameSiteCookiestrict') === 'true'
483
			&& $this->passesLaxCookieCheck()) {
484
			return true;
485
		}
486
487
		return false;
488
	}
489
490
	/**
491
	 * Checks if the lax cookie has been sent with the request if the request
492
	 * is including any cookies.
493
	 *
494
	 * @return bool
495
	 * @since 9.1.0
496
	 */
497
	public function passesLaxCookieCheck() {
498
		if(count($this->cookies) === 0) {
499
			return true;
500
		}
501
502
		if($this->getCookie('nc_sameSiteCookielax') === 'true') {
503
			return true;
504
		}
505
506
		return false;
507
	}
508
509
	/**
510
	 * Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
511
	 * If `mod_unique_id` is installed this value will be taken.
512
	 * @return string
513
	 */
514
	public function getId() {
515
		if(isset($this->server['UNIQUE_ID'])) {
516
			return $this->server['UNIQUE_ID'];
517
		}
518
519
		if(empty($this->requestId)) {
520
			$this->requestId = $this->secureRandom->generate(20);
521
		}
522
523
		return $this->requestId;
524
	}
525
526
	/**
527
	 * Returns the remote address, if the connection came from a trusted proxy
528
	 * and `forwarded_for_headers` has been configured then the IP address
529
	 * specified in this header will be returned instead.
530
	 * Do always use this instead of $_SERVER['REMOTE_ADDR']
531
	 * @return string IP address
532
	 */
533
	public function getRemoteAddress() {
534
		$remoteAddress = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
535
		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
536
537
		if(is_array($trustedProxies) && in_array($remoteAddress, $trustedProxies)) {
538
			$forwardedForHeaders = $this->config->getSystemValue('forwarded_for_headers', [
539
				'HTTP_X_FORWARDED_FOR'
540
				// only have one default, so we cannot ship an insecure product out of the box
541
			]);
542
543
			foreach($forwardedForHeaders as $header) {
544
				if(isset($this->server[$header])) {
545
					foreach(explode(',', $this->server[$header]) as $IP) {
546
						$IP = trim($IP);
547
						if (filter_var($IP, FILTER_VALIDATE_IP) !== false) {
548
							return $IP;
549
						}
550
					}
551
				}
552
			}
553
		}
554
555
		return $remoteAddress;
556
	}
557
558
	/**
559
	 * Check overwrite condition
560
	 * @param string $type
561
	 * @return bool
562
	 */
563
	private function isOverwriteCondition($type = '') {
564
		$regex = '/' . $this->config->getSystemValue('overwritecondaddr', '')  . '/';
565
		$remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
566
		return $regex === '//' || preg_match($regex, $remoteAddr) === 1
567
		|| $type !== 'protocol';
568
	}
569
570
	/**
571
	 * Returns the server protocol. It respects one or more reverse proxies servers
572
	 * and load balancers
573
	 * @return string Server protocol (http or https)
574
	 */
575
	public function getServerProtocol() {
576
		if($this->config->getSystemValue('overwriteprotocol') !== ''
577
			&& $this->isOverwriteCondition('protocol')) {
578
			return $this->config->getSystemValue('overwriteprotocol');
579
		}
580
581
		if (isset($this->server['HTTP_X_FORWARDED_PROTO'])) {
582 View Code Duplication
			if (strpos($this->server['HTTP_X_FORWARDED_PROTO'], ',') !== false) {
583
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_PROTO']);
584
				$proto = strtolower(trim($parts[0]));
585
			} else {
586
				$proto = strtolower($this->server['HTTP_X_FORWARDED_PROTO']);
587
			}
588
589
			// Verify that the protocol is always HTTP or HTTPS
590
			// default to http if an invalid value is provided
591
			return $proto === 'https' ? 'https' : 'http';
592
		}
593
594
		if (isset($this->server['HTTPS'])
595
			&& $this->server['HTTPS'] !== null
596
			&& $this->server['HTTPS'] !== 'off'
597
			&& $this->server['HTTPS'] !== '') {
598
			return 'https';
599
		}
600
601
		return 'http';
602
	}
603
604
	/**
605
	 * Returns the used HTTP protocol.
606
	 *
607
	 * @return string HTTP protocol. HTTP/2, HTTP/1.1 or HTTP/1.0.
608
	 */
609 View Code Duplication
	public function getHttpProtocol() {
0 ignored issues
show
This method seems to be duplicated in 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...
610
		$claimedProtocol = strtoupper($this->server['SERVER_PROTOCOL']);
611
612
		$validProtocols = [
613
			'HTTP/1.0',
614
			'HTTP/1.1',
615
			'HTTP/2',
616
		];
617
618
		if(in_array($claimedProtocol, $validProtocols, true)) {
619
			return $claimedProtocol;
620
		}
621
622
		return 'HTTP/1.1';
623
	}
624
625
	/**
626
	 * Returns the request uri, even if the website uses one or more
627
	 * reverse proxies
628
	 * @return string
629
	 */
630
	public function getRequestUri() {
631
		$uri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
632
		if($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) {
633
			$uri = $this->getScriptName() . substr($uri, strlen($this->server['SCRIPT_NAME']));
634
		}
635
		return $uri;
636
	}
637
638
	/**
639
	 * Get raw PathInfo from request (not urldecoded)
640
	 * @throws \Exception
641
	 * @return string Path info
642
	 */
643
	public function getRawPathInfo() {
644
		$requestUri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
645
		// remove too many leading slashes - can be caused by reverse proxy configuration
646
		if (strpos($requestUri, '/') === 0) {
647
			$requestUri = '/' . ltrim($requestUri, '/');
648
		}
649
650
		$requestUri = preg_replace('%/{2,}%', '/', $requestUri);
651
652
		// Remove the query string from REQUEST_URI
653
		if ($pos = strpos($requestUri, '?')) {
654
			$requestUri = substr($requestUri, 0, $pos);
655
		}
656
657
		$scriptName = $this->server['SCRIPT_NAME'];
658
		$pathInfo = $requestUri;
659
660
		// strip off the script name's dir and file name
661
		// FIXME: Sabre does not really belong here
662
		list($path, $name) = \Sabre\HTTP\URLUtil::splitPath($scriptName);
663
		if (!empty($path)) {
664
			if($path === $pathInfo || strpos($pathInfo, $path.'/') === 0) {
665
				$pathInfo = substr($pathInfo, strlen($path));
666
			} else {
667
				throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')");
668
			}
669
		}
670
		if (strpos($pathInfo, '/'.$name) === 0) {
671
			$pathInfo = substr($pathInfo, strlen($name) + 1);
672
		}
673
		if (strpos($pathInfo, $name) === 0) {
674
			$pathInfo = substr($pathInfo, strlen($name));
675
		}
676
		if($pathInfo === false || $pathInfo === '/'){
677
			return '';
678
		} else {
679
			return $pathInfo;
680
		}
681
	}
682
683
	/**
684
	 * Get PathInfo from request
685
	 * @throws \Exception
686
	 * @return string|false Path info or false when not found
687
	 */
688
	public function getPathInfo() {
689
		if(isset($this->server['PATH_INFO'])) {
690
			return $this->server['PATH_INFO'];
691
		}
692
693
		$pathInfo = $this->getRawPathInfo();
694
		// following is taken from \Sabre\HTTP\URLUtil::decodePathSegment
695
		$pathInfo = rawurldecode($pathInfo);
696
		$encoding = mb_detect_encoding($pathInfo, ['UTF-8', 'ISO-8859-1']);
697
698
		switch($encoding) {
699
			case 'ISO-8859-1' :
700
				$pathInfo = utf8_encode($pathInfo);
701
		}
702
		// end copy
703
704
		return $pathInfo;
705
	}
706
707
	/**
708
	 * Returns the script name, even if the website uses one or more
709
	 * reverse proxies
710
	 * @return string the script name
711
	 */
712
	public function getScriptName() {
713
		$name = $this->server['SCRIPT_NAME'];
714
		$overwriteWebRoot =  $this->config->getSystemValue('overwritewebroot');
715
		if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) {
716
			// FIXME: This code is untestable due to __DIR__, also that hardcoded path is really dangerous
717
			$serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -strlen('lib/private/appframework/http/')));
718
			$suburi = str_replace('\\', '/', substr(realpath($this->server['SCRIPT_FILENAME']), strlen($serverRoot)));
719
			$name = '/' . ltrim($overwriteWebRoot . $suburi, '/');
720
		}
721
		return $name;
722
	}
723
724
	/**
725
	 * Checks whether the user agent matches a given regex
726
	 * @param array $agent array of agent names
727
	 * @return bool true if at least one of the given agent matches, false otherwise
728
	 */
729
	public function isUserAgent(array $agent) {
730
		if (!isset($this->server['HTTP_USER_AGENT'])) {
731
			return false;
732
		}
733
		foreach ($agent as $regex) {
734
			if (preg_match($regex, $this->server['HTTP_USER_AGENT'])) {
735
				return true;
736
			}
737
		}
738
		return false;
739
	}
740
741
	/**
742
	 * Returns the unverified server host from the headers without checking
743
	 * whether it is a trusted domain
744
	 * @return string Server host
745
	 */
746
	public function getInsecureServerHost() {
747
		$host = 'localhost';
748
		if (isset($this->server['HTTP_X_FORWARDED_HOST'])) {
749 View Code Duplication
			if (strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) {
750
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_HOST']);
751
				$host = trim(current($parts));
752
			} else {
753
				$host = $this->server['HTTP_X_FORWARDED_HOST'];
754
			}
755
		} else {
756
			if (isset($this->server['HTTP_HOST'])) {
757
				$host = $this->server['HTTP_HOST'];
758
			} else if (isset($this->server['SERVER_NAME'])) {
759
				$host = $this->server['SERVER_NAME'];
760
			}
761
		}
762
		return $host;
763
	}
764
765
766
	/**
767
	 * Returns the server host from the headers, or the first configured
768
	 * trusted domain if the host isn't in the trusted list
769
	 * @return string Server host
770
	 */
771
	public function getServerHost() {
772
		// overwritehost is always trusted
773
		$host = $this->getOverwriteHost();
774
		if ($host !== null) {
775
			return $host;
776
		}
777
778
		// get the host from the headers
779
		$host = $this->getInsecureServerHost();
780
781
		// Verify that the host is a trusted domain if the trusted domains
782
		// are defined
783
		// If no trusted domain is provided the first trusted domain is returned
784
		$trustedDomainHelper = new TrustedDomainHelper($this->config);
785
		if ($trustedDomainHelper->isTrustedDomain($host)) {
786
			return $host;
787
		} else {
788
			$trustedList = $this->config->getSystemValue('trusted_domains', []);
789
			if(!empty($trustedList)) {
790
				return $trustedList[0];
791
			} else {
792
				return '';
793
			}
794
		}
795
	}
796
797
	/**
798
	 * Returns the overwritehost setting from the config if set and
799
	 * if the overwrite condition is met
800
	 * @return string|null overwritehost value or null if not defined or the defined condition
801
	 * isn't met
802
	 */
803
	private function getOverwriteHost() {
804
		if($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) {
805
			return $this->config->getSystemValue('overwritehost');
806
		}
807
		return null;
808
	}
809
810
}
811