1<?php 2 3namespace Elgg\Http; 4 5use Elgg\BadRequestException; 6use Elgg\Context; 7use Elgg\HttpException; 8use Elgg\Router\Route; 9use Symfony\Component\HttpFoundation\File\UploadedFile; 10use Symfony\Component\HttpFoundation\Request as SymfonyRequest; 11use Elgg\Config; 12 13/** 14 * Elgg HTTP request. 15 * 16 * @internal 17 */ 18class Request extends SymfonyRequest { 19 20 const REWRITE_TEST_TOKEN = '__testing_rewrite'; 21 const REWRITE_TEST_OUTPUT = 'success'; 22 23 /** 24 * @var Context 25 */ 26 protected $context_stack; 27 28 /** 29 * @var Route 30 */ 31 protected $route; 32 33 /** 34 * @ var array 35 */ 36 protected $request_overrides; 37 38 /** 39 * {@inheritdoc} 40 */ 41 public function __construct( 42 array $query = [], 43 array $request = [], 44 array $attributes = [], 45 array $cookies = [], 46 array $files = [], 47 array $server = [], 48 $content = null 49 ) { 50 parent::__construct($query, $request, $attributes, $cookies, $files, $server, $content); 51 52 $this->initializeContext(); 53 54 $this->request_overrides = []; 55 } 56 57 /** 58 * Configure trusted proxy servers to allow access to more client information 59 * 60 * @return void 61 */ 62 public function initializeTrustedProxyConfiguration(Config $config) { 63 $trusted_proxies = $config->http_request_trusted_proxy_ips; 64 if (empty($trusted_proxies)) { 65 return; 66 } 67 68 $allowed_headers = $config->http_request_trusted_proxy_headers; 69 if (empty($allowed_headers)) { 70 $allowed_headers = self::HEADER_X_FORWARDED_ALL; 71 } 72 73 $this->setTrustedProxies($trusted_proxies, $allowed_headers); 74 } 75 76 /** 77 * Initialize context stack 78 * @return static 79 */ 80 public function initializeContext() { 81 $context = new Context($this); 82 $this->context_stack = $context; 83 84 return $this; 85 } 86 87 /** 88 * Returns context stack 89 * @return Context 90 */ 91 public function getContextStack() { 92 return $this->context_stack; 93 } 94 95 /** 96 * Sets the route matched for this request by the router 97 * 98 * @param Route $route Route 99 * 100 * @return static 101 */ 102 public function setRoute(Route $route) { 103 $this->route = $route; 104 foreach ($route->getMatchedParameters() as $key => $value) { 105 $this->setParam($key, $value); 106 } 107 108 return $this; 109 } 110 111 /** 112 * Returns the route matched for this request by the router 113 * @return Route|null 114 */ 115 public function getRoute() { 116 return $this->route; 117 } 118 119 /** 120 * Sets an input value that may later be retrieved by get_input 121 * 122 * Note: this function does not handle nested arrays (ex: form input of param[m][n]) 123 * 124 * @param string $key The name of the variable 125 * @param string|string[] $value The value of the variable 126 * @param bool $override_request The variable should override request values (default: false) 127 * 128 * @return static 129 */ 130 public function setParam($key, $value, $override_request = false) { 131 if ((bool) $override_request) { 132 $this->request_overrides[$key] = $value; 133 } else { 134 $this->request->set($key, $value); 135 } 136 137 return $this; 138 } 139 140 /** 141 * Get some input from variables passed submitted through GET or POST. 142 * 143 * If using any data obtained from get_input() in a web page, please be aware that 144 * it is a possible vector for a reflected XSS attack. If you are expecting an 145 * integer, cast it to an int. If it is a string, escape quotes. 146 * 147 * Note: this function does not handle nested arrays (ex: form input of param[m][n]) 148 * because of the filtering done in htmlawed from the filter_tags call. 149 * @todo Is this ^ still true? 150 * 151 * @param string $key The variable name we want. 152 * @param mixed $default A default value for the variable if it is not found. 153 * @param bool $filter_result If true, then the result is filtered for bad tags. 154 * 155 * @return mixed 156 */ 157 public function getParam($key, $default = null, $filter_result = true) { 158 $result = $default; 159 160 $values = $this->getParams($filter_result); 161 162 $value = elgg_extract($key, $values, $default); 163 if ($value !== null) { 164 $result = $value; 165 } 166 167 return $result; 168 } 169 170 /** 171 * Returns all values parsed from the request 172 * 173 * @param bool $filter_result Sanitize input values 174 * 175 * @return array 176 */ 177 public function getParams($filter_result = true) { 178 $request_overrides = $this->request_overrides; 179 $query = $this->query->all(); 180 $attributes = $this->attributes->all(); 181 $post = $this->request->all(); 182 183 $result = array_merge($post, $attributes, $query, $request_overrides); 184 185 if ($filter_result) { 186 $this->getContextStack()->push('input'); 187 188 $result = filter_tags($result); 189 190 $this->getContextStack()->pop(); 191 } 192 193 return $result; 194 } 195 196 /** 197 * Returns current page URL 198 * 199 * @return string 200 */ 201 public function getCurrentURL() { 202 $url = parse_url(elgg_get_site_url()); 203 204 $page = $url['scheme'] . "://" . $url['host']; 205 206 if (isset($url['port']) && $url['port']) { 207 $page .= ":" . $url['port']; 208 } 209 210 $page = trim($page, "/"); 211 212 $page .= $this->getRequestUri(); 213 214 return $page; 215 } 216 217 /** 218 * Get the Elgg URL segments 219 * 220 * @param bool $raw If true, the segments will not be HTML escaped 221 * 222 * @return string[] 223 */ 224 public function getUrlSegments($raw = false) { 225 $path = trim($this->getElggPath(), '/'); 226 if (!$raw) { 227 $path = htmlspecialchars($path, ENT_QUOTES, 'UTF-8'); 228 } 229 if (!$path) { 230 return []; 231 } 232 233 return explode('/', $path); 234 } 235 236 /** 237 * Get a cloned request with new Elgg URL segments 238 * 239 * @param string[] $segments URL segments 240 * 241 * @return Request 242 */ 243 public function setUrlSegments(array $segments) { 244 $base_path = trim($this->getBasePath(), '/'); 245 $server = $this->server->all(); 246 $server['REQUEST_URI'] = "$base_path/" . implode('/', $segments); 247 248 return $this->duplicate(null, null, null, null, null, $server); 249 } 250 251 /** 252 * Get first Elgg URL segment 253 * 254 * @see \Elgg\Http\Request::getUrlSegments() 255 * 256 * @return string 257 */ 258 public function getFirstUrlSegment() { 259 $segments = $this->getUrlSegments(); 260 if (!empty($segments)) { 261 return array_shift($segments); 262 } 263 264 return ''; 265 } 266 267 /** 268 * Get the Request URI minus querystring 269 * 270 * @return string 271 */ 272 public function getElggPath() { 273 if (php_sapi_name() === 'cli-server') { 274 $path = $this->getRequestUri(); 275 } else { 276 $path = $this->getPathInfo(); 277 } 278 279 return preg_replace('~(\?.*)$~', '', $path); 280 } 281 282 /** 283 * {@inheritdoc} 284 */ 285 public function getClientIp() { 286 $ip = parent::getClientIp(); 287 288 if ($ip == $this->server->get('REMOTE_ADDR')) { 289 // try one more 290 $ip_addresses = $this->server->get('HTTP_X_REAL_IP'); 291 if ($ip_addresses) { 292 $ip_addresses = explode(',', $ip_addresses); 293 294 return array_pop($ip_addresses); 295 } 296 } 297 298 return $ip; 299 } 300 301 /** 302 * {@inheritdoc} 303 */ 304 public function isXmlHttpRequest() { 305 return (strtolower($this->headers->get('X-Requested-With')) === 'xmlhttprequest' 306 || $this->query->get('X-Requested-With') === 'XMLHttpRequest' 307 || $this->request->get('X-Requested-With') === 'XMLHttpRequest'); 308 // GET/POST check is necessary for jQuery.form and other iframe-based "ajax". #8735 309 } 310 311 /** 312 * Sniff the Elgg site URL with trailing slash 313 * 314 * @return string 315 */ 316 public function sniffElggUrl() { 317 $base_url = $this->getBaseUrl(); 318 319 // baseURL may end with the PHP script 320 if ('.php' === substr($base_url, -4)) { 321 $base_url = dirname($base_url); 322 } 323 324 $base_url = str_replace('\\', '/', $base_url); 325 326 return rtrim($this->getSchemeAndHttpHost() . $base_url, '/') . '/'; 327 } 328 329 /** 330 * Is the request for checking URL rewriting? 331 * 332 * @return bool 333 */ 334 public function isRewriteCheck() { 335 if ($this->getPathInfo() !== ('/' . self::REWRITE_TEST_TOKEN)) { 336 return false; 337 } 338 339 if (!$this->get(self::REWRITE_TEST_TOKEN)) { 340 return false; 341 } 342 343 return true; 344 } 345 346 /** 347 * Is PHP running the CLI server front controller 348 * 349 * @return bool 350 */ 351 public function isCliServer() { 352 return php_sapi_name() === 'cli-server'; 353 } 354 355 /** 356 * Is the request pointing to a file that the CLI server can handle? 357 * 358 * @param string $root Root directory 359 * 360 * @return bool 361 */ 362 public function isCliServable($root) { 363 $file = rtrim($root, '\\/') . $this->getElggPath(); 364 if (!is_file($file)) { 365 return false; 366 } 367 368 // http://php.net/manual/en/features.commandline.webserver.php 369 $extensions = ".3gp, .apk, .avi, .bmp, .css, .csv, .doc, .docx, .flac, .gif, .gz, .gzip, .htm, .html, .ics, .jpe, .jpeg, .jpg, .js, .kml, .kmz, .m4a, .mov, .mp3, .mp4, .mpeg, .mpg, .odp, .ods, .odt, .oga, .ogg, .ogv, .pdf, .pdf, .png, .pps, .pptx, .qt, .svg, .swf, .tar, .text, .tif, .txt, .wav, .webm, .wmv, .xls, .xlsx, .xml, .xsl, .xsd, and .zip"; 370 371 // The CLI server routes ALL requests here (even existing files), so we have to check for these. 372 $ext = pathinfo($file, PATHINFO_EXTENSION); 373 if (!$ext) { 374 return false; 375 } 376 377 $ext = preg_quote($ext, '~'); 378 379 return (bool) preg_match("~\\.{$ext}[,$]~", $extensions); 380 } 381 382 /** 383 * Returns an array of uploaded file objects regardless of upload status/errors 384 * 385 * @param string $input_name Form input name 386 * 387 * @return UploadedFile[] 388 */ 389 public function getFiles($input_name) { 390 $files = $this->files->get($input_name); 391 if (empty($files)) { 392 return []; 393 } 394 395 if (!is_array($files)) { 396 $files = [$files]; 397 } 398 399 return $files; 400 } 401 402 /** 403 * Returns the first file found based on the input name 404 * 405 * @param string $input_name Form input name 406 * @param bool $check_for_validity If there is an uploaded file, is it required to be valid 407 * 408 * @return UploadedFile|false 409 */ 410 public function getFile($input_name, $check_for_validity = true) { 411 $files = $this->getFiles($input_name); 412 if (empty($files)) { 413 return false; 414 } 415 416 $file = $files[0]; 417 if (empty($file)) { 418 return false; 419 } 420 421 if ($check_for_validity && !$file->isValid()) { 422 return false; 423 } 424 425 return $file; 426 } 427 428 /** 429 * Validate the request 430 * 431 * @return void 432 * @throws HttpException 433 */ 434 public function validate() { 435 436 $reported_bytes = $this->server->get('CONTENT_LENGTH'); 437 438 // Requests with multipart content type 439 $post_data_count = count($this->request->all()); 440 441 // Requests with other content types 442 $content = $this->getContent(); 443 $post_body_length = is_string($content) ? elgg_strlen($content) : 0; 444 445 $file_count = count($this->files->all()); 446 447 $is_valid = function() use ($reported_bytes, $post_data_count, $post_body_length, $file_count) { 448 if (empty($reported_bytes)) { 449 // Content length is set for POST requests only 450 return true; 451 } 452 453 if (empty($post_data_count) && empty($post_body_length) && empty($file_count)) { 454 // The size of $_POST or uploaded files has exceed the size limit 455 // and the request body/query has been truncated 456 // thus the request reported bytes is set, but no postdata is found 457 return false; 458 } 459 460 return true; 461 }; 462 463 if (!$is_valid()) { 464 $error_msg = elgg_trigger_plugin_hook('action_gatekeeper:upload_exceeded_msg', 'all', [ 465 'post_size' => $reported_bytes, 466 'visible_errors' => true, 467 ], elgg_echo('actiongatekeeper:uploadexceeded')); 468 469 throw new BadRequestException($error_msg); 470 } 471 } 472} 473