Completed
Push — master ( ca3aef...5b2d6b )
by Morris
15:49
created

Request::cookieCheckRequired()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 0
dl 0
loc 10
rs 9.2
c 0
b 0
f 0
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
	// 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\)( Ubuntu Chromium\/[0-9.]+|) 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|::1)$/';
71
72
	/**
73
	 * @deprecated use \OCP\IRequest::USER_AGENT_CLIENT_IOS instead
74
	 */
75
	const USER_AGENT_OWNCLOUD_IOS = '/^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-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) {
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;
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
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) {
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' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
316
			case 'CONTENT_LENGTH' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
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
			&& $this->getHeader('Content-Length') !== 0
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $this->getHeader('Content-Length') (string) and 0 (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
405
			&& $this->getHeader('Content-Length') !== null
406
			&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') === false
407
			&& strpos($this->getHeader('Content-Type'), 'application/json') === false
408
		) {
409
			if ($this->content === false) {
410
				throw new \LogicException(
411
					'"put" can only be accessed once if not '
412
					. 'application/x-www-form-urlencoded or application/json.'
413
				);
414
			}
415
			$this->content = false;
416
			return fopen($this->inputStream, 'rb');
417
		} else {
418
			$this->decodeContent();
419
			return $this->items['parameters'];
420
		}
421
	}
422
423
	/**
424
	 * Attempt to decode the content and populate parameters
425
	 */
426
	protected function decodeContent() {
427
		if ($this->contentDecoded) {
428
			return;
429
		}
430
		$params = [];
431
432
		// 'application/json' must be decoded manually.
433
		if (strpos($this->getHeader('Content-Type'), 'application/json') !== false) {
434
			$params = json_decode(file_get_contents($this->inputStream), true);
435
			if($params !== null && count($params) > 0) {
436
				$this->items['params'] = $params;
437
				if($this->method === 'POST') {
438
					$this->items['post'] = $params;
439
				}
440
			}
441
442
		// Handle application/x-www-form-urlencoded for methods other than GET
443
		// or post correctly
444
		} elseif($this->method !== 'GET'
445
				&& $this->method !== 'POST'
446
				&& strpos($this->getHeader('Content-Type'), 'application/x-www-form-urlencoded') !== false) {
447
448
			parse_str(file_get_contents($this->inputStream), $params);
449
			if(is_array($params)) {
450
				$this->items['params'] = $params;
451
			}
452
		}
453
454
		if (is_array($params)) {
455
			$this->items['parameters'] = array_merge($this->items['parameters'], $params);
456
		}
457
		$this->contentDecoded = true;
458
	}
459
460
461
	/**
462
	 * Checks if the CSRF check was correct
463
	 * @return bool true if CSRF check passed
464
	 */
465
	public function passesCSRFCheck() {
466
		if($this->csrfTokenManager === null) {
467
			return false;
468
		}
469
470
		if(!$this->passesStrictCookieCheck()) {
471
			return false;
472
		}
473
474
		if (isset($this->items['get']['requesttoken'])) {
475
			$token = $this->items['get']['requesttoken'];
476
		} elseif (isset($this->items['post']['requesttoken'])) {
477
			$token = $this->items['post']['requesttoken'];
478
		} elseif (isset($this->items['server']['HTTP_REQUESTTOKEN'])) {
479
			$token = $this->items['server']['HTTP_REQUESTTOKEN'];
480
		} else {
481
			//no token found.
482
			return false;
483
		}
484
		$token = new CsrfToken($token);
485
486
		return $this->csrfTokenManager->isTokenValid($token);
487
	}
488
489
	/**
490
	 * Whether the cookie checks are required
491
	 *
492
	 * @return bool
493
	 */
494
	private function cookieCheckRequired() {
495
		if ($this->getHeader('OCS-APIREQUEST')) {
496
			return false;
497
		}
498
		if($this->getCookie(session_name()) === null && $this->getCookie('nc_token') === null) {
499
			return false;
500
		}
501
502
		return true;
503
	}
504
505
	/**
506
	 * Wrapper around session_get_cookie_params
507
	 *
508
	 * @return array
509
	 */
510
	protected function getCookieParams() {
511
		return session_get_cookie_params();
512
	}
513
514
	/**
515
	 * Appends the __Host- prefix to the cookie if applicable
516
	 *
517
	 * @param string $name
518
	 * @return string
519
	 */
520
	protected function getProtectedCookieName($name) {
521
		$cookieParams = $this->getCookieParams();
522
		$prefix = '';
523
		if($cookieParams['secure'] === true && $cookieParams['path'] === '/') {
524
			$prefix = '__Host-';
525
		}
526
527
		return $prefix.$name;
528
	}
529
530
	/**
531
	 * Checks if the strict cookie has been sent with the request if the request
532
	 * is including any cookies.
533
	 *
534
	 * @return bool
535
	 * @since 9.1.0
536
	 */
537 View Code Duplication
	public function passesStrictCookieCheck() {
538
		if(!$this->cookieCheckRequired()) {
539
			return true;
540
		}
541
542
		$cookieName = $this->getProtectedCookieName('nc_sameSiteCookiestrict');
543
		if($this->getCookie($cookieName) === 'true'
544
			&& $this->passesLaxCookieCheck()) {
545
			return true;
546
		}
547
		return false;
548
	}
549
550
	/**
551
	 * Checks if the lax cookie has been sent with the request if the request
552
	 * is including any cookies.
553
	 *
554
	 * @return bool
555
	 * @since 9.1.0
556
	 */
557 View Code Duplication
	public function passesLaxCookieCheck() {
558
		if(!$this->cookieCheckRequired()) {
559
			return true;
560
		}
561
562
		$cookieName = $this->getProtectedCookieName('nc_sameSiteCookielax');
563
		if($this->getCookie($cookieName) === 'true') {
564
			return true;
565
		}
566
		return false;
567
	}
568
569
570
	/**
571
	 * Returns an ID for the request, value is not guaranteed to be unique and is mostly meant for logging
572
	 * If `mod_unique_id` is installed this value will be taken.
573
	 * @return string
574
	 */
575
	public function getId() {
576
		if(isset($this->server['UNIQUE_ID'])) {
577
			return $this->server['UNIQUE_ID'];
578
		}
579
580
		if(empty($this->requestId)) {
581
			$validChars = ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS;
582
			$this->requestId = $this->secureRandom->generate(20, $validChars);
583
		}
584
585
		return $this->requestId;
586
	}
587
588
	/**
589
	 * Returns the remote address, if the connection came from a trusted proxy
590
	 * and `forwarded_for_headers` has been configured then the IP address
591
	 * specified in this header will be returned instead.
592
	 * Do always use this instead of $_SERVER['REMOTE_ADDR']
593
	 * @return string IP address
594
	 */
595
	public function getRemoteAddress() {
596
		$remoteAddress = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
597
		$trustedProxies = $this->config->getSystemValue('trusted_proxies', []);
598
599
		if(is_array($trustedProxies) && in_array($remoteAddress, $trustedProxies)) {
600
			$forwardedForHeaders = $this->config->getSystemValue('forwarded_for_headers', [
601
				'HTTP_X_FORWARDED_FOR'
602
				// only have one default, so we cannot ship an insecure product out of the box
603
			]);
604
605
			foreach($forwardedForHeaders as $header) {
606
				if(isset($this->server[$header])) {
607
					foreach(explode(',', $this->server[$header]) as $IP) {
608
						$IP = trim($IP);
609
						if (filter_var($IP, FILTER_VALIDATE_IP) !== false) {
610
							return $IP;
611
						}
612
					}
613
				}
614
			}
615
		}
616
617
		return $remoteAddress;
618
	}
619
620
	/**
621
	 * Check overwrite condition
622
	 * @param string $type
623
	 * @return bool
624
	 */
625
	private function isOverwriteCondition($type = '') {
626
		$regex = '/' . $this->config->getSystemValue('overwritecondaddr', '')  . '/';
627
		$remoteAddr = isset($this->server['REMOTE_ADDR']) ? $this->server['REMOTE_ADDR'] : '';
628
		return $regex === '//' || preg_match($regex, $remoteAddr) === 1
629
		|| $type !== 'protocol';
630
	}
631
632
	/**
633
	 * Returns the server protocol. It respects one or more reverse proxies servers
634
	 * and load balancers
635
	 * @return string Server protocol (http or https)
636
	 */
637
	public function getServerProtocol() {
638
		if($this->config->getSystemValue('overwriteprotocol') !== ''
639
			&& $this->isOverwriteCondition('protocol')) {
640
			return $this->config->getSystemValue('overwriteprotocol');
641
		}
642
643
		if (isset($this->server['HTTP_X_FORWARDED_PROTO'])) {
644 View Code Duplication
			if (strpos($this->server['HTTP_X_FORWARDED_PROTO'], ',') !== false) {
645
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_PROTO']);
646
				$proto = strtolower(trim($parts[0]));
647
			} else {
648
				$proto = strtolower($this->server['HTTP_X_FORWARDED_PROTO']);
649
			}
650
651
			// Verify that the protocol is always HTTP or HTTPS
652
			// default to http if an invalid value is provided
653
			return $proto === 'https' ? 'https' : 'http';
654
		}
655
656
		if (isset($this->server['HTTPS'])
657
			&& $this->server['HTTPS'] !== null
658
			&& $this->server['HTTPS'] !== 'off'
659
			&& $this->server['HTTPS'] !== '') {
660
			return 'https';
661
		}
662
663
		return 'http';
664
	}
665
666
	/**
667
	 * Returns the used HTTP protocol.
668
	 *
669
	 * @return string HTTP protocol. HTTP/2, HTTP/1.1 or HTTP/1.0.
670
	 */
671 View Code Duplication
	public function getHttpProtocol() {
672
		$claimedProtocol = strtoupper($this->server['SERVER_PROTOCOL']);
673
674
		$validProtocols = [
675
			'HTTP/1.0',
676
			'HTTP/1.1',
677
			'HTTP/2',
678
		];
679
680
		if(in_array($claimedProtocol, $validProtocols, true)) {
681
			return $claimedProtocol;
682
		}
683
684
		return 'HTTP/1.1';
685
	}
686
687
	/**
688
	 * Returns the request uri, even if the website uses one or more
689
	 * reverse proxies
690
	 * @return string
691
	 */
692
	public function getRequestUri() {
693
		$uri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
694
		if($this->config->getSystemValue('overwritewebroot') !== '' && $this->isOverwriteCondition()) {
695
			$uri = $this->getScriptName() . substr($uri, strlen($this->server['SCRIPT_NAME']));
696
		}
697
		return $uri;
698
	}
699
700
	/**
701
	 * Get raw PathInfo from request (not urldecoded)
702
	 * @throws \Exception
703
	 * @return string Path info
704
	 */
705
	public function getRawPathInfo() {
706
		$requestUri = isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : '';
707
		// remove too many leading slashes - can be caused by reverse proxy configuration
708
		if (strpos($requestUri, '/') === 0) {
709
			$requestUri = '/' . ltrim($requestUri, '/');
710
		}
711
712
		$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 741 which is incompatible with the return type declared by the interface OCP\IRequest::getRawPathInfo of type string.
Loading history...
713
714
		// Remove the query string from REQUEST_URI
715
		if ($pos = strpos($requestUri, '?')) {
716
			$requestUri = substr($requestUri, 0, $pos);
717
		}
718
719
		$scriptName = $this->server['SCRIPT_NAME'];
720
		$pathInfo = $requestUri;
721
722
		// strip off the script name's dir and file name
723
		// FIXME: Sabre does not really belong here
724
		list($path, $name) = \Sabre\Uri\split($scriptName);
725
		if (!empty($path)) {
726
			if($path === $pathInfo || strpos($pathInfo, $path.'/') === 0) {
727
				$pathInfo = substr($pathInfo, strlen($path));
728
			} else {
729
				throw new \Exception("The requested uri($requestUri) cannot be processed by the script '$scriptName')");
730
			}
731
		}
732
		if (strpos($pathInfo, '/'.$name) === 0) {
733
			$pathInfo = substr($pathInfo, strlen($name) + 1);
734
		}
735
		if (strpos($pathInfo, $name) === 0) {
736
			$pathInfo = substr($pathInfo, strlen($name));
737
		}
738
		if($pathInfo === false || $pathInfo === '/'){
739
			return '';
740
		} else {
741
			return $pathInfo;
742
		}
743
	}
744
745
	/**
746
	 * Get PathInfo from request
747
	 * @throws \Exception
748
	 * @return string|false Path info or false when not found
749
	 */
