Path: blob/master/src/infrastructure/env/PhabricatorEnv.php
12241 views
<?php12/**3* Manages the execution environment configuration, exposing APIs to read4* configuration settings and other similar values that are derived directly5* from configuration settings.6*7*8* = Reading Configuration =9*10* The primary role of this class is to provide an API for reading11* Phabricator configuration, @{method:getEnvConfig}:12*13* $value = PhabricatorEnv::getEnvConfig('some.key', $default);14*15* The class also handles some URI construction based on configuration, via16* the methods @{method:getURI}, @{method:getProductionURI},17* @{method:getCDNURI}, and @{method:getDoclink}.18*19* For configuration which allows you to choose a class to be responsible for20* some functionality (e.g., which mail adapter to use to deliver email),21* @{method:newObjectFromConfig} provides a simple interface that validates22* the configured value.23*24*25* = Unit Test Support =26*27* In unit tests, you can use @{method:beginScopedEnv} to create a temporary,28* mutable environment. The method returns a scope guard object which restores29* the environment when it is destroyed. For example:30*31* public function testExample() {32* $env = PhabricatorEnv::beginScopedEnv();33* $env->overrideEnv('some.key', 'new-value-for-this-test');34*35* // Some test which depends on the value of 'some.key'.36*37* }38*39* Your changes will persist until the `$env` object leaves scope or is40* destroyed.41*42* You should //not// use this in normal code.43*44*45* @task read Reading Configuration46* @task uri URI Validation47* @task test Unit Test Support48* @task internal Internals49*/50final class PhabricatorEnv extends Phobject {5152private static $sourceStack;53private static $repairSource;54private static $overrideSource;55private static $requestBaseURI;56private static $cache;57private static $localeCode;58private static $readOnly;59private static $readOnlyReason;6061const READONLY_CONFIG = 'config';62const READONLY_UNREACHABLE = 'unreachable';63const READONLY_SEVERED = 'severed';64const READONLY_MASTERLESS = 'masterless';6566/**67* @phutil-external-symbol class PhabricatorStartup68*/69public static function initializeWebEnvironment() {70self::initializeCommonEnvironment(false);71}7273public static function initializeScriptEnvironment($config_optional) {74self::initializeCommonEnvironment($config_optional);7576// NOTE: This is dangerous in general, but we know we're in a script context77// and are not vulnerable to CSRF.78AphrontWriteGuard::allowDangerousUnguardedWrites(true);7980// There are several places where we log information (about errors, events,81// service calls, etc.) for analysis via DarkConsole or similar. These are82// useful for web requests, but grow unboundedly in long-running scripts and83// daemons. Discard data as it arrives in these cases.84PhutilServiceProfiler::getInstance()->enableDiscardMode();85DarkConsoleErrorLogPluginAPI::enableDiscardMode();86DarkConsoleEventPluginAPI::enableDiscardMode();87}888990private static function initializeCommonEnvironment($config_optional) {91PhutilErrorHandler::initialize();9293self::resetUmask();94self::buildConfigurationSourceStack($config_optional);9596// Force a valid timezone. If both PHP and Phabricator configuration are97// invalid, use UTC.98$tz = self::getEnvConfig('phabricator.timezone');99if ($tz) {100@date_default_timezone_set($tz);101}102$ok = @date_default_timezone_set(date_default_timezone_get());103if (!$ok) {104date_default_timezone_set('UTC');105}106107// Prepend '/support/bin' and append any paths to $PATH if we need to.108$env_path = getenv('PATH');109$phabricator_path = dirname(phutil_get_library_root('phabricator'));110$support_path = $phabricator_path.'/support/bin';111$env_path = $support_path.PATH_SEPARATOR.$env_path;112$append_dirs = self::getEnvConfig('environment.append-paths');113if (!empty($append_dirs)) {114$append_path = implode(PATH_SEPARATOR, $append_dirs);115$env_path = $env_path.PATH_SEPARATOR.$append_path;116}117putenv('PATH='.$env_path);118119// Write this back into $_ENV, too, so ExecFuture picks it up when creating120// subprocess environments.121$_ENV['PATH'] = $env_path;122123124// If an instance identifier is defined, write it into the environment so125// it's available to subprocesses.126$instance = self::getEnvConfig('cluster.instance');127if ($instance !== null && strlen($instance)) {128putenv('PHABRICATOR_INSTANCE='.$instance);129$_ENV['PHABRICATOR_INSTANCE'] = $instance;130}131132PhabricatorEventEngine::initialize();133134// TODO: Add a "locale.default" config option once we have some reasonable135// defaults which aren't silly nonsense.136self::setLocaleCode('en_US');137138// Load the preamble utility library if we haven't already. On web139// requests this loaded earlier, but we want to load it for non-web140// requests so that unit tests can call these functions.141require_once $phabricator_path.'/support/startup/preamble-utils.php';142}143144public static function beginScopedLocale($locale_code) {145return new PhabricatorLocaleScopeGuard($locale_code);146}147148public static function getLocaleCode() {149return self::$localeCode;150}151152public static function setLocaleCode($locale_code) {153if (!$locale_code) {154return;155}156157if ($locale_code == self::$localeCode) {158return;159}160161try {162$locale = PhutilLocale::loadLocale($locale_code);163$translations = PhutilTranslation::getTranslationMapForLocale(164$locale_code);165166$override = self::getEnvConfig('translation.override');167if (!is_array($override)) {168$override = array();169}170171PhutilTranslator::getInstance()172->setLocale($locale)173->setTranslations($override + $translations);174175self::$localeCode = $locale_code;176} catch (Exception $ex) {177// Just ignore this; the user likely has an out-of-date locale code.178}179}180181private static function buildConfigurationSourceStack($config_optional) {182self::dropConfigCache();183184$stack = new PhabricatorConfigStackSource();185self::$sourceStack = $stack;186187$default_source = id(new PhabricatorConfigDefaultSource())188->setName(pht('Global Default'));189$stack->pushSource($default_source);190191$env = self::getSelectedEnvironmentName();192if ($env) {193$stack->pushSource(194id(new PhabricatorConfigFileSource($env))195->setName(pht("File '%s'", $env)));196}197198$stack->pushSource(199id(new PhabricatorConfigLocalSource())200->setName(pht('Local Config')));201202// If the install overrides the database adapter, we might need to load203// the database adapter class before we can push on the database config.204// This config is locked and can't be edited from the web UI anyway.205foreach (self::getEnvConfig('load-libraries') as $library) {206phutil_load_library($library);207}208209// Drop any class map caches, since they will have generated without210// any classes from libraries. Without this, preflight setup checks can211// cause generation of a setup check cache that omits checks defined in212// libraries, for example.213PhutilClassMapQuery::deleteCaches();214215// If custom libraries specify config options, they won't get default216// values as the Default source has already been loaded, so we get it to217// pull in all options from non-phabricator libraries now they are loaded.218$default_source->loadExternalOptions();219220// If this install has site config sources, load them now.221$site_sources = id(new PhutilClassMapQuery())222->setAncestorClass('PhabricatorConfigSiteSource')223->setSortMethod('getPriority')224->execute();225226foreach ($site_sources as $site_source) {227$stack->pushSource($site_source);228229// If the site source did anything which reads config, throw it away230// to make sure any additional site sources get clean reads.231self::dropConfigCache();232}233234$masters = PhabricatorDatabaseRef::getMasterDatabaseRefs();235if (!$masters) {236self::setReadOnly(true, self::READONLY_MASTERLESS);237} else {238// If any master is severed, we drop to readonly mode. In theory we239// could try to continue if we're only missing some applications, but240// this is very complex and we're unlikely to get it right.241242foreach ($masters as $master) {243// Give severed masters one last chance to get healthy.244if ($master->isSevered()) {245$master->checkHealth();246}247248if ($master->isSevered()) {249self::setReadOnly(true, self::READONLY_SEVERED);250break;251}252}253}254255try {256// See T13403. If we're starting up in "config optional" mode, suppress257// messages about connection retries.258if ($config_optional) {259$database_source = @new PhabricatorConfigDatabaseSource('default');260} else {261$database_source = new PhabricatorConfigDatabaseSource('default');262}263264$database_source->setName(pht('Database'));265266$stack->pushSource($database_source);267} catch (AphrontSchemaQueryException $exception) {268// If the database is not available, just skip this configuration269// source. This happens during `bin/storage upgrade`, `bin/conf` before270// schema setup, etc.271} catch (PhabricatorClusterStrandedException $ex) {272// This means we can't connect to any database host. That's fine as273// long as we're running a setup script like `bin/storage`.274if (!$config_optional) {275throw $ex;276}277}278279// Drop the config cache one final time to make sure we're getting clean280// reads now that we've finished building the stack.281self::dropConfigCache();282}283284public static function repairConfig($key, $value) {285if (!self::$repairSource) {286self::$repairSource = id(new PhabricatorConfigDictionarySource(array()))287->setName(pht('Repaired Config'));288self::$sourceStack->pushSource(self::$repairSource);289}290self::$repairSource->setKeys(array($key => $value));291self::dropConfigCache();292}293294public static function overrideConfig($key, $value) {295if (!self::$overrideSource) {296self::$overrideSource = id(new PhabricatorConfigDictionarySource(array()))297->setName(pht('Overridden Config'));298self::$sourceStack->pushSource(self::$overrideSource);299}300self::$overrideSource->setKeys(array($key => $value));301self::dropConfigCache();302}303304public static function getUnrepairedEnvConfig($key, $default = null) {305foreach (self::$sourceStack->getStack() as $source) {306if ($source === self::$repairSource) {307continue;308}309$result = $source->getKeys(array($key));310if ($result) {311return $result[$key];312}313}314return $default;315}316317public static function getSelectedEnvironmentName() {318$env_var = 'PHABRICATOR_ENV';319320$env = idx($_SERVER, $env_var);321322if (!$env) {323$env = getenv($env_var);324}325326if (!$env) {327$env = idx($_ENV, $env_var);328}329330if (!$env) {331$root = dirname(phutil_get_library_root('phabricator'));332$path = $root.'/conf/local/ENVIRONMENT';333if (Filesystem::pathExists($path)) {334$env = trim(Filesystem::readFile($path));335}336}337338return $env;339}340341342/* -( Reading Configuration )---------------------------------------------- */343344345/**346* Get the current configuration setting for a given key.347*348* If the key is not found, then throw an Exception.349*350* @task read351*/352public static function getEnvConfig($key) {353if (!self::$sourceStack) {354throw new Exception(355pht(356'Trying to read configuration "%s" before configuration has been '.357'initialized.',358$key));359}360361if (isset(self::$cache[$key])) {362return self::$cache[$key];363}364365if (array_key_exists($key, self::$cache)) {366return self::$cache[$key];367}368369$result = self::$sourceStack->getKeys(array($key));370if (array_key_exists($key, $result)) {371self::$cache[$key] = $result[$key];372return $result[$key];373} else {374throw new Exception(375pht(376"No config value specified for key '%s'.",377$key));378}379}380381/**382* Get the current configuration setting for a given key. If the key383* does not exist, return a default value instead of throwing. This is384* primarily useful for migrations involving keys which are slated for385* removal.386*387* @task read388*/389public static function getEnvConfigIfExists($key, $default = null) {390try {391return self::getEnvConfig($key);392} catch (Exception $ex) {393return $default;394}395}396397398/**399* Get the fully-qualified URI for a path.400*401* @task read402*/403public static function getURI($path) {404return rtrim(self::getAnyBaseURI(), '/').$path;405}406407408/**409* Get the fully-qualified production URI for a path.410*411* @task read412*/413public static function getProductionURI($path) {414// If we're passed a URI which already has a domain, simply return it415// unmodified. In particular, files may have URIs which point to a CDN416// domain.417$uri = new PhutilURI($path);418if ($uri->getDomain()) {419return $path;420}421422$production_domain = self::getEnvConfig('phabricator.production-uri');423if (!$production_domain) {424$production_domain = self::getAnyBaseURI();425}426return rtrim($production_domain, '/').$path;427}428429430public static function isSelfURI($raw_uri) {431$uri = new PhutilURI($raw_uri);432433$host = $uri->getDomain();434if (!strlen($host)) {435return false;436}437438$host = phutil_utf8_strtolower($host);439440$self_map = self::getSelfURIMap();441return isset($self_map[$host]);442}443444private static function getSelfURIMap() {445$self_uris = array();446$self_uris[] = self::getProductionURI('/');447$self_uris[] = self::getURI('/');448449$allowed_uris = self::getEnvConfig('phabricator.allowed-uris');450foreach ($allowed_uris as $allowed_uri) {451$self_uris[] = $allowed_uri;452}453454$self_map = array();455foreach ($self_uris as $self_uri) {456$host = id(new PhutilURI($self_uri))->getDomain();457if (!strlen($host)) {458continue;459}460461$host = phutil_utf8_strtolower($host);462$self_map[$host] = $host;463}464465return $self_map;466}467468/**469* Get the fully-qualified production URI for a static resource path.470*471* @task read472*/473public static function getCDNURI($path) {474$alt = self::getEnvConfig('security.alternate-file-domain');475if (!$alt) {476$alt = self::getAnyBaseURI();477}478$uri = new PhutilURI($alt);479$uri->setPath($path);480return (string)$uri;481}482483484/**485* Get the fully-qualified production URI for a documentation resource.486*487* @task read488*/489public static function getDoclink($resource, $type = 'article') {490$params = array(491'name' => $resource,492'type' => $type,493'jump' => true,494);495496$uri = new PhutilURI(497'https://secure.phabricator.com/diviner/find/',498$params);499500return phutil_string_cast($uri);501}502503504/**505* Build a concrete object from a configuration key.506*507* @task read508*/509public static function newObjectFromConfig($key, $args = array()) {510$class = self::getEnvConfig($key);511return newv($class, $args);512}513514public static function getAnyBaseURI() {515$base_uri = self::getEnvConfig('phabricator.base-uri');516517if (!$base_uri) {518$base_uri = self::getRequestBaseURI();519}520521if (!$base_uri) {522throw new Exception(523pht(524"Define '%s' in your configuration to continue.",525'phabricator.base-uri'));526}527528return $base_uri;529}530531public static function getRequestBaseURI() {532return self::$requestBaseURI;533}534535public static function setRequestBaseURI($uri) {536self::$requestBaseURI = $uri;537}538539public static function isReadOnly() {540if (self::$readOnly !== null) {541return self::$readOnly;542}543return self::getEnvConfig('cluster.read-only');544}545546public static function setReadOnly($read_only, $reason) {547self::$readOnly = $read_only;548self::$readOnlyReason = $reason;549}550551public static function getReadOnlyMessage() {552$reason = self::getReadOnlyReason();553switch ($reason) {554case self::READONLY_MASTERLESS:555return pht(556'This server is in read-only mode (no writable database '.557'is configured).');558case self::READONLY_UNREACHABLE:559return pht(560'This server is in read-only mode (unreachable master).');561case self::READONLY_SEVERED:562return pht(563'This server is in read-only mode (major interruption).');564}565566return pht('This server is in read-only mode.');567}568569public static function getReadOnlyURI() {570return urisprintf(571'/readonly/%s/',572self::getReadOnlyReason());573}574575public static function getReadOnlyReason() {576if (!self::isReadOnly()) {577return null;578}579580if (self::$readOnlyReason !== null) {581return self::$readOnlyReason;582}583584return self::READONLY_CONFIG;585}586587588/* -( Unit Test Support )-------------------------------------------------- */589590591/**592* @task test593*/594public static function beginScopedEnv() {595return new PhabricatorScopedEnv(self::pushTestEnvironment());596}597598599/**600* @task test601*/602private static function pushTestEnvironment() {603self::dropConfigCache();604$source = new PhabricatorConfigDictionarySource(array());605self::$sourceStack->pushSource($source);606return spl_object_hash($source);607}608609610/**611* @task test612*/613public static function popTestEnvironment($key) {614self::dropConfigCache();615$source = self::$sourceStack->popSource();616$stack_key = spl_object_hash($source);617if ($stack_key !== $key) {618self::$sourceStack->pushSource($source);619throw new Exception(620pht(621'Scoped environments were destroyed in a different order than they '.622'were initialized.'));623}624}625626627/* -( URI Validation )----------------------------------------------------- */628629630/**631* Detect if a URI satisfies either @{method:isValidLocalURIForLink} or632* @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the633* URI of some other resource which has a valid protocol. This rejects634* garbage URIs and URIs with protocols which do not appear in the635* `uri.allowed-protocols` configuration, notably 'javascript:' URIs.636*637* NOTE: This method is generally intended to reject URIs which it may be638* unsafe to put in an "href" link attribute.639*640* @param string URI to test.641* @return bool True if the URI identifies a web resource.642* @task uri643*/644public static function isValidURIForLink($uri) {645return self::isValidLocalURIForLink($uri) ||646self::isValidRemoteURIForLink($uri);647}648649650/**651* Detect if a URI identifies some page on this server.652*653* NOTE: This method is generally intended to reject URIs which it may be654* unsafe to issue a "Location:" redirect to.655*656* @param string URI to test.657* @return bool True if the URI identifies a local page.658* @task uri659*/660public static function isValidLocalURIForLink($uri) {661$uri = (string)$uri;662663if (!strlen($uri)) {664return false;665}666667if (preg_match('/\s/', $uri)) {668// PHP hasn't been vulnerable to header injection attacks for a bunch of669// years, but we can safely reject these anyway since they're never valid.670return false;671}672673// Chrome (at a minimum) interprets backslashes in Location headers and the674// URL bar as forward slashes. This is probably intended to reduce user675// error caused by confusion over which key is "forward slash" vs "back676// slash".677//678// However, it means a URI like "/\evil.com" is interpreted like679// "//evil.com", which is a protocol relative remote URI.680//681// Since we currently never generate URIs with backslashes in them, reject682// these unconditionally rather than trying to figure out how browsers will683// interpret them.684if (preg_match('/\\\\/', $uri)) {685return false;686}687688// Valid URIs must begin with '/', followed by the end of the string or some689// other non-'/' character. This rejects protocol-relative URIs like690// "//evil.com/evil_stuff/".691return (bool)preg_match('@^/([^/]|$)@', $uri);692}693694695/**696* Detect if a URI identifies some valid linkable remote resource.697*698* @param string URI to test.699* @return bool True if a URI identifies a remote resource with an allowed700* protocol.701* @task uri702*/703public static function isValidRemoteURIForLink($uri) {704try {705self::requireValidRemoteURIForLink($uri);706return true;707} catch (Exception $ex) {708return false;709}710}711712713/**714* Detect if a URI identifies a valid linkable remote resource, throwing a715* detailed message if it does not.716*717* A valid linkable remote resource can be safely linked or redirected to.718* This is primarily a protocol whitelist check.719*720* @param string URI to test.721* @return void722* @task uri723*/724public static function requireValidRemoteURIForLink($raw_uri) {725$uri = new PhutilURI($raw_uri);726727$proto = $uri->getProtocol();728if (!strlen($proto)) {729throw new Exception(730pht(731'URI "%s" is not a valid linkable resource. A valid linkable '.732'resource URI must specify a protocol.',733$raw_uri));734}735736$protocols = self::getEnvConfig('uri.allowed-protocols');737if (!isset($protocols[$proto])) {738throw new Exception(739pht(740'URI "%s" is not a valid linkable resource. A valid linkable '.741'resource URI must use one of these protocols: %s.',742$raw_uri,743implode(', ', array_keys($protocols))));744}745746$domain = $uri->getDomain();747if (!strlen($domain)) {748throw new Exception(749pht(750'URI "%s" is not a valid linkable resource. A valid linkable '.751'resource URI must specify a domain.',752$raw_uri));753}754}755756757/**758* Detect if a URI identifies a valid fetchable remote resource.759*760* @param string URI to test.761* @param list<string> Allowed protocols.762* @return bool True if the URI is a valid fetchable remote resource.763* @task uri764*/765public static function isValidRemoteURIForFetch($uri, array $protocols) {766try {767self::requireValidRemoteURIForFetch($uri, $protocols);768return true;769} catch (Exception $ex) {770return false;771}772}773774775/**776* Detect if a URI identifies a valid fetchable remote resource, throwing777* a detailed message if it does not.778*779* A valid fetchable remote resource can be safely fetched using a request780* originating on this server. This is a primarily an address check against781* the outbound address blacklist.782*783* @param string URI to test.784* @param list<string> Allowed protocols.785* @return pair<string, string> Pre-resolved URI and domain.786* @task uri787*/788public static function requireValidRemoteURIForFetch(789$raw_uri,790array $protocols) {791792$uri = new PhutilURI($raw_uri);793794$proto = $uri->getProtocol();795if (!strlen($proto)) {796throw new Exception(797pht(798'URI "%s" is not a valid fetchable resource. A valid fetchable '.799'resource URI must specify a protocol.',800$raw_uri));801}802803$protocols = array_fuse($protocols);804if (!isset($protocols[$proto])) {805throw new Exception(806pht(807'URI "%s" is not a valid fetchable resource. A valid fetchable '.808'resource URI must use one of these protocols: %s.',809$raw_uri,810implode(', ', array_keys($protocols))));811}812813$domain = $uri->getDomain();814if (!strlen($domain)) {815throw new Exception(816pht(817'URI "%s" is not a valid fetchable resource. A valid fetchable '.818'resource URI must specify a domain.',819$raw_uri));820}821822$addresses = gethostbynamel($domain);823if (!$addresses) {824throw new Exception(825pht(826'URI "%s" is not a valid fetchable resource. The domain "%s" could '.827'not be resolved.',828$raw_uri,829$domain));830}831832foreach ($addresses as $address) {833if (self::isBlacklistedOutboundAddress($address)) {834throw new Exception(835pht(836'URI "%s" is not a valid fetchable resource. The domain "%s" '.837'resolves to the address "%s", which is blacklisted for '.838'outbound requests.',839$raw_uri,840$domain,841$address));842}843}844845$resolved_uri = clone $uri;846$resolved_uri->setDomain(head($addresses));847848return array($resolved_uri, $domain);849}850851852/**853* Determine if an IP address is in the outbound address blacklist.854*855* @param string IP address.856* @return bool True if the address is blacklisted.857*/858public static function isBlacklistedOutboundAddress($address) {859$blacklist = self::getEnvConfig('security.outbound-blacklist');860861return PhutilCIDRList::newList($blacklist)->containsAddress($address);862}863864public static function isClusterRemoteAddress() {865$cluster_addresses = self::getEnvConfig('cluster.addresses');866if (!$cluster_addresses) {867return false;868}869870$address = self::getRemoteAddress();871if (!$address) {872throw new Exception(873pht(874'Unable to test remote address against cluster whitelist: '.875'REMOTE_ADDR is not defined or not valid.'));876}877878return self::isClusterAddress($address);879}880881public static function isClusterAddress($address) {882$cluster_addresses = self::getEnvConfig('cluster.addresses');883if (!$cluster_addresses) {884throw new Exception(885pht(886'This server is not configured to serve cluster requests. '.887'Set `cluster.addresses` in the configuration to whitelist '.888'cluster hosts before sending requests that use a cluster '.889'authentication mechanism.'));890}891892return PhutilCIDRList::newList($cluster_addresses)893->containsAddress($address);894}895896public static function getRemoteAddress() {897$address = idx($_SERVER, 'REMOTE_ADDR');898if (!$address) {899return null;900}901902try {903return PhutilIPAddress::newAddress($address);904} catch (Exception $ex) {905return null;906}907}908909/* -( Internals )---------------------------------------------------------- */910911912/**913* @task internal914*/915public static function envConfigExists($key) {916return array_key_exists($key, self::$sourceStack->getKeys(array($key)));917}918919920/**921* @task internal922*/923public static function getAllConfigKeys() {924return self::$sourceStack->getAllKeys();925}926927public static function getConfigSourceStack() {928return self::$sourceStack;929}930931/**932* @task internal933*/934public static function overrideTestEnvConfig($stack_key, $key, $value) {935$tmp = array();936937// If we don't have the right key, we'll throw when popping the last938// source off the stack.939do {940$source = self::$sourceStack->popSource();941array_unshift($tmp, $source);942if (spl_object_hash($source) == $stack_key) {943$source->setKeys(array($key => $value));944break;945}946} while (true);947948foreach ($tmp as $source) {949self::$sourceStack->pushSource($source);950}951952self::dropConfigCache();953}954955private static function dropConfigCache() {956self::$cache = array();957}958959private static function resetUmask() {960// Reset the umask to the common standard umask. The umask controls default961// permissions when files are created and propagates to subprocesses.962963// "022" is the most common umask, but sometimes it is set to something964// unusual by the calling environment.965966// Since various things rely on this umask to work properly and we are967// not aware of any legitimate reasons to adjust it, unconditionally968// normalize it until such reasons arise. See T7475 for discussion.969umask(022);970}971972973/**974* Get the path to an empty directory which is readable by all of the system975* user accounts that Phabricator acts as.976*977* In some cases, a binary needs some valid HOME or CWD to continue, but not978* all user accounts have valid home directories and even if they do they979* may not be readable after a `sudo` operation.980*981* @return string Path to an empty directory suitable for use as a CWD.982*/983public static function getEmptyCWD() {984$root = dirname(phutil_get_library_root('phabricator'));985return $root.'/support/empty/';986}987988989}990991992