diff --git a/src/applications/cache/PhabricatorCaches.php b/src/applications/cache/PhabricatorCaches.php index e9baa299a2..c9bd304ccf 100644 --- a/src/applications/cache/PhabricatorCaches.php +++ b/src/applications/cache/PhabricatorCaches.php @@ -1,361 +1,394 @@ setCaches($caches); } /* -( Request Cache )------------------------------------------------------ */ /** * Get a request cache stack. * * This cache stack is destroyed after each logical request. In particular, * it is destroyed periodically by the daemons, while `static` caches are * not. * * @return PhutilKeyValueCacheStack Request cache stack. */ public static function getRequestCache() { if (!self::$requestCache) { self::$requestCache = new PhutilInRequestKeyValueCache(); } return self::$requestCache; } /** * Destroy the request cache. * * This is called at the beginning of each logical request. * * @return void */ public static function destroyRequestCache() { self::$requestCache = null; } /* -( Immutable Cache )---------------------------------------------------- */ /** * Gets an immutable cache stack. * * This stack trades mutability away for improved performance. Normally, it is * APC + DB. * * In the general case with multiple web frontends, this stack can not be * cleared, so it is only appropriate for use if the value of a given key is * permanent and immutable. * * @return PhutilKeyValueCacheStack Best immutable stack available. * @task immutable */ public static function getImmutableCache() { static $cache; if (!$cache) { $caches = self::buildImmutableCaches(); $cache = self::newStackFromCaches($caches); } return $cache; } /** * Build the immutable cache stack. * * @return list List of caches. * @task immutable */ private static function buildImmutableCaches() { $caches = array(); $apc = new PhutilAPCKeyValueCache(); if ($apc->isAvailable()) { $caches[] = $apc; } $caches[] = new PhabricatorKeyValueDatabaseCache(); return $caches; } /* -( Repository Graph Cache )--------------------------------------------- */ public static function getRepositoryGraphL1Cache() { static $cache; if (!$cache) { $caches = self::buildRepositoryGraphL1Caches(); $cache = self::newStackFromCaches($caches); } return $cache; } private static function buildRepositoryGraphL1Caches() { $caches = array(); $request = new PhutilInRequestKeyValueCache(); $request->setLimit(32); $caches[] = $request; $apc = new PhutilAPCKeyValueCache(); if ($apc->isAvailable()) { $caches[] = $apc; } return $caches; } public static function getRepositoryGraphL2Cache() { static $cache; if (!$cache) { $caches = self::buildRepositoryGraphL2Caches(); $cache = self::newStackFromCaches($caches); } return $cache; } private static function buildRepositoryGraphL2Caches() { $caches = array(); $caches[] = new PhabricatorKeyValueDatabaseCache(); return $caches; } +/* -( Server State Cache )------------------------------------------------- */ + + + /** + * Highly specialized cache for storing server process state. + * + * We use this cache to track initial steps in the setup phase, before + * configuration is loaded. + * + * This cache does NOT use the cache namespace (it must be accessed before + * we build configuration), and is global across all instances on the host. + * + * @return PhutilKeyValueCacheStack Best available server state cache stack. + * @task setup + */ + public static function getServerStateCache() { + static $cache; + if (!$cache) { + $caches = self::buildSetupCaches('phabricator-server'); + + // NOTE: We are NOT adding a cache namespace here! This cache is shared + // across all instances on the host. + + $caches = self::addProfilerToCaches($caches); + $cache = id(new PhutilKeyValueCacheStack()) + ->setCaches($caches); + + } + return $cache; + } + + + /* -( Setup Cache )-------------------------------------------------------- */ /** * Highly specialized cache for performing setup checks. We use this cache * to determine if we need to run expensive setup checks when the page * loads. Without it, we would need to run these checks every time. * * Normally, this cache is just APC. In the absence of APC, this cache * degrades into a slow, quirky on-disk cache. * * NOTE: Do not use this cache for anything else! It is not a general-purpose * cache! * * @return PhutilKeyValueCacheStack Most qualified available cache stack. * @task setup */ public static function getSetupCache() { static $cache; if (!$cache) { - $caches = self::buildSetupCaches(); + $caches = self::buildSetupCaches('phabricator-setup'); $cache = self::newStackFromCaches($caches); } return $cache; } /** * @task setup */ - private static function buildSetupCaches() { + private static function buildSetupCaches($cache_name) { // If this is the CLI, just build a setup cache. if (php_sapi_name() == 'cli') { return array(); } // In most cases, we should have APC. This is an ideal cache for our // purposes -- it's fast and empties on server restart. $apc = new PhutilAPCKeyValueCache(); if ($apc->isAvailable()) { return array($apc); } // If we don't have APC, build a poor approximation on disk. This is still // much better than nothing; some setup steps are quite slow. - $disk_path = self::getSetupCacheDiskCachePath(); + $disk_path = self::getSetupCacheDiskCachePath($cache_name); if ($disk_path) { $disk = new PhutilOnDiskKeyValueCache(); $disk->setCacheFile($disk_path); $disk->setWait(0.1); if ($disk->isAvailable()) { return array($disk); } } return array(); } /** * @task setup */ - private static function getSetupCacheDiskCachePath() { + private static function getSetupCacheDiskCachePath($name) { // The difficulty here is in choosing a path which will change on server // restart (we MUST have this property), but as rarely as possible // otherwise (we desire this property to give the cache the best hit rate // we can). // Unfortunately, we don't have a very good strategy for minimizing the // churn rate of the cache. We previously tried to use the parent process // PID in some cases, but this was not reliable. See T9599 for one case of // this. $pid_basis = getmypid(); // If possible, we also want to know when the process launched, so we can // drop the cache if a process restarts but gets the same PID an earlier // process had. "/proc" is not available everywhere (e.g., not on OSX), but // check if we have it. $epoch_basis = null; $stat = @stat("/proc/{$pid_basis}"); if ($stat !== false) { $epoch_basis = $stat['ctime']; } $tmp_dir = sys_get_temp_dir(); - $tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.'phabricator-setup'; + $tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.$name; if (!file_exists($tmp_path)) { @mkdir($tmp_path); } $is_ok = self::testTemporaryDirectory($tmp_path); if (!$is_ok) { $tmp_path = $tmp_dir; $is_ok = self::testTemporaryDirectory($tmp_path); if (!$is_ok) { // We can't find anywhere to write the cache, so just bail. return null; } } $tmp_name = 'setup-'.$pid_basis; if ($epoch_basis) { $tmp_name .= '.'.$epoch_basis; } $tmp_name .= '.cache'; return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name; } /** * @task setup */ private static function testTemporaryDirectory($dir) { if (!@file_exists($dir)) { return false; } if (!@is_dir($dir)) { return false; } if (!@is_writable($dir)) { return false; } return true; } private static function addProfilerToCaches(array $caches) { foreach ($caches as $key => $cache) { $pcache = new PhutilKeyValueCacheProfiler($cache); $pcache->setProfiler(PhutilServiceProfiler::getInstance()); $caches[$key] = $pcache; } return $caches; } private static function addNamespaceToCaches(array $caches) { $namespace = self::getNamespace(); if (!$namespace) { return $caches; } foreach ($caches as $key => $cache) { $ncache = new PhutilKeyValueCacheNamespace($cache); $ncache->setNamespace($namespace); $caches[$key] = $ncache; } return $caches; } /** * Deflate a value, if deflation is available and has an impact. * * If the value is larger than 1KB, we have `gzdeflate()`, we successfully * can deflate it, and it benefits from deflation, we deflate it. Otherwise * we leave it as-is. * * Data can later be inflated with @{method:inflateData}. * * @param string String to attempt to deflate. * @return string|null Deflated string, or null if it was not deflated. * @task compress */ public static function maybeDeflateData($value) { $len = strlen($value); if ($len <= 1024) { return null; } if (!function_exists('gzdeflate')) { return null; } $deflated = gzdeflate($value); if ($deflated === false) { return null; } $deflated_len = strlen($deflated); if ($deflated_len >= ($len / 2)) { return null; } return $deflated; } /** * Inflate data previously deflated by @{method:maybeDeflateData}. * * @param string Deflated data, from @{method:maybeDeflateData}. * @return string Original, uncompressed data. * @task compress */ public static function inflateData($value) { if (!function_exists('gzinflate')) { throw new Exception( pht( '%s is not available; unable to read deflated data!', 'gzinflate()')); } $value = gzinflate($value); if ($value === false) { throw new Exception(pht('Failed to inflate data!')); } return $value; } } diff --git a/src/applications/config/check/PhabricatorSetupCheck.php b/src/applications/config/check/PhabricatorSetupCheck.php index 0c9888cd77..7947f5aa79 100644 --- a/src/applications/config/check/PhabricatorSetupCheck.php +++ b/src/applications/config/check/PhabricatorSetupCheck.php @@ -1,271 +1,275 @@ isPreflightCheck()) { return 0; } else { return 1000; } } /** * Should this check execute before we load configuration? * * The majority of checks (particularly, those checks which examine * configuration) should run in the normal setup phase, after configuration * loads. However, a small set of critical checks (mostly, tests for PHP * setup and extensions) need to run before we can load configuration. * * @return bool True to execute before configuration is loaded. */ public function isPreflightCheck() { return false; } final protected function newIssue($key) { $issue = id(new PhabricatorSetupIssue()) ->setIssueKey($key); $this->issues[$key] = $issue; if ($this->getDefaultGroup()) { $issue->setGroup($this->getDefaultGroup()); } return $issue; } final public function getIssues() { return $this->issues; } protected function addIssue(PhabricatorSetupIssue $issue) { $this->issues[$issue->getIssueKey()] = $issue; return $this; } public function getDefaultGroup() { return null; } final public function runSetupChecks() { $this->issues = array(); $this->executeChecks(); } final public static function getOpenSetupIssueKeys() { $cache = PhabricatorCaches::getSetupCache(); return $cache->getKey('phabricator.setup.issue-keys'); } final public static function setOpenSetupIssueKeys( array $keys, $update_database) { $cache = PhabricatorCaches::getSetupCache(); $cache->setKey('phabricator.setup.issue-keys', $keys); + $server_cache = PhabricatorCaches::getServerStateCache(); + $server_cache->setKey('phabricator.in-flight', 1); + if ($update_database) { $db_cache = new PhabricatorKeyValueDatabaseCache(); try { $json = phutil_json_encode($keys); $db_cache->setKey('phabricator.setup.issue-keys', $json); } catch (Exception $ex) { // Ignore any write failures, since they likely just indicate that we // have a database-related setup issue that needs to be resolved. } } } final public static function getOpenSetupIssueKeysFromDatabase() { $db_cache = new PhabricatorKeyValueDatabaseCache(); try { $value = $db_cache->getKey('phabricator.setup.issue-keys'); if (!strlen($value)) { return null; } return phutil_json_decode($value); } catch (Exception $ex) { return null; } } final public static function getUnignoredIssueKeys(array $all_issues) { assert_instances_of($all_issues, 'PhabricatorSetupIssue'); $keys = array(); foreach ($all_issues as $issue) { if (!$issue->getIsIgnored()) { $keys[] = $issue->getIssueKey(); } } return $keys; } final public static function getConfigNeedsRepair() { $cache = PhabricatorCaches::getSetupCache(); return $cache->getKey('phabricator.setup.needs-repair'); } final public static function setConfigNeedsRepair($needs_repair) { $cache = PhabricatorCaches::getSetupCache(); $cache->setKey('phabricator.setup.needs-repair', $needs_repair); } final public static function deleteSetupCheckCache() { $cache = PhabricatorCaches::getSetupCache(); $cache->deleteKeys( array( 'phabricator.setup.needs-repair', 'phabricator.setup.issue-keys', )); } final public static function willPreflightRequest() { $checks = self::loadAllChecks(); foreach ($checks as $check) { if (!$check->isPreflightCheck()) { continue; } $check->runSetupChecks(); foreach ($check->getIssues() as $key => $issue) { return self::newIssueResponse($issue); } } return null; } public static function newIssueResponse(PhabricatorSetupIssue $issue) { $view = id(new PhabricatorSetupIssueView()) ->setIssue($issue); return id(new PhabricatorConfigResponse()) ->setView($view); } final public static function willProcessRequest() { $issue_keys = self::getOpenSetupIssueKeys(); if ($issue_keys === null) { $issues = self::runNormalChecks(); foreach ($issues as $issue) { if ($issue->getIsFatal()) { return self::newIssueResponse($issue); } } $issue_keys = self::getUnignoredIssueKeys($issues); self::setOpenSetupIssueKeys($issue_keys, $update_database = true); } else if ($issue_keys) { // If Phabricator is configured in a cluster with multiple web devices, // we can end up with setup issues cached on every device. This can cause // a warning banner to show on every device so that each one needs to // be dismissed individually, which is pretty annoying. See T10876. // To avoid this, check if the issues we found have already been cleared // in the database. If they have, we'll just wipe out our own cache and // move on. $issue_keys = self::getOpenSetupIssueKeysFromDatabase(); if ($issue_keys !== null) { self::setOpenSetupIssueKeys($issue_keys, $update_database = false); } } // Try to repair configuration unless we have a clean bill of health on it. // We need to keep doing this on every page load until all the problems // are fixed, which is why it's separate from setup checks (which run // once per restart). $needs_repair = self::getConfigNeedsRepair(); if ($needs_repair !== false) { $needs_repair = self::repairConfig(); self::setConfigNeedsRepair($needs_repair); } } /** * Test if we've survived through setup on at least one normal request * without fataling. * * If we've made it through setup without hitting any fatals, we switch * to render a more friendly error page when encountering issues like * database connection failures. This gives users a smoother experience in * the face of intermittent failures. * * @return bool True if we've made it through setup since the last restart. */ final public static function isInFlight() { - return (self::getOpenSetupIssueKeys() !== null); + $cache = PhabricatorCaches::getServerStateCache(); + return (bool)$cache->getKey('phabricator.in-flight'); } final public static function loadAllChecks() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setSortMethod('getExecutionOrder') ->execute(); } final public static function runNormalChecks() { $checks = self::loadAllChecks(); foreach ($checks as $key => $check) { if ($check->isPreflightCheck()) { unset($checks[$key]); } } $issues = array(); foreach ($checks as $check) { $check->runSetupChecks(); foreach ($check->getIssues() as $key => $issue) { if (isset($issues[$key])) { throw new Exception( pht( "Two setup checks raised an issue with key '%s'!", $key)); } $issues[$key] = $issue; if ($issue->getIsFatal()) { break 2; } } } $ignore_issues = PhabricatorEnv::getEnvConfig('config.ignore-issues'); foreach ($ignore_issues as $ignorable => $derp) { if (isset($issues[$ignorable])) { $issues[$ignorable]->setIsIgnored(true); } } return $issues; } final public static function repairConfig() { $needs_repair = false; $options = PhabricatorApplicationConfigOptions::loadAllOptions(); foreach ($options as $option) { try { $option->getGroup()->validateOption( $option, PhabricatorEnv::getEnvConfig($option->getKey())); } catch (PhabricatorConfigValidationException $ex) { PhabricatorEnv::repairConfig($option->getKey(), $option->getDefault()); $needs_repair = true; } } return $needs_repair; } } diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 7ea00957ff..2dfe7d9c62 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -1,912 +1,912 @@ 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 extends Phobject { private static $sourceStack; private static $repairSource; private static $overrideSource; private static $requestBaseURI; private static $cache; private static $localeCode; private static $readOnly; private static $readOnlyReason; const READONLY_CONFIG = 'config'; const READONLY_UNREACHABLE = 'unreachable'; const READONLY_SEVERED = 'severed'; const READONLY_MASTERLESS = 'masterless'; /** * @phutil-external-symbol class PhabricatorStartup */ public static function initializeWebEnvironment() { self::initializeCommonEnvironment(false); } public static function initializeScriptEnvironment($config_optional) { self::initializeCommonEnvironment($config_optional); // 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($config_optional) { PhutilErrorHandler::initialize(); self::resetUmask(); self::buildConfigurationSourceStack($config_optional); // Force a valid timezone. If both PHP and Phabricator configuration are // invalid, use UTC. $tz = self::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 = self::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 = self::getEnvConfig('cluster.instance'); if (strlen($instance)) { putenv('PHABRICATOR_INSTANCE='.$instance); $_ENV['PHABRICATOR_INSTANCE'] = $instance; } PhabricatorEventEngine::initialize(); // TODO: Add a "locale.default" config option once we have some reasonable // defaults which aren't silly nonsense. self::setLocaleCode('en_US'); } public static function beginScopedLocale($locale_code) { return new PhabricatorLocaleScopeGuard($locale_code); } public static function getLocaleCode() { return self::$localeCode; } public static function setLocaleCode($locale_code) { if (!$locale_code) { return; } if ($locale_code == self::$localeCode) { return; } try { $locale = PhutilLocale::loadLocale($locale_code); $translations = PhutilTranslation::getTranslationMapForLocale( $locale_code); $override = self::getEnvConfig('translation.override'); if (!is_array($override)) { $override = array(); } PhutilTranslator::getInstance() ->setLocale($locale) ->setTranslations($override + $translations); self::$localeCode = $locale_code; } catch (Exception $ex) { // Just ignore this; the user likely has an out-of-date locale code. } } private static function buildConfigurationSourceStack($config_optional) { 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 (self::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 PhutilClassMapQuery()) ->setAncestorClass('PhabricatorConfigSiteSource') ->setSortMethod('getPriority') ->execute(); foreach ($site_sources as $site_source) { $stack->pushSource($site_source); } $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); if (!$master) { self::setReadOnly(true, self::READONLY_MASTERLESS); } else if ($master->isSevered()) { $master->checkHealth(); if ($master->isSevered()) { self::setReadOnly(true, self::READONLY_SEVERED); } } try { $stack->pushSource( id(new PhabricatorConfigDatabaseSource('default')) ->setName(pht('Database'))); } catch (AphrontSchemaQueryException $exception) { // If the database is not available, just skip this configuration // source. This happens during `bin/storage upgrade`, `bin/conf` before // schema setup, etc. } catch (AphrontConnectionQueryException $ex) { if (!$config_optional) { throw $ex; } } catch (AphrontInvalidCredentialsQueryException $ex) { if (!$config_optional) { throw $ex; } } } 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; } /* -( 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]; - } - if (!self::$sourceStack) { throw new Exception( pht( 'Trying to read configuration "%s" before configuration has been '. 'initialized.', $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( pht( "No config value specified for key '%s'.", $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( pht( "Define '%s' in your configuration to continue.", 'phabricator.base-uri')); } return $base_uri; } public static function getRequestBaseURI() { return self::$requestBaseURI; } public static function setRequestBaseURI($uri) { self::$requestBaseURI = $uri; } public static function isReadOnly() { if (self::$readOnly !== null) { return self::$readOnly; } return self::getEnvConfig('cluster.read-only'); } public static function setReadOnly($read_only, $reason) { self::$readOnly = $read_only; self::$readOnlyReason = $reason; } public static function getReadOnlyMessage() { $reason = self::getReadOnlyReason(); switch ($reason) { case self::READONLY_MASTERLESS: return pht( 'Phabricator is in read-only mode (no writable database '. 'is configured).'); case self::READONLY_UNREACHABLE: return pht( 'Phabricator is in read-only mode (unreachable master).'); case self::READONLY_SEVERED: return pht( 'Phabricator is in read-only mode (major interruption).'); } return pht('Phabricator is in read-only mode.'); } public static function getReadOnlyURI() { return urisprintf( '/readonly/%s/', self::getReadOnlyReason()); } public static function getReadOnlyReason() { if (!self::isReadOnly()) { return null; } if (self::$readOnlyReason !== null) { return self::$readOnlyReason; } return self::READONLY_CONFIG; } /* -( 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( pht( 'Scoped environments were destroyed in a different order than they '. 'were initialized.')); } } /* -( URI Validation )----------------------------------------------------- */ /** * Detect if a URI satisfies either @{method:isValidLocalURIForLink} or * @{method:isValidRemoteURIForLink}, 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 isValidURIForLink($uri) { return self::isValidLocalURIForLink($uri) || self::isValidRemoteURIForLink($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 isValidLocalURIForLink($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 linkable 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 isValidRemoteURIForLink($uri) { try { self::requireValidRemoteURIForLink($uri); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid linkable remote resource, throwing a * detailed message if it does not. * * A valid linkable remote resource can be safely linked or redirected to. * This is primarily a protocol whitelist check. * * @param string URI to test. * @return void * @task uri */ public static function requireValidRemoteURIForLink($raw_uri) { $uri = new PhutilURI($raw_uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a protocol.', $raw_uri)); } $protocols = self::getEnvConfig('uri.allowed-protocols'); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must use one of these protocols: %s.', $raw_uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid linkable resource. A valid linkable '. 'resource URI must specify a domain.', $raw_uri)); } } /** * Detect if a URI identifies a valid fetchable remote resource. * * @param string URI to test. * @param list Allowed protocols. * @return bool True if the URI is a valid fetchable remote resource. * @task uri */ public static function isValidRemoteURIForFetch($uri, array $protocols) { try { self::requireValidRemoteURIForFetch($uri, $protocols); return true; } catch (Exception $ex) { return false; } } /** * Detect if a URI identifies a valid fetchable remote resource, throwing * a detailed message if it does not. * * A valid fetchable remote resource can be safely fetched using a request * originating on this server. This is a primarily an address check against * the outbound address blacklist. * * @param string URI to test. * @param list Allowed protocols. * @return pair Pre-resolved URI and domain. * @task uri */ public static function requireValidRemoteURIForFetch( $uri, array $protocols) { $uri = new PhutilURI($uri); $proto = $uri->getProtocol(); if (!strlen($proto)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a protocol.', $uri)); } $protocols = array_fuse($protocols); if (!isset($protocols[$proto])) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must use one of these protocols: %s.', $uri, implode(', ', array_keys($protocols)))); } $domain = $uri->getDomain(); if (!strlen($domain)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. A valid fetchable '. 'resource URI must specify a domain.', $uri)); } $addresses = gethostbynamel($domain); if (!$addresses) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" could '. 'not be resolved.', $uri, $domain)); } foreach ($addresses as $address) { if (self::isBlacklistedOutboundAddress($address)) { throw new Exception( pht( 'URI "%s" is not a valid fetchable resource. The domain "%s" '. 'resolves to the address "%s", which is blacklisted for '. 'outbound requests.', $uri, $domain, $address)); } } $resolved_uri = clone $uri; $resolved_uri->setDomain(head($addresses)); return array($resolved_uri, $domain); } /** * Determine if an IP address is in the outbound address blacklist. * * @param string IP address. * @return bool True if the address is blacklisted. */ public static function isBlacklistedOutboundAddress($address) { $blacklist = self::getEnvConfig('security.outbound-blacklist'); return PhutilCIDRList::newList($blacklist)->containsAddress($address); } public static function isClusterRemoteAddress() { $cluster_addresses = self::getEnvConfig('cluster.addresses'); if (!$cluster_addresses) { return false; } $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 = self::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(); } private static function resetUmask() { // Reset the umask to the common standard umask. The umask controls default // permissions when files are created and propagates to subprocesses. // "022" is the most common umask, but sometimes it is set to something // unusual by the calling environment. // Since various things rely on this umask to work properly and we are // not aware of any legitimate reasons to adjust it, unconditionally // normalize it until such reasons arise. See T7475 for discussion. umask(022); } /** * Get the path to an empty directory which is readable by all of the system * user accounts that Phabricator acts as. * * In some cases, a binary needs some valid HOME or CWD to continue, but not * all user accounts have valid home directories and even if they do they * may not be readable after a `sudo` operation. * * @return string Path to an empty directory suitable for use as a CWD. */ public static function getEmptyCWD() { $root = dirname(phutil_get_library_root('phabricator')); return $root.'/support/empty/'; } }