[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/aphront/ -> AphrontRequest.php (source)

   1  <?php
   2  
   3  /**
   4   * @task data   Accessing Request Data
   5   * @task cookie Managing Cookies
   6   *
   7   */
   8  final class AphrontRequest {
   9  
  10    // NOTE: These magic request-type parameters are automatically included in
  11    // certain requests (e.g., by phabricator_form(), JX.Request,
  12    // JX.Workflow, and ConduitClient) and help us figure out what sort of
  13    // response the client expects.
  14  
  15    const TYPE_AJAX = '__ajax__';
  16    const TYPE_FORM = '__form__';
  17    const TYPE_CONDUIT = '__conduit__';
  18    const TYPE_WORKFLOW = '__wflow__';
  19    const TYPE_CONTINUE = '__continue__';
  20    const TYPE_PREVIEW = '__preview__';
  21    const TYPE_HISEC = '__hisec__';
  22  
  23    private $host;
  24    private $path;
  25    private $requestData;
  26    private $user;
  27    private $applicationConfiguration;
  28    private $uriData;
  29  
  30    final public function __construct($host, $path) {
  31      $this->host = $host;
  32      $this->path = $path;
  33    }
  34  
  35    final public function setURIMap(array $uri_data) {
  36      $this->uriData = $uri_data;
  37      return $this;
  38    }
  39  
  40    final public function getURIMap() {
  41      return $this->uriData;
  42    }
  43  
  44    final public function getURIData($key, $default = null) {
  45      return idx($this->uriData, $key, $default);
  46    }
  47  
  48    final public function setApplicationConfiguration(
  49      $application_configuration) {
  50      $this->applicationConfiguration = $application_configuration;
  51      return $this;
  52    }
  53  
  54    final public function getApplicationConfiguration() {
  55      return $this->applicationConfiguration;
  56    }
  57  
  58    final public function setPath($path) {
  59      $this->path = $path;
  60      return $this;
  61    }
  62  
  63    final public function getPath() {
  64      return $this->path;
  65    }
  66  
  67    final public function getHost() {
  68      // The "Host" header may include a port number, or may be a malicious
  69      // header in the form "realdomain.com:[email protected]". Invoke the full
  70      // parser to extract the real domain correctly. See here for coverage of
  71      // a similar issue in Django:
  72      //
  73      //  https://www.djangoproject.com/weblog/2012/oct/17/security/
  74      $uri = new PhutilURI('http://'.$this->host);
  75      return $uri->getDomain();
  76    }
  77  
  78  
  79  /* -(  Accessing Request Data  )--------------------------------------------- */
  80  
  81  
  82    /**
  83     * @task data
  84     */
  85    final public function setRequestData(array $request_data) {
  86      $this->requestData = $request_data;
  87      return $this;
  88    }
  89  
  90  
  91    /**
  92     * @task data
  93     */
  94    final public function getRequestData() {
  95      return $this->requestData;
  96    }
  97  
  98  
  99    /**
 100     * @task data
 101     */
 102    final public function getInt($name, $default = null) {
 103      if (isset($this->requestData[$name])) {
 104        return (int)$this->requestData[$name];
 105      } else {
 106        return $default;
 107      }
 108    }
 109  
 110  
 111    /**
 112     * @task data
 113     */
 114    final public function getBool($name, $default = null) {
 115      if (isset($this->requestData[$name])) {
 116        if ($this->requestData[$name] === 'true') {
 117          return true;
 118        } else if ($this->requestData[$name] === 'false') {
 119          return false;
 120        } else {
 121          return (bool)$this->requestData[$name];
 122        }
 123      } else {
 124        return $default;
 125      }
 126    }
 127  
 128  
 129    /**
 130     * @task data
 131     */
 132    final public function getStr($name, $default = null) {
 133      if (isset($this->requestData[$name])) {
 134        $str = (string)$this->requestData[$name];
 135        // Normalize newline craziness.
 136        $str = str_replace(
 137          array("\r\n", "\r"),
 138          array("\n", "\n"),
 139          $str);
 140        return $str;
 141      } else {
 142        return $default;
 143      }
 144    }
 145  
 146  
 147    /**
 148     * @task data
 149     */
 150    final public function getArr($name, $default = array()) {
 151      if (isset($this->requestData[$name]) &&
 152          is_array($this->requestData[$name])) {
 153        return $this->requestData[$name];
 154      } else {
 155        return $default;
 156      }
 157    }
 158  
 159  
 160    /**
 161     * @task data
 162     */
 163    final public function getStrList($name, $default = array()) {
 164      if (!isset($this->requestData[$name])) {
 165        return $default;
 166      }
 167      $list = $this->getStr($name);
 168      $list = preg_split('/[\s,]+/', $list, $limit = -1, PREG_SPLIT_NO_EMPTY);
 169      return $list;
 170    }
 171  
 172  
 173    /**
 174     * @task data
 175     */
 176    final public function getExists($name) {
 177      return array_key_exists($name, $this->requestData);
 178    }
 179  
 180    final public function getFileExists($name) {
 181      return isset($_FILES[$name]) &&
 182             (idx($_FILES[$name], 'error') !== UPLOAD_ERR_NO_FILE);
 183    }
 184  
 185    final public function isHTTPGet() {
 186      return ($_SERVER['REQUEST_METHOD'] == 'GET');
 187    }
 188  
 189    final public function isHTTPPost() {
 190      return ($_SERVER['REQUEST_METHOD'] == 'POST');
 191    }
 192  
 193    final public function isAjax() {
 194      return $this->getExists(self::TYPE_AJAX);
 195    }
 196  
 197    final public function isJavelinWorkflow() {
 198      return $this->getExists(self::TYPE_WORKFLOW);
 199    }
 200  
 201    final public function isConduit() {
 202      return $this->getExists(self::TYPE_CONDUIT);
 203    }
 204  
 205    public static function getCSRFTokenName() {
 206      return '__csrf__';
 207    }
 208  
 209    public static function getCSRFHeaderName() {
 210      return 'X-Phabricator-Csrf';
 211    }
 212  
 213    final public function validateCSRF() {
 214      $token_name = self::getCSRFTokenName();
 215      $token = $this->getStr($token_name);
 216  
 217      // No token in the request, check the HTTP header which is added for Ajax
 218      // requests.
 219      if (empty($token)) {
 220        $token = self::getHTTPHeader(self::getCSRFHeaderName());
 221      }
 222  
 223      $valid = $this->getUser()->validateCSRFToken($token);
 224      if (!$valid) {
 225  
 226        // Add some diagnostic details so we can figure out if some CSRF issues
 227        // are JS problems or people accessing Ajax URIs directly with their
 228        // browsers.
 229        $more_info = array();
 230  
 231        if ($this->isAjax()) {
 232          $more_info[] = pht('This was an Ajax request.');
 233        } else {
 234          $more_info[] = pht('This was a Web request.');
 235        }
 236  
 237        if ($token) {
 238          $more_info[] = pht('This request had an invalid CSRF token.');
 239        } else {
 240          $more_info[] = pht('This request had no CSRF token.');
 241        }
 242  
 243        // Give a more detailed explanation of how to avoid the exception
 244        // in developer mode.
 245        if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
 246          // TODO: Clean this up, see T1921.
 247          $more_info[] =
 248            "To avoid this error, use phabricator_form() to construct forms. ".
 249            "If you are already using phabricator_form(), make sure the form ".
 250            "'action' uses a relative URI (i.e., begins with a '/'). Forms ".
 251            "using absolute URIs do not include CSRF tokens, to prevent ".
 252            "leaking tokens to external sites.\n\n".
 253            "If this page performs writes which do not require CSRF ".
 254            "protection (usually, filling caches or logging), you can use ".
 255            "AphrontWriteGuard::beginScopedUnguardedWrites() to temporarily ".
 256            "bypass CSRF protection while writing. You should use this only ".
 257            "for writes which can not be protected with normal CSRF ".
 258            "mechanisms.\n\n".
 259            "Some UI elements (like PhabricatorActionListView) also have ".
 260            "methods which will allow you to render links as forms (like ".
 261            "setRenderAsForm(true)).";
 262        }
 263  
 264        // This should only be able to happen if you load a form, pull your
 265        // internet for 6 hours, and then reconnect and immediately submit,
 266        // but give the user some indication of what happened since the workflow
 267        // is incredibly confusing otherwise.
 268        throw new AphrontCSRFException(
 269          pht(
 270            "You are trying to save some data to Phabricator, but the request ".
 271            "your browser made included an incorrect token. Reload the page ".
 272            "and try again. You may need to clear your cookies.\n\n%s",
 273            implode("\n", $more_info)));
 274      }
 275  
 276      return true;
 277    }
 278  
 279    final public function isFormPost() {
 280      $post = $this->getExists(self::TYPE_FORM) &&
 281              !$this->getExists(self::TYPE_HISEC) &&
 282              $this->isHTTPPost();
 283  
 284      if (!$post) {
 285        return false;
 286      }
 287  
 288      return $this->validateCSRF();
 289    }
 290  
 291    final public function isFormOrHisecPost() {
 292      $post = $this->getExists(self::TYPE_FORM) &&
 293              $this->isHTTPPost();
 294  
 295      if (!$post) {
 296        return false;
 297      }
 298  
 299      return $this->validateCSRF();
 300    }
 301  
 302  
 303    final public function setCookiePrefix($prefix) {
 304      $this->cookiePrefix = $prefix;
 305      return $this;
 306    }
 307  
 308    final private function getPrefixedCookieName($name) {
 309      if (strlen($this->cookiePrefix)) {
 310        return $this->cookiePrefix.'_'.$name;
 311      } else {
 312        return $name;
 313      }
 314    }
 315  
 316    final public function getCookie($name, $default = null) {
 317      $name = $this->getPrefixedCookieName($name);
 318      $value = idx($_COOKIE, $name, $default);
 319  
 320      // Internally, PHP deletes cookies by setting them to the value 'deleted'
 321      // with an expiration date in the past.
 322  
 323      // At least in Safari, the browser may send this cookie anyway in some
 324      // circumstances. After logging out, the 302'd GET to /login/ consistently
 325      // includes deleted cookies on my local install. If a cookie value is
 326      // literally 'deleted', pretend it does not exist.
 327  
 328      if ($value === 'deleted') {
 329        return null;
 330      }
 331  
 332      return $value;
 333    }
 334  
 335    final public function clearCookie($name) {
 336      $name = $this->getPrefixedCookieName($name);
 337      $this->setCookieWithExpiration($name, '', time() - (60 * 60 * 24 * 30));
 338      unset($_COOKIE[$name]);
 339    }
 340  
 341    /**
 342     * Get the domain which cookies should be set on for this request, or null
 343     * if the request does not correspond to a valid cookie domain.
 344     *
 345     * @return PhutilURI|null   Domain URI, or null if no valid domain exists.
 346     *
 347     * @task cookie
 348     */
 349    private function getCookieDomainURI() {
 350      if (PhabricatorEnv::getEnvConfig('security.require-https') &&
 351          !$this->isHTTPS()) {
 352        return null;
 353      }
 354  
 355      $host = $this->getHost();
 356  
 357      // If there's no base domain configured, just use whatever the request
 358      // domain is. This makes setup easier, and we'll tell administrators to
 359      // configure a base domain during the setup process.
 360      $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
 361      if (!strlen($base_uri)) {
 362        return new PhutilURI('http://'.$host.'/');
 363      }
 364  
 365      $alternates = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris');
 366      $allowed_uris = array_merge(
 367        array($base_uri),
 368        $alternates);
 369  
 370      foreach ($allowed_uris as $allowed_uri) {
 371        $uri = new PhutilURI($allowed_uri);
 372        if ($uri->getDomain() == $host) {
 373          return $uri;
 374        }
 375      }
 376  
 377      return null;
 378    }
 379  
 380    /**
 381     * Determine if security policy rules will allow cookies to be set when
 382     * responding to the request.
 383     *
 384     * @return bool True if setCookie() will succeed. If this method returns
 385     *              false, setCookie() will throw.
 386     *
 387     * @task cookie
 388     */
 389    final public function canSetCookies() {
 390      return (bool)$this->getCookieDomainURI();
 391    }
 392  
 393  
 394    /**
 395     * Set a cookie which does not expire for a long time.
 396     *
 397     * To set a temporary cookie, see @{method:setTemporaryCookie}.
 398     *
 399     * @param string  Cookie name.
 400     * @param string  Cookie value.
 401     * @return this
 402     * @task cookie
 403     */
 404    final public function setCookie($name, $value) {
 405      $far_future = time() + (60 * 60 * 24 * 365 * 5);
 406      return $this->setCookieWithExpiration($name, $value, $far_future);
 407    }
 408  
 409  
 410    /**
 411     * Set a cookie which expires soon.
 412     *
 413     * To set a durable cookie, see @{method:setCookie}.
 414     *
 415     * @param string  Cookie name.
 416     * @param string  Cookie value.
 417     * @return this
 418     * @task cookie
 419     */
 420    final public function setTemporaryCookie($name, $value) {
 421      return $this->setCookieWithExpiration($name, $value, 0);
 422    }
 423  
 424  
 425    /**
 426     * Set a cookie with a given expiration policy.
 427     *
 428     * @param string  Cookie name.
 429     * @param string  Cookie value.
 430     * @param int     Epoch timestamp for cookie expiration.
 431     * @return this
 432     * @task cookie
 433     */
 434    final private function setCookieWithExpiration(
 435      $name,
 436      $value,
 437      $expire) {
 438  
 439      $is_secure = false;
 440  
 441      $base_domain_uri = $this->getCookieDomainURI();
 442      if (!$base_domain_uri) {
 443        $configured_as = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
 444        $accessed_as = $this->getHost();
 445  
 446        throw new Exception(
 447          pht(
 448            'This Phabricator install is configured as "%s", but you are '.
 449            'using the domain name "%s" to access a page which is trying to '.
 450            'set a cookie. Acccess Phabricator on the configured primary '.
 451            'domain or a configured alternate domain. Phabricator will not '.
 452            'set cookies on other domains for security reasons.',
 453            $configured_as,
 454            $accessed_as));
 455      }
 456  
 457      $base_domain = $base_domain_uri->getDomain();
 458      $is_secure = ($base_domain_uri->getProtocol() == 'https');
 459  
 460      $name = $this->getPrefixedCookieName($name);
 461  
 462      if (php_sapi_name() == 'cli') {
 463        // Do nothing, to avoid triggering "Cannot modify header information"
 464        // warnings.
 465  
 466        // TODO: This is effectively a test for whether we're running in a unit
 467        // test or not. Move this actual call to HTTPSink?
 468      } else {
 469        setcookie(
 470          $name,
 471          $value,
 472          $expire,
 473          $path = '/',
 474          $base_domain,
 475          $is_secure,
 476          $http_only = true);
 477      }
 478  
 479      $_COOKIE[$name] = $value;
 480  
 481      return $this;
 482    }
 483  
 484    final public function setUser($user) {
 485      $this->user = $user;
 486      return $this;
 487    }
 488  
 489    final public function getUser() {
 490      return $this->user;
 491    }
 492  
 493    final public function getViewer() {
 494      return $this->user;
 495    }
 496  
 497    final public function getRequestURI() {
 498      $get = $_GET;
 499      unset($get['__path__']);
 500      $path = phutil_escape_uri($this->getPath());
 501      return id(new PhutilURI($path))->setQueryParams($get);
 502    }
 503  
 504    final public function isDialogFormPost() {
 505      return $this->isFormPost() && $this->getStr('__dialog__');
 506    }
 507  
 508    final public function getRemoteAddr() {
 509      return $_SERVER['REMOTE_ADDR'];
 510    }
 511  
 512    public function isHTTPS() {
 513      if (empty($_SERVER['HTTPS'])) {
 514        return false;
 515      }
 516      if (!strcasecmp($_SERVER['HTTPS'], 'off')) {
 517        return false;
 518      }
 519      return true;
 520    }
 521  
 522    public function isContinueRequest() {
 523      return $this->isFormPost() && $this->getStr('__continue__');
 524    }
 525  
 526    public function isPreviewRequest() {
 527      return $this->isFormPost() && $this->getStr('__preview__');
 528    }
 529  
 530    /**
 531     * Get application request parameters in a flattened form suitable for
 532     * inclusion in an HTTP request, excluding parameters with special meanings.
 533     * This is primarily useful if you want to ask the user for more input and
 534     * then resubmit their request.
 535     *
 536     * @return  dict<string, string>  Original request parameters.
 537     */
 538    public function getPassthroughRequestParameters() {
 539      return self::flattenData($this->getPassthroughRequestData());
 540    }
 541  
 542    /**
 543     * Get request data other than "magic" parameters.
 544     *
 545     * @return dict<string, wild> Request data, with magic filtered out.
 546     */
 547    public function getPassthroughRequestData() {
 548      $data = $this->getRequestData();
 549  
 550      // Remove magic parameters like __dialog__ and __ajax__.
 551      foreach ($data as $key => $value) {
 552        if (!strncmp($key, '__', 2)) {
 553          unset($data[$key]);
 554        }
 555      }
 556  
 557      return $data;
 558    }
 559  
 560  
 561    /**
 562     * Flatten an array of key-value pairs (possibly including arrays as values)
 563     * into a list of key-value pairs suitable for submitting via HTTP request
 564     * (with arrays flattened).
 565     *
 566     * @param   dict<string, wild>    Data to flatten.
 567     * @return  dict<string, string>  Flat data suitable for inclusion in an HTTP
 568     *                                request.
 569     */
 570    public static function flattenData(array $data) {
 571      $result = array();
 572      foreach ($data as $key => $value) {
 573        if (is_array($value)) {
 574          foreach (self::flattenData($value) as $fkey => $fvalue) {
 575            $fkey = '['.preg_replace('/(?=\[)|$/', ']', $fkey, $limit = 1);
 576            $result[$key.$fkey] = $fvalue;
 577          }
 578        } else {
 579          $result[$key] = (string)$value;
 580        }
 581      }
 582  
 583      ksort($result);
 584  
 585      return $result;
 586    }
 587  
 588  
 589    /**
 590     * Read the value of an HTTP header from `$_SERVER`, or a similar datasource.
 591     *
 592     * This function accepts a canonical header name, like `"Accept-Encoding"`,
 593     * and looks up the appropriate value in `$_SERVER` (in this case,
 594     * `"HTTP_ACCEPT_ENCODING"`).
 595     *
 596     * @param   string        Canonical header name, like `"Accept-Encoding"`.
 597     * @param   wild          Default value to return if header is not present.
 598     * @param   array?        Read this instead of `$_SERVER`.
 599     * @return  string|wild   Header value if present, or `$default` if not.
 600     */
 601    public static function getHTTPHeader($name, $default = null, $data = null) {
 602      // PHP mangles HTTP headers by uppercasing them and replacing hyphens with
 603      // underscores, then prepending 'HTTP_'.
 604      $php_index = strtoupper($name);
 605      $php_index = str_replace('-', '_', $php_index);
 606  
 607      $try_names = array();
 608  
 609      $try_names[] = 'HTTP_'.$php_index;
 610      if ($php_index == 'CONTENT_TYPE' || $php_index == 'CONTENT_LENGTH') {
 611        // These headers may be available under alternate names. See
 612        // http://www.php.net/manual/en/reserved.variables.server.php#110763
 613        $try_names[] = $php_index;
 614      }
 615  
 616      if ($data === null) {
 617        $data = $_SERVER;
 618      }
 619  
 620      foreach ($try_names as $try_name) {
 621        if (array_key_exists($try_name, $data)) {
 622          return $data[$try_name];
 623        }
 624      }
 625  
 626      return $default;
 627    }
 628  
 629  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1