Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F34109704
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
40 KB
Subscribers
None
View Options
diff --git a/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php b/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php
index 8c85553e7..ac8ba4ebd 100644
--- a/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php
+++ b/src/applications/differential/field/DifferentialRevisionIDCommitMessageField.php
@@ -1,100 +1,86 @@
<?php
final class DifferentialRevisionIDCommitMessageField
extends DifferentialCommitMessageField {
const FIELDKEY = 'revisionID';
public function getFieldName() {
return pht('Differential Revision');
}
public function getFieldOrder() {
return 200000;
}
public function isTemplateField() {
return false;
}
public function parseFieldValue($value) {
// If the complete commit message we are parsing has unrecognized custom
// fields at the end, they can end up parsed into the field value for this
// field. For example, if the message looks like this:
// Differential Revision: xyz
// Some-Other-Field: abc
// ...we will receive "xyz\nSome-Other-Field: abc" as the field value for
// this field. Ideally, the install would define these fields so they can
// parse formally, but we can reasonably assume that only the first line
// of any value we encounter actually contains a revision identifier, so
// start by throwing away any other lines.
$value = trim($value);
$value = phutil_split_lines($value, false);
$value = head($value);
$value = trim($value);
// If the value is just "D123" or similar, parse the ID from it directly.
$matches = null;
if (preg_match('/^[dD]([1-9]\d*)\z/', $value, $matches)) {
return (int)$matches[1];
}
// Otherwise, try to extract a URI value.
return self::parseRevisionIDFromURI($value);
}
private static function parseRevisionIDFromURI($uri_string) {
$uri = new PhutilURI($uri_string);
$path = $uri->getPath();
- $matches = null;
- if (preg_match('#^/D(\d+)$#', $path, $matches)) {
- $id = (int)$matches[1];
-
- $prod_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/D'.$id));
-
- // Make sure the URI is the same as our URI. Basically, we want to ignore
- // commits from other Phabricator installs.
- if ($uri->getDomain() == $prod_uri->getDomain()) {
- return $id;
- }
-
- $allowed_uris = PhabricatorEnv::getAllowedURIs('/D'.$id);
-
- foreach ($allowed_uris as $allowed_uri) {
- if ($uri_string == $allowed_uri) {
- return $id;
- }
+ if (PhabricatorEnv::isSelfURI($uri_string)) {
+ $matches = null;
+ if (preg_match('#^/D(\d+)$#', $path, $matches)) {
+ return (int)$matches[1];
}
}
return null;
}
public function readFieldValueFromObject(DifferentialRevision $revision) {
return $revision->getID();
}
public function readFieldValueFromConduit($value) {
if (is_int($value)) {
$value = (string)$value;
}
return $this->readStringFieldValueFromConduit($value);
}
public function renderFieldValue($value) {
if (!strlen($value)) {
return null;
}
return PhabricatorEnv::getProductionURI('/D'.$value);
}
public function getFieldTransactions($value) {
return array();
}
}
diff --git a/src/applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php b/src/applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php
index 295da8ca5..41fd2992f 100644
--- a/src/applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php
+++ b/src/applications/differential/field/__tests__/DifferentialCommitMessageFieldTestCase.php
@@ -1,30 +1,31 @@
<?php
final class DifferentialCommitMessageFieldTestCase
extends PhabricatorTestCase {
public function testRevisionCommitMessageFieldParsing() {
$base_uri = 'https://www.example.com/';
$tests = array(
'D123' => 123,
'd123' => 123,
" \n d123 \n " => 123,
"D123\nSome-Custom-Field: The End" => 123,
"{$base_uri}D123" => 123,
"{$base_uri}D123\nSome-Custom-Field: The End" => 123,
+ 'https://www.other.com/D123' => null,
);
$env = PhabricatorEnv::beginScopedEnv();
$env->overrideEnvConfig('phabricator.base-uri', $base_uri);
foreach ($tests as $input => $expect) {
$actual = id(new DifferentialRevisionIDCommitMessageField())
->parseFieldValue($input);
$this->assertEqual($expect, $actual, pht('Parse of: %s', $input));
}
unset($env);
}
}
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
index b50c85c0f..70ddb8063 100644
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -1,940 +1,963 @@
<?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 (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);
}
// 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);
}
$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 {
$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 (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;
}
}
}
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];
}
$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;
+
+ public static function isSelfURI($raw_uri) {
+ $uri = new PhutilURI($raw_uri);
+
+ $host = $uri->getDomain();
+ if (!strlen($host)) {
+ return false;
}
+ $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');
- $return = array();
foreach ($allowed_uris as $allowed_uri) {
- $return[] = rtrim($allowed_uri, '/').$path;
+ $self_uris[] = $allowed_uri;
}
- return $return;
- }
+ $self_map = array();
+ foreach ($self_uris as $self_uri) {
+ $host = id(new PhutilURI($self_uri))->getDomain();
+ if (!strlen($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') {
$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<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 (!strlen($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 (!strlen($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(
'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);
}
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/';
}
}
diff --git a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
index d47258822..f73299aa1 100644
--- a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
+++ b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
@@ -1,221 +1,256 @@
<?php
final class PhabricatorEnvTestCase extends PhabricatorTestCase {
public function testLocalURIForLink() {
$map = array(
'/' => true,
'/D123' => true,
'/path/to/something/' => true,
"/path/to/\nHeader: x" => false,
'http://evil.com/' => false,
'//evil.com/evil/' => false,
'javascript:lol' => false,
'' => false,
null => false,
'/\\evil.com' => false,
);
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidLocalURIForLink($uri),
pht('Valid local resource: %s', $uri));
}
}
public function testRemoteURIForLink() {
$map = array(
'http://example.com/' => true,
'derp://example.com/' => false,
'javascript:alert(1)' => false,
'http://127.0.0.1/' => true,
'http://169.254.169.254/latest/meta-data/hostname' => true,
);
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidRemoteURIForLink($uri),
pht('Valid linkable remote URI: %s', $uri));
}
}
public function testRemoteURIForFetch() {
$map = array(
'http://example.com/' => true,
// No domain or protocol.
'' => false,
// No domain.
'http://' => false,
// No protocol.
'evil.com' => false,
// No protocol.
'//evil.com' => false,
// Bad protocol.
'javascript://evil.com/' => false,
'file:///etc/shadow' => false,
// Unresolvable hostname.
'http://u1VcxwUp368SIFzl7rkWWg23KM5JPB2kTHHngxjXCQc.zzz/' => false,
// Domains explicitly in blacklisted IP space.
'http://127.0.0.1/' => false,
'http://169.254.169.254/latest/meta-data/hostname' => false,
// Domain resolves into blacklisted IP space.
'http://localhost/' => false,
);
$protocols = array('http', 'https');
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidRemoteURIForFetch($uri, $protocols),
pht('Valid fetchable remote URI: %s', $uri));
}
}
public function testDictionarySource() {
$source = new PhabricatorConfigDictionarySource(array('x' => 1));
$this->assertEqual(
array(
'x' => 1,
),
$source->getKeys(array('x', 'z')));
$source->setKeys(array('z' => 2));
$this->assertEqual(
array(
'x' => 1,
'z' => 2,
),
$source->getKeys(array('x', 'z')));
$source->setKeys(array('x' => 3));
$this->assertEqual(
array(
'x' => 3,
'z' => 2,
),
$source->getKeys(array('x', 'z')));
$source->deleteKeys(array('x'));
$this->assertEqual(
array(
'z' => 2,
),
$source->getKeys(array('x', 'z')));
}
public function testStackSource() {
$s1 = new PhabricatorConfigDictionarySource(array('x' => 1));
$s2 = new PhabricatorConfigDictionarySource(array('x' => 2));
$stack = new PhabricatorConfigStackSource();
$this->assertEqual(array(), $stack->getKeys(array('x')));
$stack->pushSource($s1);
$this->assertEqual(array('x' => 1), $stack->getKeys(array('x')));
$stack->pushSource($s2);
$this->assertEqual(array('x' => 2), $stack->getKeys(array('x')));
$stack->setKeys(array('x' => 3));
$this->assertEqual(array('x' => 3), $stack->getKeys(array('x')));
$stack->popSource();
$this->assertEqual(array('x' => 1), $stack->getKeys(array('x')));
$stack->popSource();
$this->assertEqual(array(), $stack->getKeys(array('x')));
$caught = null;
try {
$stack->popSource();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
}
public function testOverrides() {
$outer = PhabricatorEnv::beginScopedEnv();
$outer->overrideEnvConfig('test.value', 1);
$this->assertEqual(1, PhabricatorEnv::getEnvConfig('test.value'));
$inner = PhabricatorEnv::beginScopedEnv();
$inner->overrideEnvConfig('test.value', 2);
$this->assertEqual(2, PhabricatorEnv::getEnvConfig('test.value'));
if (phutil_is_hiphop_runtime()) {
$inner->__destruct();
}
unset($inner);
$this->assertEqual(1, PhabricatorEnv::getEnvConfig('test.value'));
if (phutil_is_hiphop_runtime()) {
$outer->__destruct();
}
unset($outer);
}
public function testOverrideOrder() {
$outer = PhabricatorEnv::beginScopedEnv();
$inner = PhabricatorEnv::beginScopedEnv();
$caught = null;
try {
$outer->__destruct();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue(
$caught instanceof Exception,
pht(
'Destroying a scoped environment which is not on the top of the '.
'stack should throw.'));
if (phutil_is_hiphop_runtime()) {
$inner->__destruct();
}
unset($inner);
if (phutil_is_hiphop_runtime()) {
$outer->__destruct();
}
unset($outer);
}
public function testGetEnvExceptions() {
$caught = null;
try {
PhabricatorEnv::getEnvConfig('not.a.real.config.option');
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertTrue($caught instanceof Exception);
$caught = null;
try {
PhabricatorEnv::getEnvConfig('test.value');
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertFalse($caught instanceof Exception);
}
+ public function testSelfURI() {
+ $base_uri = 'https://allowed.example.com/';
+
+ $allowed_uris = array(
+ 'https://old.example.com/',
+ );
+
+ $env = PhabricatorEnv::beginScopedEnv();
+ $env->overrideEnvConfig('phabricator.base-uri', $base_uri);
+ $env->overrideEnvConfig('phabricator.allowed-uris', $allowed_uris);
+
+ $map = array(
+ 'https://allowed.example.com/' => true,
+ 'https://allowed.example.com' => true,
+ 'https://allowed.EXAMPLE.com' => true,
+ 'http://allowed.example.com/' => true,
+ 'https://allowed.example.com/path/to/resource.png' => true,
+
+ 'https://old.example.com/' => true,
+ 'https://old.example.com' => true,
+ 'https://old.EXAMPLE.com' => true,
+ 'http://old.example.com/' => true,
+ 'https://old.example.com/path/to/resource.png' => true,
+
+ 'https://other.example.com/' => false,
+ );
+
+ foreach ($map as $input => $expect) {
+ $this->assertEqual(
+ $expect,
+ PhabricatorEnv::isSelfURI($input),
+ pht('Is self URI? %s', $input));
+ }
+ }
+
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Dec 5, 4:40 AM (18 h, 4 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
71/a7/924dfb5debcc660576b5e3f1df27
Attached To
rPHAB Phabricator
Event Timeline
Log In to Comment