[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/infrastructure/env/ -> PhabricatorEnv.php (source)

   1  <?php
   2  
   3  /**
   4   * Manages the execution environment configuration, exposing APIs to read
   5   * configuration settings and other similar values that are derived directly
   6   * from configuration settings.
   7   *
   8   *
   9   * = Reading Configuration =
  10   *
  11   * The primary role of this class is to provide an API for reading
  12   * Phabricator configuration, @{method:getEnvConfig}:
  13   *
  14   *   $value = PhabricatorEnv::getEnvConfig('some.key', $default);
  15   *
  16   * The class also handles some URI construction based on configuration, via
  17   * the methods @{method:getURI}, @{method:getProductionURI},
  18   * @{method:getCDNURI}, and @{method:getDoclink}.
  19   *
  20   * For configuration which allows you to choose a class to be responsible for
  21   * some functionality (e.g., which mail adapter to use to deliver email),
  22   * @{method:newObjectFromConfig} provides a simple interface that validates
  23   * the configured value.
  24   *
  25   *
  26   * = Unit Test Support =
  27   *
  28   * In unit tests, you can use @{method:beginScopedEnv} to create a temporary,
  29   * mutable environment. The method returns a scope guard object which restores
  30   * the environment when it is destroyed. For example:
  31   *
  32   *   public function testExample() {
  33   *     $env = PhabricatorEnv::beginScopedEnv();
  34   *     $env->overrideEnv('some.key', 'new-value-for-this-test');
  35   *
  36   *     // Some test which depends on the value of 'some.key'.
  37   *
  38   *   }
  39   *
  40   * Your changes will persist until the `$env` object leaves scope or is
  41   * destroyed.
  42   *
  43   * You should //not// use this in normal code.
  44   *
  45   *
  46   * @task read     Reading Configuration
  47   * @task uri      URI Validation
  48   * @task test     Unit Test Support
  49   * @task internal Internals
  50   */
  51  final class PhabricatorEnv {
  52  
  53    private static $sourceStack;
  54    private static $repairSource;
  55    private static $overrideSource;
  56    private static $requestBaseURI;
  57    private static $cache;
  58  
  59    /**
  60     * @phutil-external-symbol class PhabricatorStartup
  61     */
  62    public static function initializeWebEnvironment() {
  63      self::initializeCommonEnvironment();
  64    }
  65  
  66    public static function initializeScriptEnvironment() {
  67      self::initializeCommonEnvironment();
  68  
  69      // NOTE: This is dangerous in general, but we know we're in a script context
  70      // and are not vulnerable to CSRF.
  71      AphrontWriteGuard::allowDangerousUnguardedWrites(true);
  72  
  73      // There are several places where we log information (about errors, events,
  74      // service calls, etc.) for analysis via DarkConsole or similar. These are
  75      // useful for web requests, but grow unboundedly in long-running scripts and
  76      // daemons. Discard data as it arrives in these cases.
  77      PhutilServiceProfiler::getInstance()->enableDiscardMode();
  78      DarkConsoleErrorLogPluginAPI::enableDiscardMode();
  79      DarkConsoleEventPluginAPI::enableDiscardMode();
  80    }
  81  
  82  
  83    private static function initializeCommonEnvironment() {
  84      PhutilErrorHandler::initialize();
  85  
  86      self::buildConfigurationSourceStack();
  87  
  88      // Force a valid timezone. If both PHP and Phabricator configuration are
  89      // invalid, use UTC.
  90      $tz = PhabricatorEnv::getEnvConfig('phabricator.timezone');
  91      if ($tz) {
  92        @date_default_timezone_set($tz);
  93      }
  94      $ok = @date_default_timezone_set(date_default_timezone_get());
  95      if (!$ok) {
  96        date_default_timezone_set('UTC');
  97      }
  98  
  99      // Prepend '/support/bin' and append any paths to $PATH if we need to.
 100      $env_path = getenv('PATH');
 101      $phabricator_path = dirname(phutil_get_library_root('phabricator'));
 102      $support_path = $phabricator_path.'/support/bin';
 103      $env_path = $support_path.PATH_SEPARATOR.$env_path;
 104      $append_dirs = PhabricatorEnv::getEnvConfig('environment.append-paths');
 105      if (!empty($append_dirs)) {
 106        $append_path = implode(PATH_SEPARATOR, $append_dirs);
 107        $env_path = $env_path.PATH_SEPARATOR.$append_path;
 108      }
 109      putenv('PATH='.$env_path);
 110  
 111      // Write this back into $_ENV, too, so ExecFuture picks it up when creating
 112      // subprocess environments.
 113      $_ENV['PATH'] = $env_path;
 114  
 115      PhabricatorEventEngine::initialize();
 116  
 117      $translation = PhabricatorEnv::newObjectFromConfig('translation.provider');
 118      PhutilTranslator::getInstance()
 119        ->setLanguage($translation->getLanguage())
 120        ->addTranslations($translation->getTranslations());
 121    }
 122  
 123    private static function buildConfigurationSourceStack() {
 124      self::dropConfigCache();
 125  
 126      $stack = new PhabricatorConfigStackSource();
 127      self::$sourceStack = $stack;
 128  
 129      $default_source = id(new PhabricatorConfigDefaultSource())
 130        ->setName(pht('Global Default'));
 131      $stack->pushSource($default_source);
 132  
 133      $env = self::getSelectedEnvironmentName();
 134      if ($env) {
 135        $stack->pushSource(
 136          id(new PhabricatorConfigFileSource($env))
 137            ->setName(pht("File '%s'", $env)));
 138      }
 139  
 140      $stack->pushSource(
 141        id(new PhabricatorConfigLocalSource())
 142          ->setName(pht('Local Config')));
 143  
 144      // If the install overrides the database adapter, we might need to load
 145      // the database adapter class before we can push on the database config.
 146      // This config is locked and can't be edited from the web UI anyway.
 147      foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) {
 148        phutil_load_library($library);
 149      }
 150  
 151      // If custom libraries specify config options, they won't get default
 152      // values as the Default source has already been loaded, so we get it to
 153      // pull in all options from non-phabricator libraries now they are loaded.
 154      $default_source->loadExternalOptions();
 155  
 156      // If this install has site config sources, load them now.
 157      $site_sources = id(new PhutilSymbolLoader())
 158        ->setAncestorClass('PhabricatorConfigSiteSource')
 159        ->loadObjects();
 160      $site_sources = msort($site_sources, 'getPriority');
 161      foreach ($site_sources as $site_source) {
 162        $stack->pushSource($site_source);
 163      }
 164  
 165      try {
 166        $stack->pushSource(
 167          id(new PhabricatorConfigDatabaseSource('default'))
 168            ->setName(pht('Database')));
 169      } catch (AphrontQueryException $exception) {
 170        // If the database is not available, just skip this configuration
 171        // source. This happens during `bin/storage upgrade`, `bin/conf` before
 172        // schema setup, etc.
 173      }
 174    }
 175  
 176    public static function repairConfig($key, $value) {
 177      if (!self::$repairSource) {
 178        self::$repairSource = id(new PhabricatorConfigDictionarySource(array()))
 179          ->setName(pht('Repaired Config'));
 180        self::$sourceStack->pushSource(self::$repairSource);
 181      }
 182      self::$repairSource->setKeys(array($key => $value));
 183      self::dropConfigCache();
 184    }
 185  
 186    public static function overrideConfig($key, $value) {
 187      if (!self::$overrideSource) {
 188        self::$overrideSource = id(new PhabricatorConfigDictionarySource(array()))
 189          ->setName(pht('Overridden Config'));
 190        self::$sourceStack->pushSource(self::$overrideSource);
 191      }
 192      self::$overrideSource->setKeys(array($key => $value));
 193      self::dropConfigCache();
 194    }
 195  
 196    public static function getUnrepairedEnvConfig($key, $default = null) {
 197      foreach (self::$sourceStack->getStack() as $source) {
 198        if ($source === self::$repairSource) {
 199          continue;
 200        }
 201        $result = $source->getKeys(array($key));
 202        if ($result) {
 203          return $result[$key];
 204        }
 205      }
 206      return $default;
 207    }
 208  
 209    public static function getSelectedEnvironmentName() {
 210      $env_var = 'PHABRICATOR_ENV';
 211  
 212      $env = idx($_SERVER, $env_var);
 213  
 214      if (!$env) {
 215        $env = getenv($env_var);
 216      }
 217  
 218      if (!$env) {
 219        $env = idx($_ENV, $env_var);
 220      }
 221  
 222      if (!$env) {
 223        $root = dirname(phutil_get_library_root('phabricator'));
 224        $path = $root.'/conf/local/ENVIRONMENT';
 225        if (Filesystem::pathExists($path)) {
 226          $env = trim(Filesystem::readFile($path));
 227        }
 228      }
 229  
 230      return $env;
 231    }
 232  
 233    public static function calculateEnvironmentHash() {
 234      $keys = array_keys(self::getAllConfigKeys());
 235      ksort($keys);
 236  
 237      $values = array();
 238      foreach ($keys as $key) {
 239        $values[$key] = self::getEnvConfigIfExists($key);
 240      }
 241      return PhabricatorHash::digest(json_encode($values));
 242    }
 243  
 244  
 245  /* -(  Reading Configuration  )---------------------------------------------- */
 246  
 247  
 248    /**
 249     * Get the current configuration setting for a given key.
 250     *
 251     * If the key is not found, then throw an Exception.
 252     *
 253     * @task read
 254     */
 255    public static function getEnvConfig($key) {
 256      if (isset(self::$cache[$key])) {
 257        return self::$cache[$key];
 258      }
 259  
 260      if (array_key_exists($key, self::$cache)) {
 261        return self::$cache[$key];
 262      }
 263  
 264      $result = self::$sourceStack->getKeys(array($key));
 265      if (array_key_exists($key, $result)) {
 266        self::$cache[$key] = $result[$key];
 267        return $result[$key];
 268      } else {
 269        throw new Exception("No config value specified for key '{$key}'.");
 270      }
 271    }
 272  
 273  
 274    /**
 275     * Get the current configuration setting for a given key. If the key
 276     * does not exist, return a default value instead of throwing. This is
 277     * primarily useful for migrations involving keys which are slated for
 278     * removal.
 279     *
 280     * @task read
 281     */
 282    public static function getEnvConfigIfExists($key, $default = null) {
 283      try {
 284        return self::getEnvConfig($key);
 285      } catch (Exception $ex) {
 286        return $default;
 287      }
 288    }
 289  
 290  
 291    /**
 292     * Get the fully-qualified URI for a path.
 293     *
 294     * @task read
 295     */
 296    public static function getURI($path) {
 297      return rtrim(self::getAnyBaseURI(), '/').$path;
 298    }
 299  
 300  
 301    /**
 302     * Get the fully-qualified production URI for a path.
 303     *
 304     * @task read
 305     */
 306    public static function getProductionURI($path) {
 307      // If we're passed a URI which already has a domain, simply return it
 308      // unmodified. In particular, files may have URIs which point to a CDN
 309      // domain.
 310      $uri = new PhutilURI($path);
 311      if ($uri->getDomain()) {
 312        return $path;
 313      }
 314  
 315      $production_domain = self::getEnvConfig('phabricator.production-uri');
 316      if (!$production_domain) {
 317        $production_domain = self::getAnyBaseURI();
 318      }
 319      return rtrim($production_domain, '/').$path;
 320    }
 321  
 322    public static function getAllowedURIs($path) {
 323      $uri = new PhutilURI($path);
 324      if ($uri->getDomain()) {
 325        return $path;
 326      }
 327  
 328      $allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
 329      $return = array();
 330      foreach ($allowed_uris as $allowed_uri) {
 331        $return[] = rtrim($allowed_uri, '/').$path;
 332      }
 333  
 334      return $return;
 335    }
 336  
 337  
 338    /**
 339     * Get the fully-qualified production URI for a static resource path.
 340     *
 341     * @task read
 342     */
 343    public static function getCDNURI($path) {
 344      $alt = self::getEnvConfig('security.alternate-file-domain');
 345      if (!$alt) {
 346        $alt = self::getAnyBaseURI();
 347      }
 348      $uri = new PhutilURI($alt);
 349      $uri->setPath($path);
 350      return (string)$uri;
 351    }
 352  
 353  
 354    /**
 355     * Get the fully-qualified production URI for a documentation resource.
 356     *
 357     * @task read
 358     */
 359    public static function getDoclink($resource, $type = 'article') {
 360      $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/');
 361      $uri->setQueryParam('name', $resource);
 362      $uri->setQueryParam('type', $type);
 363      $uri->setQueryParam('jump', true);
 364      return (string)$uri;
 365    }
 366  
 367  
 368    /**
 369     * Build a concrete object from a configuration key.
 370     *
 371     * @task read
 372     */
 373    public static function newObjectFromConfig($key, $args = array()) {
 374      $class = self::getEnvConfig($key);
 375      return newv($class, $args);
 376    }
 377  
 378    public static function getAnyBaseURI() {
 379      $base_uri = self::getEnvConfig('phabricator.base-uri');
 380  
 381      if (!$base_uri) {
 382        $base_uri = self::getRequestBaseURI();
 383      }
 384  
 385      if (!$base_uri) {
 386        throw new Exception(
 387          "Define 'phabricator.base-uri' in your configuration to continue.");
 388      }
 389  
 390      return $base_uri;
 391    }
 392  
 393    public static function getRequestBaseURI() {
 394      return self::$requestBaseURI;
 395    }
 396  
 397    public static function setRequestBaseURI($uri) {
 398      self::$requestBaseURI = $uri;
 399    }
 400  
 401  /* -(  Unit Test Support  )-------------------------------------------------- */
 402  
 403  
 404    /**
 405     * @task test
 406     */
 407    public static function beginScopedEnv() {
 408      return new PhabricatorScopedEnv(self::pushTestEnvironment());
 409    }
 410  
 411  
 412    /**
 413     * @task test
 414     */
 415    private static function pushTestEnvironment() {
 416      self::dropConfigCache();
 417      $source = new PhabricatorConfigDictionarySource(array());
 418      self::$sourceStack->pushSource($source);
 419      return spl_object_hash($source);
 420    }
 421  
 422  
 423    /**
 424     * @task test
 425     */
 426    public static function popTestEnvironment($key) {
 427      self::dropConfigCache();
 428      $source = self::$sourceStack->popSource();
 429      $stack_key = spl_object_hash($source);
 430      if ($stack_key !== $key) {
 431        self::$sourceStack->pushSource($source);
 432        throw new Exception(
 433          'Scoped environments were destroyed in a diffent order than they '.
 434          'were initialized.');
 435      }
 436    }
 437  
 438  
 439  /* -(  URI Validation  )----------------------------------------------------- */
 440  
 441  
 442    /**
 443     * Detect if a URI satisfies either @{method:isValidLocalWebResource} or
 444     * @{method:isValidRemoteWebResource}, i.e. is a page on this server or the
 445     * URI of some other resource which has a valid protocol. This rejects
 446     * garbage URIs and URIs with protocols which do not appear in the
 447     * ##uri.allowed-protocols## configuration, notably 'javascript:' URIs.
 448     *
 449     * NOTE: This method is generally intended to reject URIs which it may be
 450     * unsafe to put in an "href" link attribute.
 451     *
 452     * @param string URI to test.
 453     * @return bool True if the URI identifies a web resource.
 454     * @task uri
 455     */
 456    public static function isValidWebResource($uri) {
 457      return self::isValidLocalWebResource($uri) ||
 458             self::isValidRemoteWebResource($uri);
 459    }
 460  
 461  
 462    /**
 463     * Detect if a URI identifies some page on this server.
 464     *
 465     * NOTE: This method is generally intended to reject URIs which it may be
 466     * unsafe to issue a "Location:" redirect to.
 467     *
 468     * @param string URI to test.
 469     * @return bool True if the URI identifies a local page.
 470     * @task uri
 471     */
 472    public static function isValidLocalWebResource($uri) {
 473      $uri = (string)$uri;
 474  
 475      if (!strlen($uri)) {
 476        return false;
 477      }
 478  
 479      if (preg_match('/\s/', $uri)) {
 480        // PHP hasn't been vulnerable to header injection attacks for a bunch of
 481        // years, but we can safely reject these anyway since they're never valid.
 482        return false;
 483      }
 484  
 485      // Chrome (at a minimum) interprets backslashes in Location headers and the
 486      // URL bar as forward slashes. This is probably intended to reduce user
 487      // error caused by confusion over which key is "forward slash" vs "back
 488      // slash".
 489      //
 490      // However, it means a URI like "/\evil.com" is interpreted like
 491      // "//evil.com", which is a protocol relative remote URI.
 492      //
 493      // Since we currently never generate URIs with backslashes in them, reject
 494      // these unconditionally rather than trying to figure out how browsers will
 495      // interpret them.
 496      if (preg_match('/\\\\/', $uri)) {
 497        return false;
 498      }
 499  
 500      // Valid URIs must begin with '/', followed by the end of the string or some
 501      // other non-'/' character. This rejects protocol-relative URIs like
 502      // "//evil.com/evil_stuff/".
 503      return (bool)preg_match('@^/([^/]|$)@', $uri);
 504    }
 505  
 506  
 507    /**
 508     * Detect if a URI identifies some valid remote resource.
 509     *
 510     * @param string URI to test.
 511     * @return bool True if a URI idenfies a remote resource with an allowed
 512     *              protocol.
 513     * @task uri
 514     */
 515    public static function isValidRemoteWebResource($uri) {
 516      $uri = (string)$uri;
 517  
 518      $proto = id(new PhutilURI($uri))->getProtocol();
 519      if (!$proto) {
 520        return false;
 521      }
 522  
 523      $allowed = self::getEnvConfig('uri.allowed-protocols');
 524      if (empty($allowed[$proto])) {
 525        return false;
 526      }
 527  
 528      return true;
 529    }
 530  
 531  
 532  /* -(  Internals  )---------------------------------------------------------- */
 533  
 534  
 535    /**
 536     * @task internal
 537     */
 538    public static function envConfigExists($key) {
 539      return array_key_exists($key, self::$sourceStack->getKeys(array($key)));
 540    }
 541  
 542  
 543    /**
 544     * @task internal
 545     */
 546    public static function getAllConfigKeys() {
 547      return self::$sourceStack->getAllKeys();
 548    }
 549  
 550    public static function getConfigSourceStack() {
 551      return self::$sourceStack;
 552    }
 553  
 554    /**
 555     * @task internal
 556     */
 557    public static function overrideTestEnvConfig($stack_key, $key, $value) {
 558      $tmp = array();
 559  
 560      // If we don't have the right key, we'll throw when popping the last
 561      // source off the stack.
 562      do {
 563        $source = self::$sourceStack->popSource();
 564        array_unshift($tmp, $source);
 565        if (spl_object_hash($source) == $stack_key) {
 566          $source->setKeys(array($key => $value));
 567          break;
 568        }
 569      } while (true);
 570  
 571      foreach ($tmp as $source) {
 572        self::$sourceStack->pushSource($source);
 573      }
 574  
 575      self::dropConfigCache();
 576    }
 577  
 578    private static function dropConfigCache() {
 579      self::$cache = array();
 580    }
 581  
 582  }


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