Completed
Pull Request — master (#479)
by Lukas
33:52 queued 25:33
created

Request::__unset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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) 2016, ownCloud, Inc.
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
57
	const USER_AGENT_IE = '/(MSIE)|(Trident)/';
58
	const USER_AGENT_IE_8 = '/MSIE 8.0/';
59
	// Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
60
	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.]+$/';
61
	// Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference
62
	const USER_AGENT_FIREFOX = '/^Mozilla\/5\.0 \([^)]+\) Gecko\/[0-9.]+ Firefox\/[0-9.]+$/';
63
	// Chrome User Agent from https://developer.chrome.com/multidevice/user-agent
64
	const USER_AGENT_CHROME = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (Mobile Safari|Safari)\/[0-9.]+$/';
65
	// Safari User Agent from http://www.useragentstring.com/pages/Safari/
66
	const USER_AGENT_SAFARI = '/^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Version\/[0-9.]+ Safari\/[0-9.A-Z]+$/';
67
	// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
68
	const USER_AGENT_ANDROID_MOBILE_CHROME = '#Android.*Chrome/[.0-9]*#';
69
	const USER_AGENT_FREEBOX = '#^Mozilla/5\.0$#';
70
	const REGEX_LOCALHOST = '/^(127\.0\.0\.1|localhost)$/';
71
72
	/**
73
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_IOS instead
74
	 */
75
	const USER_AGENT_OWNCLOUD_IOS = '/^Mozilla\/5\.0 \(iOS\) ownCloud\-iOS.*$/';
76
	/**
77
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_ANDROID instead
78
	 */
79
	const USER_AGENT_OWNCLOUD_ANDROID = '/^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/';
80
	/**
81
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_DESKTOP instead
82
	 */
83
	const USER_AGENT_OWNCLOUD_DESKTOP = '/^Mozilla\/5\.0 \([A-Za-z ]+\) (mirall|csyncoC)\/.*$/';
84
85
	protected $inputStream;
86
	protected $content;
87
	protected $items = array();
88
	protected $allowedKeys = array(
89
		'get',
90
		'post',
91
		'files',
92
		'server',
93
		'env',
94
		'cookies',
95
		'urlParams',
96
		'parameters',
97
		'method',
98
		'requesttoken',
99
	);
100
	/** @var ISecureRandom */
101
	protected $secureRandom;
102
	/** @var IConfig */
103
	protected $config;
104
	/** @var string */
105
	protected $requestId = '';
106
	/** @var ICrypto */
107
	protected $crypto;
108
	/** @var CsrfTokenManager|null */
109
	protected $csrfTokenManager;
110
111
	/** @var bool */
112
	protected $contentDecoded = false;
113
114
	/**
115
	 * @param array $vars An associative array with the following optional values:
116
	 *        - array 'urlParams' the parameters which were matched from the URL
117
	 *        - array 'get' the $_GET array
118
	 *        - array|string 'post' the $_POST array or JSON string
119
	 *        - array 'files' the $_FILES array
120
	 *        - array 'server' the $_SERVER array
121
	 *        - array 'env' the $_ENV array
122
	 *        - array 'cookies' the $_COOKIE array
123
	 *        - string 'method' the request method (GET, POST etc)
124
	 *        - string|false 'requesttoken' the requesttoken or false when not available
125
	 * @param ISecureRandom $secureRandom
126
	 * @param IConfig $config
127
	 * @param CsrfTokenManager|null $csrfTokenManager
128
	 * @param string $stream
129
	 * @see http://www.php.net/manual/en/reserved.variables.php
130
	 */
131
	public function __construct(array $vars=array(),
132
								ISecureRandom $secureRandom = null,
133
								IConfig $config,
134
								CsrfTokenManager $csrfTokenManager = null,
135
								$stream = 'php://input') {
136
		$this->inputStream = $stream;
137
		$this->items['params'] = array();
138
		$this->secureRandom = $secureRandom;
139
		$this->config = $config;
140
		$this->csrfTokenManager = $csrfTokenManager;
141
142
		if(!array_key_exists('method', $vars)) {
143
			$vars['method'] = 'GET';
144
		}
145
146
		foreach($this->allowedKeys as $name) {
147
			$this->items[$name] = isset($vars[$name])
148
				? $vars[$name]
149
				: array();
150
		}
151
152
		$this->items['parameters'] = array_merge(
153
			$this->items['get'],
154
			$this->items['post'],
155
			$this->items['urlParams'],
156
			$this->items['params']
157
		);
158
159
	}
