overrideEnv('some.key', 'new-value-for-this-test'); * * // Some test which depends on the value of 'some.key'. * * } * * Your changes will persist until the `$env` object leaves scope or is * destroyed. * * You should //not// use this in normal code. * * * @task read Reading Configuration * @task uri URI Validation * @task test Unit Test Support * @task internal Internals */ final class PhabricatorEnv { private static $sourceStack; private static $repairSource; private static $overrideSource; private static $requestBaseURI; private static $cache; /** * @phutil-external-symbol class PhabricatorStartup */ public static function initializeWebEnvironment() { self::initializeCommonEnvironment(); } public static function initializeScriptEnvironment() { self::initializeCommonEnvironment(); // NOTE: This is dangerous in general, but we know we're in a script context // and are not vulnerable to CSRF. AphrontWriteGuard::allowDangerousUnguardedWrites(true); // There are several places where we log information (about errors, events, // service calls, etc.) for analysis via DarkConsole or similar. These are // useful for web requests, but grow unboundedly in long-running scripts and // daemons. Discard data as it arrives in these cases. PhutilServiceProfiler::getInstance()->enableDiscardMode(); DarkConsoleErrorLogPluginAPI::enableDiscardMode(); DarkConsoleEventPluginAPI::enableDiscardMode(); } private static function initializeCommonEnvironment() { PhutilErrorHandler::initialize(); self::buildConfigurationSourceStack(); // Force a valid timezone. If both PHP and Phabricator configuration are // invalid, use UTC. $tz = PhabricatorEnv::getEnvConfig('phabricator.timezone'); if ($tz) { @date_default_timezone_set($tz); } $ok = @date_default_timezone_set(date_default_timezone_get()); if (!$ok) { date_default_timezone_set('UTC'); } // Prepend '/support/bin' and append any paths to $PATH if we need to. $env_path = getenv('PATH'); $phabricator_path = dirname(phutil_get_library_root('phabricator')); $support_path = $phabricator_path.'/support/bin'; $env_path = $support_path.PATH_SEPARATOR.$env_path; $append_dirs = PhabricatorEnv::getEnvConfig('environment.append-paths'); if (!empty($append_dirs)) { $append_path = implode(PATH_SEPARATOR, $append_dirs); $env_path = $env_path.PATH_SEPARATOR.$append_path; } putenv('PATH='.$env_path); // Write this back into $_ENV, too, so ExecFuture picks it up when creating // subprocess environments. $_ENV['PATH'] = $env_path; // If an instance identifier is defined, write it into the environment so // it's available to subprocesses. $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { putenv('PHABRICATOR_INSTANCE='.$instance); $_ENV['PHABRICATOR_INSTANCE'] = $instance; } PhabricatorEventEngine::initialize(); $translation = PhabricatorEnv::newObjectFromConfig('translation.provider'); PhutilTranslator::getInstance() ->setLanguage($translation->getLanguage()) ->addTranslations($translation->getCleanTranslations()); } private static function buildConfigurationSourceStack() { self::dropConfigCache(); $stack = new PhabricatorConfigStackSource(); self::$sourceStack = $stack; $default_source = id(new PhabricatorConfigDefaultSource()) ->setName(pht('Global Default')); $stack->pushSource($default_source); $env = self::getSelectedEnvironmentName(); if ($env) { $stack->pushSource( id(new PhabricatorConfigFileSource($env)) ->setName(pht("File '%s'", $env))); } $stack->pushSource( id(new PhabricatorConfigLocalSource()) ->setName(pht('Local Config'))); // If the install overrides the database adapter, we might need to load // the database adapter class before we can push on the database config. // This config is locked and can't be edited from the web UI anyway. foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) { phutil_load_library($library); } // If custom libraries specify config options, they won't get default // values as the Default source has already been loaded, so we get it to // pull in all options from non-phabricator libraries now they are loaded. $default_source->loadExternalOptions(); // If this install has site config sources, load them now. $site_sources = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorConfigSiteSource') ->loadObjects(); $site_sources = msort($site_sources, 'getPriority'); foreach ($site_sources as $site_source) { $stack->pushSource($site_source); } try { $stack->pushSource( id(new PhabricatorConfigDatabaseSource('default')) ->setName(pht('Database'))); } catch (AphrontQueryException $exception) { // If the database is not available, just skip this configuration // source. This happens during `bin/storage upgrade`, `bin/conf` before // schema setup, etc. } } public static function repairConfig($key, $value) { if (!self::$repairSource) { self::$repairSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Repaired Config')); self::$sourceStack->pushSource(self::$repairSource); } self::$repairSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function overrideConfig($key, $value) { if (!self::$overrideSource) { self::$overrideSource = id(new PhabricatorConfigDictionarySource(array())) ->setName(pht('Overridden Config')); self::$sourceStack->pushSource(self::$overrideSource); } self::$overrideSource->setKeys(array($key => $value)); self::dropConfigCache(); } public static function getUnrepairedEnvConfig($key, $default = null) { foreach (self::$sourceStack->getStack() as $source) { if ($source === self::$repairSource) { continue; } $result = $source->getKeys(array($key)); if ($result) { return $result[$key]; } } return $default; } public static function getSelectedEnvironmentName() { $env_var = 'PHABRICATOR_ENV'; $env = idx($_SERVER, $env_var); if (!$env) { $env = getenv($env_var); } if (!$env) { $env = idx($_ENV, $env_var); } if (!$env) { $root = dirname(phutil_get_library_root('phabricator')); $path = $root.'/conf/local/ENVIRONMENT'; if (Filesystem::pathExists($path)) { $env = trim(Filesystem::readFile($path)); } } return $env; } public static function calculateEnvironmentHash() { $keys = self::getKeysForConsistencyCheck(); $values = array(); foreach ($keys as $key) { $values[$key] = self::getEnvConfigIfExists($key); } return PhabricatorHash::digest(json_encode($values)); } /** * Returns a summary of non-default configuration settings to allow the * "daemons and web have different config" setup check to list divergent * keys. */ public static function calculateEnvironmentInfo() { $keys = self::getKeysForConsistencyCheck(); $info = array(); $defaults = id(new PhabricatorConfigDefaultSource())->getAllKeys(); foreach ($keys as $key) { $current = self::getEnvConfigIfExists($key); $default = idx($defaults, $key, null); if ($current !== $default) { $info[$key] = PhabricatorHash::digestForIndex(json_encode($current)); } } $keys_hash = array_keys($defaults); sort($keys_hash); $keys_hash = implode("\0", $keys_hash); $keys_hash = PhabricatorHash::digestForIndex($keys_hash); return array( 'version' => 1, 'keys' => $keys_hash, 'values' => $info, ); } /** * Compare two environment info summaries to generate a human-readable * list of discrepancies. */ public static function compareEnvironmentInfo(array $u, array $v) { $issues = array(); $uversion = idx($u, 'version'); $vversion = idx($v, 'version'); if ($uversion != $vversion) { $issues[] = pht( 'The two configurations were generated by different versions '. 'of Phabricator.'); // These may not be comparable, so stop here. return $issues; } if ($u['keys'] !== $v['keys']) { $issues[] = pht( 'The two configurations have different keys. This usually means '. 'that they are running different versions of Phabricator.'); } $uval = idx($u, 'values', array()); $vval = idx($v, 'values', array()); $all_keys = array_keys($uval + $vval); foreach ($all_keys as $key) { $uv = idx($uval, $key); $vv = idx($vval, $key); if ($uv !== $vv) { if ($uv && $vv) { $issues[] = pht( 'The configuration key "%s" is set in both configurations, but '. 'set to different values.', $key); } else { $issues[] = pht( 'The configuration key "%s" is set in only one configuration.', $key); } } } return $issues; } private static function getKeysForConsistencyCheck() { $keys = array_keys(self::getAllConfigKeys()); sort($keys); $skip_keys = self::getEnvConfig('phd.variant-config'); return array_diff($keys, $skip_keys); } /* -( Reading Configuration )---------------------------------------------- */ /** * Get the current configuration setting for a given key. * * If the key is not found, then throw an Exception. * * @task read */ public static function getEnvConfig($key) { if (isset(self::$cache[$key])) { return self::$cache[$key]; } if (array_key_exists($key, self::$cache)) { return self::$cache[$key]; } $result = self::$sourceStack->getKeys(array($key)); if (array_key_exists($key, $result)) { self::$cache[$key] = $result[$key]; return $result[$key]; } else { throw new Exception("No config value specified for key '{$key}'."); } } /** * Get the current configuration setting for a given key. If the key * does not exist, return a default value instead of throwing. This is * primarily useful for migrations involving keys which are slated for * removal. * * @task read */ public static function getEnvConfigIfExists($key, $default = null) { try { return self::getEnvConfig($key); } catch (Exception $ex) { return $default; } } /** * Get the fully-qualified URI for a path. * * @task read */ public static function getURI($path) { return rtrim(self::getAnyBaseURI(), '/').$path; } /** * Get the fully-qualified production URI for a path. * * @task read */ public static function getProductionURI($path) { // If we're passed a URI which already has a domain, simply return it // unmodified. In particular, files may have URIs which point to a CDN // domain. $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $production_domain = self::getEnvConfig('phabricator.production-uri'); if (!$production_domain) { $production_domain = self::getAnyBaseURI(); } return rtrim($production_domain, '/').$path; } public static function getAllowedURIs($path) { $uri = new PhutilURI($path); if ($uri->getDomain()) { return $path; } $allowed_uris = self::getEnvConfig('phabricator.allowed-uris'); $return = array(); foreach ($allowed_uris as $allowed_uri) { $return[] = rtrim($allowed_uri, '/').$path; } return $return; } /** * Get the fully-qualified production URI for a static resource path. * * @task read */ public static function getCDNURI($path) { $alt = self::getEnvConfig('security.alternate-file-domain'); if (!$alt) { $alt = self::getAnyBaseURI(); } $uri = new PhutilURI($alt); $uri->setPath($path); return (string)$uri; } /** * Get the fully-qualified production URI for a documentation resource. * * @task read */ public static function getDoclink($resource, $type = 'article') { $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/'); $uri->setQueryParam('name', $resource); $uri->setQueryParam('type', $type); $uri->setQueryParam('jump', true); return (string)$uri; } /** * Build a concrete object from a configuration key. * * @task read */ public static function newObjectFromConfig($key, $args = array()) { $class = self::getEnvConfig($key); return newv($class, $args); } public static function getAnyBaseURI() { $base_uri = self::getEnvConfig('phabricator.base-uri'); if (!$base_uri) { $base_uri = self::getRequestBaseURI(); } if (!$base_uri) { throw new Exception( "Define 'phabricator.base-uri' in your configuration to continue."); } return $base_uri; } public static function getRequestBaseURI() { return self::$requestBaseURI; } public static function setRequestBaseURI($uri) { self::$requestBaseURI = $uri; } /* -( Unit Test Support )-------------------------------------------------- */ /** * @task test */ public static function beginScopedEnv() { return new PhabricatorScopedEnv(self::pushTestEnvironment()); } /** * @task test */ private static function pushTestEnvironment() { self::dropConfigCache(); $source = new PhabricatorConfigDictionarySource(array()); self::$sourceStack->pushSource($source); return spl_object_hash($source); } /** * @task test */ public static function popTestEnvironment($key) { self::dropConfigCache(); $source = self::$sourceStack->popSource(); $stack_key = spl_object_hash($source); if ($stack_key !== $key) { self::$sourceStack->pushSource($source); throw new Exception( 'Scoped environments were destroyed in a diffent order than they '. 'were initialized.'); } } /* -( URI Validation )----------------------------------------------------- */ /** * Detect if a URI satisfies either @{method:isValidLocalWebResource} or * @{method:isValidRemoteWebResource}, i.e. is a page on this server or the * URI of some other resource which has a valid protocol. This rejects * garbage URIs and URIs with protocols which do not appear in the * ##uri.allowed-protocols## configuration, notably 'javascript:' URIs. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to put in an "href" link attribute. * * @param string URI to test. * @return bool True if the URI identifies a web resource. * @task uri */ public static function isValidWebResource($uri) { return self::isValidLocalWebResource($uri) || self::isValidRemoteWebResource($uri); } /** * Detect if a URI identifies some page on this server. * * NOTE: This method is generally intended to reject URIs which it may be * unsafe to issue a "Location:" redirect to. * * @param string URI to test. * @return bool True if the URI identifies a local page. * @task uri */ public static function isValidLocalWebResource($uri) { $uri = (string)$uri; if (!strlen($uri)) { return false; } if (preg_match('/\s/', $uri)) { // PHP hasn't been vulnerable to header injection attacks for a bunch of // years, but we can safely reject these anyway since they're never valid. return false; } // Chrome (at a minimum) interprets backslashes in Location headers and the // URL bar as forward slashes. This is probably intended to reduce user // error caused by confusion over which key is "forward slash" vs "back // slash". // // However, it means a URI like "/\evil.com" is interpreted like // "//evil.com", which is a protocol relative remote URI. // // Since we currently never generate URIs with backslashes in them, reject // these unconditionally rather than trying to figure out how browsers will // interpret them. if (preg_match('/\\\\/', $uri)) { return false; } // Valid URIs must begin with '/', followed by the end of the string or some // other non-'/' character. This rejects protocol-relative URIs like // "//evil.com/evil_stuff/". return (bool)preg_match('@^/([^/]|$)@', $uri); } /** * Detect if a URI identifies some valid remote resource. * * @param string URI to test. * @return bool True if a URI idenfies a remote resource with an allowed * protocol. * @task uri */ public static function isValidRemoteWebResource($uri) { $uri = (string)$uri; $proto = id(new PhutilURI($uri))->getProtocol(); if (!$proto) { return false; } $allowed = self::getEnvConfig('uri.allowed-protocols'); if (empty($allowed[$proto])) { return false; } return true; } public static function isClusterRemoteAddress() { $address = idx($_SERVER, 'REMOTE_ADDR'); if (!$address) { throw new Exception( pht( 'Unable to test remote address against cluster whitelist: '. 'REMOTE_ADDR is not defined.')); } return self::isClusterAddress($address); } public static function isClusterAddress($address) { $cluster_addresses = PhabricatorEnv::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { throw new Exception( pht( 'Phabricator is not configured to serve cluster requests. '. 'Set `cluster.addresses` in the configuration to whitelist '. 'cluster hosts before sending requests that use a cluster '. 'authentication mechanism.')); } return PhutilCIDRList::newList($cluster_addresses) ->containsAddress($address); } /* -( Internals )---------------------------------------------------------- */ /** * @task internal */ public static function envConfigExists($key) { return array_key_exists($key, self::$sourceStack->getKeys(array($key))); } /** * @task internal */ public static function getAllConfigKeys() { return self::$sourceStack->getAllKeys(); } public static function getConfigSourceStack() { return self::$sourceStack; } /** * @task internal */ public static function overrideTestEnvConfig($stack_key, $key, $value) { $tmp = array(); // If we don't have the right key, we'll throw when popping the last // source off the stack. do { $source = self::$sourceStack->popSource(); array_unshift($tmp, $source); if (spl_object_hash($source) == $stack_key) { $source->setKeys(array($key => $value)); break; } } while (true); foreach ($tmp as $source) { self::$sourceStack->pushSource($source); } self::dropConfigCache(); } private static function dropConfigCache() { self::$cache = array(); } }