750
	public function getPathInfo() {
751
		$pathInfo = $this->getRawPathInfo();
752
		// following is taken from \Sabre\HTTP\URLUtil::decodePathSegment
753
		$pathInfo = rawurldecode($pathInfo);
754
		$encoding = mb_detect_encoding($pathInfo, ['UTF-8', 'ISO-8859-1']);
755
756
		switch($encoding) {
757
			case 'ISO-8859-1' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
758
				$pathInfo = utf8_encode($pathInfo);
759
		}
760
		// end copy
761
762
		return $pathInfo;
763
	}
764
765
	/**
766
	 * Returns the script name, even if the website uses one or more
767
	 * reverse proxies
768
	 * @return string the script name
769
	 */
770
	public function getScriptName() {
771
		$name = $this->server['SCRIPT_NAME'];
772
		$overwriteWebRoot =  $this->config->getSystemValue('overwritewebroot');
773
		if ($overwriteWebRoot !== '' && $this->isOverwriteCondition()) {
774
			// FIXME: This code is untestable due to __DIR__, also that hardcoded path is really dangerous
775
			$serverRoot = str_replace('\\', '/', substr(__DIR__, 0, -strlen('lib/private/appframework/http/')));
776
			$suburi = str_replace('\\', '/', substr(realpath($this->server['SCRIPT_FILENAME']), strlen($serverRoot)));
777
			$name = '/' . ltrim($overwriteWebRoot . $suburi, '/');
778
		}
779
		return $name;
780
	}
781
782
	/**
783
	 * Checks whether the user agent matches a given regex
784
	 * @param array $agent array of agent names
785
	 * @return bool true if at least one of the given agent matches, false otherwise
786
	 */
787
	public function isUserAgent(array $agent) {
788
		if (!isset($this->server['HTTP_USER_AGENT'])) {
789
			return false;
790
		}
791
		foreach ($agent as $regex) {
792
			if (preg_match($regex, $this->server['HTTP_USER_AGENT'])) {
793
				return true;
794
			}
795
		}
796
		return false;
797
	}
798
799
	/**
800
	 * Returns the unverified server host from the headers without checking
801
	 * whether it is a trusted domain
802
	 * @return string Server host
803
	 */
804
	public function getInsecureServerHost() {
805
		$host = 'localhost';
806
		if (isset($this->server['HTTP_X_FORWARDED_HOST'])) {
807 View Code Duplication
			if (strpos($this->server['HTTP_X_FORWARDED_HOST'], ',') !== false) {
808
				$parts = explode(',', $this->server['HTTP_X_FORWARDED_HOST']);
809
				$host = trim(current($parts));
810
			} else {
811
				$host = $this->server['HTTP_X_FORWARDED_HOST'];
812
			}
813
		} else {
814
			if (isset($this->server['HTTP_HOST'])) {
815
				$host = $this->server['HTTP_HOST'];
816
			} else if (isset($this->server['SERVER_NAME'])) {
817
				$host = $this->server['SERVER_NAME'];
818
			}
819
		}
820
		return $host;
821
	}
822
823
824
	/**
825
	 * Returns the server host from the headers, or the first configured
826
	 * trusted domain if the host isn't in the trusted list
827
	 * @return string Server host
828
	 */
829
	public function getServerHost() {
830
		// overwritehost is always trusted
831
		$host = $this->getOverwriteHost();
832
		if ($host !== null) {
833
			return $host;
834
		}
835
836
		// get the host from the headers
837
		$host = $this->getInsecureServerHost();
838
839
		// Verify that the host is a trusted domain if the trusted domains
840
		// are defined
841
		// If no trusted domain is provided the first trusted domain is returned
842
		$trustedDomainHelper = new TrustedDomainHelper($this->config);
843
		if ($trustedDomainHelper->isTrustedDomain($host)) {
844
			return $host;
845
		} else {
846
			$trustedList = $this->config->getSystemValue('trusted_domains', []);
847
			if(!empty($trustedList)) {
848
				return $trustedList[0];
849
			} else {
850
				return '';
851
			}
852
		}
853
	}
854
855
	/**
856
	 * Returns the overwritehost setting from the config if set and
857
	 * if the overwrite condition is met
858
	 * @return string|null overwritehost value or null if not defined or the defined condition
859
	 * isn't met
860
	 */
861
	private function getOverwriteHost() {
862
		if($this->config->getSystemValue('overwritehost') !== '' && $this->isOverwriteCondition()) {
863
			return $this->config->getSystemValue('overwritehost');
864
		}
865
		return null;
866
	}
867
868
}
869