160
	/**
161
	 * @param array $parameters
162
	 */
163
	public function setUrlParameters(array $parameters) {
164
		$this->items['urlParams'] = $parameters;
165
		$this->items['parameters'] = array_merge(
166
			$this->items['parameters'],
167
			$this->items['urlParams']
168
		);
169
	}
170
171
	/**
172
	 * Countable method
173
	 * @return int
174
	 */
175
	public function count() {
176
		return count(array_keys($this->items['parameters']));
177
	}
178
179
	/**
180
	* ArrayAccess methods
181
	*
182
	* Gives access to the combined GET, POST and urlParams arrays
183
	*
184
	* Examples:
185
	*
186
	* $var = $request['myvar'];
187
	*
188
	* or
189
	*
190
	* if(!isset($request['myvar']) {
191
	* 	// Do something
192
	* }
193
	*
194
	* $request['myvar'] = 'something'; // This throws an exception.
195
	*
196
	* @param string $offset The key to lookup
197
	* @return boolean
198
	*/
199
	public function offsetExists($offset) {
200
		return isset($this->items['parameters'][$offset]);
201
	}
202
203
	/**
204
	* @see offsetExists
205
	*/
206
	public function offsetGet($offset) {
207
		return isset($this->items['parameters'][$offset])
208
			? $this->items['parameters'][$offset]
209
			: null;
210
	}
211
212
	/**
213
	* @see offsetExists
214
	*/
215
	public function offsetSet($offset, $value) {
216
		throw new \RuntimeException('You cannot change the contents of the request object');
217
	}
218
219
	/**
220
	* @see offsetExists
221
	*/
222
	public function offsetUnset($offset) {
223
		throw new \RuntimeException('You cannot change the contents of the request object');
224
	}
225
226
	/**
227
	 * Magic property accessors
228
	 * @param string $name
229
	 * @param mixed $value
230
	 */
231
	public function __set($name, $value) {
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
232
		throw new \RuntimeException('You cannot change the contents of the request object');
233
	}
234
235
	/**
236
	* Access request variables by method and name.
237
	* Examples:
238
	*
239
	* $request->post['myvar']; // Only look for POST variables
240
	* $request->myvar; or $request->{'myvar'}; or $request->{$myvar}
241
	* Looks in the combined GET, POST and urlParams array.
242
	*
243
	* If you access e.g. ->post but the current HTTP request method
244
	* is GET a \LogicException will be thrown.
245
	*
246
	* @param string $name The key to look for.
247
	* @throws \LogicException
248
	* @return mixed|null
249
	*/
250
	public function __get($name) {
251
		switch($name) {
252
			case 'put':
253
			case 'patch':
254
			case 'get':
255
			case 'post':
256
				if($this->method !== strtoupper($name)) {
257
					throw new \LogicException(sprintf('%s cannot be accessed in a %s request.', $name, $this->method));
258
				}
259
				return $this->getContent();
260
			case 'files':
261
			case 'server':
262
			case 'env':
263
			case 'cookies':
264
			case 'urlParams':
265
			case 'method':
266
				return isset($this->items[$name])
267
					? $this->items[$name]
268
					: null;
269
			case 'parameters':
270
			case 'params':
271
				return $this->getContent();
272
			default;
273
				return isset($this[$name])
274
					? $this[$name]
275
					: null;
276
		}
277
	}
278
279
	/**
280
	 * @param string $name
281
	 * @return bool
282
	 */
283
	public function __isset($name) {
284
		if (in_array($name, $this->allowedKeys, true)) {
285
			return true;
286
		}
287
		return isset($this->items['parameters'][$name]);
288
	}
289
290
	/**
291
	 * @param string $id
292
	 */
293
	public function __unset($id) {
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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