Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/vendors/Textpattern/Http/Request.php - 762 lines - 20089 bytes - Summary - Text - Print

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&amp;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

title

Description

title

Description

title

title

Body