Textpattern PHP Cross Reference Content Management Systems

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

Description: Inspects the current HTTP request.

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

title

Description

title

Description

title

Description

title

title

Body