Completed
Push — stable10 ( de921c...b9100d )
by Roeland
10:18
created

Request::cookieCheckRequired()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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