Textpattern | PHP Cross Reference | Content Management Systems |
Description: Inspects the current HTTP request.
1 <?php 2 3 /* 4 * Textpattern Content Management System 5 * https://textpattern.com/ 6 * 7 * Copyright (C) 2020 The Textpattern Development Team 8 * 9 * This file is part of Textpattern. 10 * 11 * Textpattern is free software; you can redistribute it and/or 12 * modify it under the terms of the GNU General Public License 13 * as published by the Free Software Foundation, version 2. 14 * 15 * Textpattern is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 * GNU General Public License for more details. 19 * 20 * You should have received a copy of the GNU General Public License 21 * along with Textpattern. If not, see <https://www.gnu.org/licenses/>. 22 */ 23 24 /** 25 * Inspects the current HTTP request. 26 * 27 * Handles content negotiations and extracting data from headers safely on 28 * different web servers. 29 * 30 * @since 4.6.0 31 * @package HTTP 32 */ 33 34 namespace Textpattern\Http; 35 36 class Request 37 { 38 /** 39 * Protocol-port map. 40 * 41 * @var array 42 */ 43 44 protected $protocolMap = array( 45 'http' => 80, 46 'https' => 443, 47 ); 48 49 /** 50 * Stores headers. 51 * 52 * @var array 53 */ 54 55 protected $headers; 56 57 /** 58 * Stores referer. 59 * 60 * @var string 61 */ 62 63 protected $referer; 64 65 /** 66 * Content types accepted by the client. 67 * 68 * @var array 69 */ 70 71 protected $acceptedTypes; 72 73 /** 74 * Formats mapping. 75 * 76 * @var array 77 */ 78 79 protected $acceptsFormats = array( 80 'html' => array('text/html', 'application/xhtml+xml', '*/*'), 81 'txt' => array('text/plain', '*/*'), 82 'js' => array('application/javascript', 'application/x-javascript', 'text/javascript', 'application/ecmascript', 'application/x-ecmascript', '*/*'), 83 'css' => array('text/css', '*/*'), 84 'json' => array('application/json', 'application/x-json', '*/*'), 85 'xml' => array('text/xml', 'application/xml', 'application/x-xml', '*/*'), 86 'rdf' => array('application/rdf+xml', '*/*'), 87 'atom' => array('application/atom+xml', '*/*'), 88 'rss' => array('application/rss+xml', '*/*'), 89 ); 90 91 /** 92 * Raw request data. 93 * 94 * Wraps around PHP's $_SERVER variable. 95 * 96 * @var \Textpattern\Server\Config 97 */ 98 99 protected $request; 100 101 /** 102 * Resolved hostnames. 103 * 104 * @var array 105 */ 106 107 protected $hostNames = array(); 108 109 /** 110 * Constructor. 111 * 112 * <code> 113 * echo Txp::get('\Textpattern\Http\Request', new Abc_Custom_Request_Data)->getHostName(); 114 * </code> 115 * 116 * @param \Textpattern\Server\Config|null $request The raw request data, defaults to the current request body 117 */ 118 119 public function __construct(\Textpattern\Server\Config $request = null) 120 { 121 if ($request === null) { 122 $this->request = \Txp::get('\Textpattern\Server\Config'); 123 } else { 124 $this->request = $request; 125 } 126 } 127 128 /** 129 * Checks whether the client accepts a certain response format. 130 * 131 * By default discards formats with quality factors below an arbitrary 132 * threshold as jQuery adds a wildcard content-type with quality of '0.01' 133 * to the 'Accept' header for XHR requests. 134 * 135 * Supplied format of 'html', 'txt', 'js', 'css', 'json', 'xml', 'rdf', 136 * 'atom' or 'rss' is autocompleted and matched against multiple valid MIMEs. 137 * 138 * Both of the following will return MIME for JSON if 'json' format is 139 * supported: 140 * 141 * <code> 142 * echo Txp::get('\Textpattern\Http\Request')->getAcceptedType('json'); 143 * echo Txp::get('\Textpattern\Http\Request')->getAcceptedType('application/json'); 144 * </code> 145 * 146 * The method can also be used to check an array of types: 147 * 148 * <code> 149 * echo Txp::get('\Textpattern\Http\Request')->getAcceptedType(array('application/xml', 'application/x-xml')); 150 * </code> 151 * 152 * Stops on first accepted format. 153 * 154 * @param string|array $formats Format to check 155 * @param float $threshold Quality threshold 156 * @return string|bool Supported type, or FALSE if not 157 */ 158 159 public function getAcceptedType($formats, $threshold = 0.1) 160 { 161 if ($this->acceptedTypes === null) { 162 $this->acceptedTypes = $this->getAcceptsMap($this->request->getVariable('HTTP_ACCEPT')); 163 } 164 165 foreach ((array) $formats as $format) { 166 if (isset($this->acceptsFormats[$format])) { 167 $format = $this->acceptsFormats[$format]; 168 } 169 170 foreach ((array) $format as $type) { 171 if (isset($this->acceptedTypes[$type]) && $this->acceptedTypes[$type]['q'] >= $threshold) { 172 return $type; 173 } 174 } 175 } 176 177 return false; 178 } 179 180 /** 181 * Gets accepted language. 182 * 183 * If $languages is NULL, returns client's favoured language. If 184 * string, checks whether the language is supported and 185 * if an array, returns the language that the client favours the most. 186 * 187 * <code> 188 * echo Txp::get('\Textpattern\Http\Request')->getAcceptedLanguage('fi-FI'); 189 * </code> 190 * 191 * The above will return 'fi-FI' as long as the Accept-Language header 192 * contains an identifier that matches Finnish, such as 'fi-fi', 'fi-Fi' 193 * or 'fi'. 194 * 195 * @param string|array $languages Languages to check 196 * @param float $threshold Quality threshold 197 * @return string|bool Accepted language, or FALSE 198 */ 199 200 public function getAcceptedLanguage($languages = null, $threshold = 0.1) 201 { 202 $accepts = $this->getAcceptsMap($this->request->getVariable('HTTP_ACCEPT_LANGUAGE')); 203 204 if ($languages === null) { 205 $accepts = array_keys($accepts); 206 207 return array_shift($accepts); 208 } 209 210 $top = 0; 211 $acceptedLanguage = false; 212 213 foreach ((array) $languages as $language) { 214 $search = array($language); 215 216 if ($identifiers = \Txp::get('\Textpattern\L10n\Locale')->getLocaleIdentifiers($language)) { 217 $search = array_map('strtolower', array_merge($search, $identifiers)); 218 } 219 220 foreach ($accepts as $accept => $params) { 221 if (in_array(strtolower($accept), $search, true) && $params['q'] >= $threshold && $params['q'] >= $top) { 222 $top = $quality; // FIXME: $quality is made out of thin air. 223 $acceptedLanguage = $language; 224 } 225 } 226 } 227 228 return $acceptedLanguage; 229 } 230 231 /** 232 * Gets accepted encoding. 233 * 234 * Negotiates a common encoding between the client and the server. 235 * 236 * <code> 237 * if (Txp::get('\Textpattern\Http\Request')->getAcceptedEncoding('gzip')) { 238 * echo 'Client accepts gzip.'; 239 * } 240 * </code> 241 * 242 * @param string|array $encodings Encoding 243 * @param float $threshold Quality threshold 244 * @return string|bool Encoding method, or FALSE 245 */ 246 247 public function getAcceptedEncoding($encodings = null, $threshold = 0.1) 248 { 249 $accepts = $this->getAcceptsMap($this->request->getVariable('HTTP_ACCEPT_ENCODING')); 250 251 if ($encodings === null) { 252 $accepts = array_keys($accepts); 253 254 return array_shift($accepts); 255 } 256 257 foreach ((array) $encodings as $encoding) { 258 if (isset($accepts[$encoding]) && $accepts[$encoding]['q'] >= $threshold) { 259 return $encoding; 260 } 261 } 262 263 return false; 264 } 265 266 /** 267 * Gets an absolute URL pointing to the requested document. 268 * 269 * <code> 270 * echo Txp::get('\Textpattern\Http\Request')->getUrl(); 271 * </code> 272 * 273 * The above will return URL pointing to the requested 274 * page, e.g. http://example.test/path/to/subpage. 275 * 276 * @return string The URL 277 */ 278 279 public function getUrl() 280 { 281 $port = ''; 282 283 if (($portNumber = $this->getPort()) !== false && strpos($this->getHost(), ':') === false) { 284 $port = ':'.$portNumber; 285 } 286 287 return $this->getProtocol().'://'.$this->getHost().$port.$this->getUri(); 288 } 289 290 /** 291 * Gets the server hostname. 292 * 293 * <code> 294 * echo Txp::get('\Textpattern\Http\Request')->getHost(); 295 * </code> 296 * 297 * Returns 'example.com' if requesting 298 * http://example.test/path/to/subpage. 299 * 300 * @return string The host 301 */ 302 303 public function getHost() 304 { 305 return (string) $this->request->getVariable('HTTP_HOST'); 306 } 307 308 /** 309 * Gets the port, if not default. 310 * 311 * This method returns FALSE, if the port is the request protocol's default. 312 * Neither '80' or 443 for HTTPS are returned. 313 * 314 * <code> 315 * echo Txp::get('\Textpattern\Http\Request')->getPort(); 316 * </code> 317 * 318 * Returns '8080' if requesting http://example.test:8080/path/to/subpage. 319 * 320 * @return int|bool Port number, or FALSE 321 */ 322 323 public function getPort() 324 { 325 $port = (int) $this->request->getVariable('SERVER_PORT'); 326 $protocol = $this->getProtocol(); 327 328 if ($port && (!isset($this->protocolMap[$protocol]) || $port !== $this->protocolMap[$protocol])) { 329 return $port; 330 } 331 332 return false; 333 } 334 335 /** 336 * Gets the client IP address. 337 * 338 * This method supports proxies and uses 'X_FORWARDED_FOR' HTTP header if 339 * deemed necessary. 340 * 341 * <code> 342 * echo Txp::get('\Textpattern\Http\Request')->getIp(); 343 * </code> 344 * 345 * Returns the IP address the request came from, e.g. '0.0.0.0'. 346 * Can be either IPv6 or IPv4 depending on the request. 347 * 348 * @return string The IP address 349 */ 350 351 public function getIp() 352 { 353 $ip = $this->request->getVariable('REMOTE_ADDR'); 354 $proxy = $this->getHeader('X-Forwarded-For'); 355 356 if ($proxy && ($ip === '127.0.0.1' || $ip === '::1' || $ip === '::ffff:127.0.0.1' || $ip === $this->request->getVariable('SERVER_ADDR'))) { 357 $ips = explode(',', $proxy); 358 $ip = trim($ips[0]); 359 } 360 361 return $ip; 362 } 363 364 /** 365 * Gets client hostname. 366 * 367 * This method resolves client's hostname. 368 * 369 * <code> 370 * echo Txp::get('\Textpattern\Http\Request')->getRemoteHostname(); 371 * </code> 372 * 373 * @return string|bool The hostname, or FALSE on failure 374 */ 375 376 public function getRemoteHostname() 377 { 378 $ip = $this->getIp(); 379 380 if (isset($this->hostNames[$ip])) { 381 return $this->hostNames[$ip]; 382 } 383 384 if ($host = @gethostbyaddr($ip)) { 385 if ($host !== $ip && @gethostbyname($host) !== $ip) { 386 $host = $ip; 387 } 388 389 return $this->hostNames[$ip] = $host; 390 } 391 392 return false; 393 } 394 395 /** 396 * Gets the request protocol. 397 * 398 * <code> 399 * echo Txp::get('\Textpattern\Http\Request')->getProtocol(); 400 * </code> 401 * 402 * Returns 'https' if requesting https://example.test:8080/path/to/subpage. 403 * 404 * @return string Either 'http' or 'https' 405 */ 406 407 public function getProtocol() 408 { 409 if (($https = $this->request->getVariable('HTTPS')) && $https !== 'off') { 410 return 'https'; 411 } 412 413 if (($https = $this->getHeader('Front-End-Https')) && strtolower($https) === 'on') { 414 return 'https'; 415 } 416 417 if (($https = $this->getHeader('X-Forwarded-Proto')) && strtolower($https) === 'https') { 418 return 'https'; 419 } 420 421 return 'http'; 422 } 423 424 /** 425 * Gets referer. 426 * 427 * Returns referer header if it does not originate from the current 428 * hostname or come from a HTTPS page to a HTTP page. 429 * 430 * <code> 431 * echo Txp::get('\Textpattern\Http\Request')->getReferer(); 432 * </code> 433 * 434 * Returns full URL such as 'http://example.com/referring/page.php?id=12'. 435 * 436 * @return string|bool Referer, or FALSE if not available 437 */ 438 439 public function getReferer() 440 { 441 if ($this->referer === null) { 442 $protocol = $this->referer = false; 443 444 if ($referer = $this->request->getVariable('HTTP_REFERER')) { 445 if (strpos($referer, '://')) { 446 $referer = explode('://', $referer); 447 $protocol = array_shift($referer); 448 $referer = join('://', $referer); 449 } 450 451 if (!$protocol || ($protocol === 'https' && $this->getProtocol() !== 'https://')) { 452 return false; 453 } 454 455 if (preg_match('/^[^\.]*\.?'.preg_quote(preg_replace('/^www\./', '', $this->getHost()), '/').'/i', $referer)) { 456 return false; 457 } 458 459 $this->referer = $protocol.'://'.$referer; 460 } 461 } 462 463 return $this->referer; 464 } 465 466 /** 467 * Gets requested URI. 468 * 469 * <code> 470 * echo Txp::get('\Textpattern\Http\Request')->getUri(); 471 * </code> 472 * 473 * Returns '/some/requested/page?and=query' if requesting 474 * http://example.com/some/requested/page?and=query. 475 * 476 * @return string The URI 477 */ 478 479 public function getUri() 480 { 481 return (string) $this->request->getVariable('REQUEST_URI'); 482 } 483 484 /** 485 * Gets an array map of raw request headers. 486 * 487 * This method is web server agnostic. 488 * 489 * The following: 490 * 491 * <code> 492 * print_r(Txp::get('\Textpattern\Http\Request')->getHeaders()); 493 * </code> 494 * 495 * Returns: 496 * 497 * <code> 498 * Array 499 * ( 500 * [Host] => example.test 501 * [Connection] => keep-alive 502 * [Cache-Control] => max-age=0 503 * [User-Agent] => User-Agent 504 * [Referer] => http://example.test/textpattern/index.php 505 * [Accept-Encoding] => gzip,deflate,sdch 506 * [Accept-Language] => en-US,en;q=0.8,fi;q=0.6 507 * [Cookie] => toggle_show_spam=1 508 * ) 509 * </code> 510 * 511 * @return array An array of HTTP request headers 512 */ 513 514 public function getHeaders() 515 { 516 if ($this->headers !== null) { 517 return $this->headers; 518 } 519 520 $this->headers = array(); 521 522 foreach ($_SERVER as $name => $value) { 523 if (strpos($name, 'HTTP_') === 0 && is_scalar($value)) { 524 $parts = explode('_', $name); 525 array_shift($parts); 526 527 foreach ($parts as &$part) { 528 $part = ucfirst(strtolower($part)); 529 } 530 531 $this->headers[join('-', $parts)] = (string) $value; 532 } 533 } 534 535 return $this->headers; 536 } 537 538 /** 539 * Gets a raw HTTP request header value. 540 * 541 * <code> 542 * echo Txp::get('\Textpattern\Http\Request')->getHeader('User-Agent'); 543 * </code> 544 * 545 * Will return the client's User-Agent header, if it has any. If the client 546 * didn't send User-Agent, the method returns FALSE. 547 * 548 * @param string $name The header name 549 * @return string|bool The header value, or FALSE on failure 550 */ 551 552 public function getHeader($name) 553 { 554 if ($headers = $this->getHeaders()) { 555 if (isset($headers[$name])) { 556 return $headers[$name]; 557 } 558 } 559 560 return false; 561 } 562 563 /** 564 * Gets an array of HTTP cookies. 565 * 566 * <code> 567 * print_r(Txp::get('\Textpattern\Http\Request')->getHeaders()); 568 * </code> 569 * 570 * Returns: 571 * 572 * <code> 573 * Array( 574 * [foobar] => value 575 * ) 576 * </code> 577 * 578 * Returned cookie values are processed properly for you, and will not 579 * contain runtime quoting slashes or be URL encoded. Just pick and choose. 580 * 581 * @return array An array of cookies 582 */ 583 584 public function getCookies() 585 { 586 $out = array(); 587 588 if ($_COOKIE) { 589 foreach ($_COOKIE as $name => $value) { 590 $out[$name] = $this->getCookie($name); 591 } 592 } 593 594 return $out; 595 } 596 597 /** 598 * Gets a HTTP cookie. 599 * 600 * <code> 601 * echo Txp::get('\Textpattern\Http\Request')->getCookie('foobar'); 602 * </code> 603 * 604 * @param string $name The cookie name 605 * @return string The value 606 */ 607 608 public function getCookie($name) 609 { 610 if (isset($_COOKIE[$name])) { 611 return $_COOKIE[$name]; 612 } 613 614 return ''; 615 } 616 617 /** 618 * Gets a query string. 619 * 620 * <code> 621 * print_r(Txp::get('\Textpattern\Http\Request')->getQuery()); 622 * </code> 623 * 624 * If requesting "?event=article&step=save", the above returns: 625 * 626 * <code> 627 * Array 628 * ( 629 * [event] => article 630 * [step] => save 631 * ) 632 * </code> 633 * 634 * @return array An array of parameters 635 */ 636 637 public function getQuery() 638 { 639 $out = array(); 640 641 if ($_GET) { 642 foreach ($_GET as $name => $value) { 643 $out[$name] = $this->getParam($name); 644 } 645 } 646 647 if ($_POST) { 648 foreach ($_POST as $name => $value) { 649 $out[$name] = $this->getPost($name); 650 } 651 } 652 653 return $out; 654 } 655 656 /** 657 * Gets a HTTP query string parameter. 658 * 659 * @param $name The parameter name 660 * @return mixed 661 */ 662 663 public function getParam($name) 664 { 665 if (isset($_GET[$name])) { 666 $out = $_GET[$name]; 667 $out = doArray($out, 'deCRLF'); 668 669 return doArray($out, 'deNull'); 670 } 671 672 return $this->getPost($name); 673 } 674 675 /** 676 * Gets a HTTP post parameter. 677 * 678 * @param string $name The parameter name 679 * @return mixed 680 */ 681 682 public function getPost($name) 683 { 684 $out = ''; 685 686 if (isset($_POST[$name])) { 687 $out = $_POST[$name]; 688 } 689 690 return doArray($out, 'deNull'); 691 } 692 693 /** 694 * Builds a content-negotiation accepts map from the given value. 695 * 696 * Keys are the accepted type and the value are the params. If client 697 * doesn't specify quality, defaults to 1.0. Values are sorted by the 698 * quality, from the highest to the lowest. 699 * 700 * This method can be used to parse Accept, Accept-Charset, Accept-Encoding 701 * and Accept-Language header values. 702 * 703 * <code> 704 * print_r(Txp::get('\Textpattern\Http\Request')->getAcceptsMap('en-us;q=1.0,en;q=0.9')); 705 * </code> 706 * 707 * Returns: 708 * 709 * <code> 710 * Array 711 * ( 712 * [en-us] => Array 713 * ( 714 * [q] => 1.0 715 * ) 716 * [en] => Array 717 * ( 718 * [q] => 0.9 719 * ) 720 * ) 721 * </code> 722 * 723 * @param string $header The header string 724 * @return array Accepts map 725 */ 726 727 public function getAcceptsMap($header) 728 { 729 $types = explode(',', $header); 730 $accepts = array(); 731 $sort = array(); 732 733 foreach ($types as $type) { 734 if ($type = trim($type)) { 735 if ($parts = explode(';', $type)) { 736 $type = array_shift($parts); 737 738 $params = array( 739 'q' => 1.0, 740 ); 741 742 foreach ($parts as $value) { 743 if (strpos($value, '=') === false) { 744 $params[$value] = true; 745 } else { 746 $value = explode('=', $value); 747 $params[array_shift($value)] = join('=', $value); 748 } 749 } 750 751 $params['q'] = floatval($params['q']); 752 $accepts[$type] = $params; 753 $sort[$type] = $params['q']; 754 } 755 } 756 } 757 758 array_multisort($sort, SORT_DESC, $accepts); 759 760 return $accepts; 761 } 762 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
title