Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2992424
PhabricatorEnv.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
28 KB
Referenced Files
None
Subscribers
None
PhabricatorEnv.php
View Options
<?php
/**
* Manages the execution environment configuration, exposing APIs to read
* configuration settings and other similar values that are derived directly
* from configuration settings.
*
*
* = Reading Configuration =
*
* The primary role of this class is to provide an API for reading
* Phabricator configuration, @{method:getEnvConfig}:
*
* $value = PhabricatorEnv::getEnvConfig('some.key', $default);
*
* The class also handles some URI construction based on configuration, via
* the methods @{method:getURI}, @{method:getProductionURI},
* @{method:getCDNURI}, and @{method:getDoclink}.
*
* For configuration which allows you to choose a class to be responsible for
* some functionality (e.g., which mail adapter to use to deliver email),
* @{method:newObjectFromConfig} provides a simple interface that validates
* the configured value.
*
*
* = Unit Test Support =
*
* In unit tests, you can use @{method:beginScopedEnv} to create a temporary,
* mutable environment. The method returns a scope guard object which restores
* the environment when it is destroyed. For example:
*
* public function testExample() {
* $env = PhabricatorEnv::beginScopedEnv();
* $env->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
(
phutil_nonempty_string
(
$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'
)
;
// Load the preamble utility library if we haven't already. On web
// requests this loaded earlier, but we want to load it for non-web
// requests so that unit tests can call these functions.
require_once
$phabricator_path
.
'/support/startup/preamble-utils.php'
;
}
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
)
;
}
// Drop any class map caches, since they will have generated without
// any classes from libraries. Without this, preflight setup checks can
// cause generation of a setup check cache that omits checks defined in
// libraries, for example.
PhutilClassMapQuery
::
deleteCaches
(
)
;
// 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
)
;
// If the site source did anything which reads config, throw it away
// to make sure any additional site sources get clean reads.
self
::
dropConfigCache
(
)
;
}
$masters
=
PhabricatorDatabaseRef
::
getMasterDatabaseRefs
(
)
;
if
(
!
$masters
)
{
self
::
setReadOnly
(
true
,
self
::
READONLY_MASTERLESS
)
;
}
else
{
// If any master is severed, we drop to readonly mode. In theory we
// could try to continue if we're only missing some applications, but
// this is very complex and we're unlikely to get it right.
foreach
(
$masters
as
$master
)
{
// Give severed masters one last chance to get healthy.
if
(
$master
->
isSevered
(
)
)
{
$master
->
checkHealth
(
)
;
}
if
(
$master
->
isSevered
(
)
)
{
self
::
setReadOnly
(
true
,
self
::
READONLY_SEVERED
)
;
break
;
}
}
}
try
{
// See T13403. If we're starting up in "config optional" mode, suppress
// messages about connection retries.
if
(
$config_optional
)
{
$database_source
=
@
new
PhabricatorConfigDatabaseSource
(
'default'
)
;
}
else
{
$database_source
=
new
PhabricatorConfigDatabaseSource
(
'default'
)
;
}
$database_source
->
setName
(
pht
(
'Database'
)
)
;
$stack
->
pushSource
(
$database_source
)
;
}
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
(
PhabricatorClusterStrandedException
$ex
)
{
// This means we can't connect to any database host. That's fine as
// long as we're running a setup script like `bin/storage`.
if
(
!
$config_optional
)
{
throw
$ex
;
}
}
// Drop the config cache one final time to make sure we're getting clean
// reads now that we've finished building the stack.
self
::
dropConfigCache
(
)
;
}
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
(
!
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
]
;
}
if
(
!
self
::
$sourceStack
)
{
throw
new
Exception
(
pht
(
'Trying to read configuration "%s" before configuration has been '
.
'initialized.'
,
$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
isSelfURI
(
$raw_uri
)
{
$uri
=
new
PhutilURI
(
$raw_uri
)
;
$host
=
$uri
->
getDomain
(
)
;
if
(
!
phutil_nonempty_string
(
$host
)
)
{
return
true
;
}
$host
=
phutil_utf8_strtolower
(
$host
)
;
$self_map
=
self
::
getSelfURIMap
(
)
;
return
isset
(
$self_map
[
$host
]
)
;
}
private
static
function
getSelfURIMap
(
)
{
$self_uris
=
array
(
)
;
$self_uris
[
]
=
self
::
getProductionURI
(
'/'
)
;
$self_uris
[
]
=
self
::
getURI
(
'/'
)
;
$allowed_uris
=
self
::
getEnvConfig
(
'phabricator.allowed-uris'
)
;
foreach
(
$allowed_uris
as
$allowed_uri
)
{
$self_uris
[
]
=
$allowed_uri
;
}
$self_map
=
array
(
)
;
foreach
(
$self_uris
as
$self_uri
)
{
$host
=
id
(
new
PhutilURI
(
$self_uri
)
)
->
getDomain
(
)
;
if
(
!
phutil_nonempty_string
(
$host
)
)
{
continue
;
}
$host
=
phutil_utf8_strtolower
(
$host
)
;
$self_map
[
$host
]
=
$host
;
}
return
$self_map
;
}
/**
* 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'
)
{
$params
=
array
(
'name'
=>
$resource
,
'type'
=>
$type
,
'jump'
=>
true
,
)
;
$uri
=
new
PhutilURI
(
'https://we.phorge.it/diviner/find/'
,
$params
)
;
return
phutil_string_cast
(
$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
(
'This server is in read-only mode (no writable database '
.
'is configured).'
)
;
case
self
::
READONLY_UNREACHABLE
:
return
pht
(
'This server is in read-only mode (unreachable master).'
)
;
case
self
::
READONLY_SEVERED
:
return
pht
(
'This server is in read-only mode (major interruption).'
)
;
}
return
pht
(
'This server 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
(
!
phutil_nonempty_string
(
$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 identifies 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
(
!
$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
(
!
$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<string> 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<string> Allowed protocols.
* @return pair<string, string> Pre-resolved URI and domain.
* @task uri
*/
public
static
function
requireValidRemoteURIForFetch
(
$raw_uri
,
array
$protocols
)
{
$uri
=
new
PhutilURI
(
$raw_uri
)
;
$proto
=
$uri
->
getProtocol
(
)
;
if
(
!
$proto
)
{
throw
new
Exception
(
pht
(
'URI "%s" is not a valid fetchable resource. A valid fetchable '
.
'resource URI must specify a protocol.'
,
$raw_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.'
,
$raw_uri
,
implode
(
', '
,
array_keys
(
$protocols
)
)
)
)
;
}
$domain
=
$uri
->
getDomain
(
)
;
if
(
!
$domain
)
{
throw
new
Exception
(
pht
(
'URI "%s" is not a valid fetchable resource. A valid fetchable '
.
'resource URI must specify a domain.'
,
$raw_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.'
,
$raw_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.'
,
$raw_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
=
self
::
getRemoteAddress
(
)
;
if
(
!
$address
)
{
throw
new
Exception
(
pht
(
'Unable to test remote address against cluster whitelist: '
.
'REMOTE_ADDR is not defined or not valid.'
)
)
;
}
return
self
::
isClusterAddress
(
$address
)
;
}
public
static
function
isClusterAddress
(
$address
)
{
$cluster_addresses
=
self
::
getEnvConfig
(
'cluster.addresses'
)
;
if
(
!
$cluster_addresses
)
{
throw
new
Exception
(
pht
(
'This server 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
)
;
}
public
static
function
getRemoteAddress
(
)
{
$address
=
idx
(
$_SERVER
,
'REMOTE_ADDR'
)
;
if
(
!
$address
)
{
return
null
;
}
try
{
return
PhutilIPAddress
::
newAddress
(
$address
)
;
}
catch
(
Exception
$ex
)
{
return
null
;
}
}
/* -( 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/'
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Sun, Feb 23, 09:37 (1 d, 14 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1181248
Default Alt Text
PhabricatorEnv.php (28 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment