Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15461148
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
24 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php
index f9af13b0b8..6b5b263a8a 100644
--- a/src/infrastructure/env/PhabricatorEnv.php
+++ b/src/infrastructure/env/PhabricatorEnv.php
@@ -1,452 +1,453 @@
<?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 {
private static $sourceStack;
/**
* @phutil-external-symbol class PhabricatorStartup
*/
public static function initializeWebEnvironment() {
$env = self::getSelectedEnvironmentName();
if (!$env) {
PhabricatorStartup::didFatal(
"The 'PHABRICATOR_ENV' environmental variable is not defined. Modify ".
"your httpd.conf to include 'SetEnv PHABRICATOR_ENV <env>', where ".
"'<env>' is one of 'development', 'production', or a custom ".
"environment.");
}
self::initializeCommonEnvironment();
}
public static function initializeScriptEnvironment() {
$env = self::getSelectedEnvironmentName();
if (!$env) {
echo phutil_console_wrap(
phutil_console_format(
"**ERROR**: PHABRICATOR_ENV Not Set\n\n".
"Define the __PHABRICATOR_ENV__ environment variable before ".
"running this script. You can do it on the command line like ".
"this:\n\n".
" $ PHABRICATOR_ENV=__custom/myconfig__ %s ...\n\n".
"Replace __custom/myconfig__ with the path to your configuration ".
"file. For more information, see the 'Configuration Guide' in the ".
"Phabricator documentation.\n\n",
$GLOBALS['argv'][0]));
exit(1);
}
self::initializeCommonEnvironment();
// NOTE: This is dangerous in general, but we know we're in a script context
// and are not vulnerable to CSRF.
AphrontWriteGuard::allowDangerousUnguardedWrites(true);
// There are several places where we log information (about errors, events,
// service calls, etc.) for analysis via DarkConsole or similar. These are
// useful for web requests, but grow unboundedly in long-running scripts and
// daemons. Discard data as it arrives in these cases.
PhutilServiceProfiler::getInstance()->enableDiscardMode();
DarkConsoleErrorLogPluginAPI::enableDiscardMode();
DarkConsoleEventPluginAPI::enableDiscardMode();
}
private static function initializeCommonEnvironment() {
$env = self::getSelectedEnvironmentName();
self::buildConfigurationSourceStack();
PhutilErrorHandler::initialize();
$tz = PhabricatorEnv::getEnvConfig('phabricator.timezone');
if ($tz) {
date_default_timezone_set($tz);
}
// Append any paths to $PATH if we need to.
$paths = PhabricatorEnv::getEnvConfig('environment.append-paths');
if (!empty($paths)) {
$current_env_path = getenv('PATH');
$new_env_paths = implode(PATH_SEPARATOR, $paths);
putenv('PATH='.$current_env_path.PATH_SEPARATOR.$new_env_paths);
}
foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) {
phutil_load_library($library);
}
PhabricatorEventEngine::initialize();
$translation = PhabricatorEnv::newObjectFromConfig('translation.provider');
PhutilTranslator::getInstance()
->setLanguage($translation->getLanguage())
->addTranslations($translation->getTranslations());
}
private static function buildConfigurationSourceStack() {
$stack = new PhabricatorConfigStackSource();
self::$sourceStack = $stack;
$stack->pushSource(
id(new PhabricatorConfigDefaultSource())
->setName(pht('Global Default')));
$env = self::getSelectedEnvironmentName();
$stack->pushSource(
id(new PhabricatorConfigFileSource($env))
->setName(pht("File '%s'", $env)));
$stack->pushSource(
id(new PhabricatorConfigLocalSource())
->setName(pht("Local Config")));
}
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.
*
* @task read
*/
public static function getEnvConfig($key, $default = null) {
$result = self::$sourceStack->getKeys(array($key));
return idx($result, $key, $default);
}
/**
* Get the fully-qualified URI for a path.
*
* @task read
*/
public static function getURI($path) {
return rtrim(self::getEnvConfig('phabricator.base-uri'), '/').$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::getEnvConfig('phabricator.base-uri');
}
return rtrim($production_domain, '/').$path;
}
/**
* 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::getEnvConfig('phabricator.base-uri');
}
$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) {
return 'http://www.phabricator.com/docs/phabricator/'.$resource;
}
/**
* Build a concrete object from a configuration key.
*
* @task read
*/
public static function newObjectFromConfig($key, $args = array()) {
$class = self::getEnvConfig($key);
$object = newv($class, $args);
$instanceof = idx(self::getRequiredClasses(), $key);
if (!($object instanceof $instanceof)) {
throw new Exception("Config setting '$key' must be an instance of ".
"'$instanceof', is '".get_class($object)."'.");
}
return $object;
}
/* -( Unit Test Support )-------------------------------------------------- */
/**
* @task test
*/
public static function beginScopedEnv() {
return new PhabricatorScopedEnv(self::pushTestEnvironment());
}
/**
* @task test
*/
private static function pushTestEnvironment() {
$source = new PhabricatorConfigDictionarySource(array());
self::$sourceStack->pushSource($source);
return spl_object_hash($source);
}
/**
* @task test
*/
public static function popTestEnvironment($key) {
$source = self::$sourceStack->popSource();
$stack_key = spl_object_hash($source);
if ($stack_key !== $key) {
+ self::$sourceStack->pushSource($source);
throw new Exception(
"Scoped environments were destroyed in a diffent order than they ".
"were initialized.");
}
}
/* -( URI Validation )----------------------------------------------------- */
/**
* Detect if a URI satisfies either @{method:isValidLocalWebResource} or
* @{method:isValidRemoteWebResource}, i.e. is a page on this server or the
* URI of some other resource which has a valid protocol. This rejects
* garbage URIs and URIs with protocols which do not appear in the
* ##uri.allowed-protocols## configuration, notably 'javascript:' URIs.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to put in an "href" link attribute.
*
* @param string URI to test.
* @return bool True if the URI identifies a web resource.
* @task uri
*/
public static function isValidWebResource($uri) {
return self::isValidLocalWebResource($uri) ||
self::isValidRemoteWebResource($uri);
}
/**
* Detect if a URI identifies some page on this server.
*
* NOTE: This method is generally intended to reject URIs which it may be
* unsafe to issue a "Location:" redirect to.
*
* @param string URI to test.
* @return bool True if the URI identifies a local page.
* @task uri
*/
public static function isValidLocalWebResource($uri) {
$uri = (string)$uri;
if (!strlen($uri)) {
return false;
}
if (preg_match('/\s/', $uri)) {
// PHP hasn't been vulnerable to header injection attacks for a bunch of
// years, but we can safely reject these anyway since they're never valid.
return false;
}
// Valid URIs must begin with '/', followed by the end of the string or some
// other non-'/' character. This rejects protocol-relative URIs like
// "//evil.com/evil_stuff/".
return (bool)preg_match('@^/([^/]|$)@', $uri);
}
/**
* Detect if a URI identifies some valid remote resource.
*
* @param string URI to test.
* @return bool True if a URI idenfies a remote resource with an allowed
* protocol.
* @task uri
*/
public static function isValidRemoteWebResource($uri) {
$uri = (string)$uri;
$proto = id(new PhutilURI($uri))->getProtocol();
if (!$proto) {
return false;
}
$allowed = self::getEnvConfig('uri.allowed-protocols');
if (empty($allowed[$proto])) {
return false;
}
return true;
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
public static function getRequiredClasses() {
return array(
'translation.provider' => 'PhabricatorTranslation',
'metamta.mail-adapter' => 'PhabricatorMailImplementationAdapter',
'metamta.maniphest.reply-handler' => 'PhabricatorMailReplyHandler',
'metamta.differential.reply-handler' => 'PhabricatorMailReplyHandler',
'metamta.diffusion.reply-handler' => 'PhabricatorMailReplyHandler',
'metamta.package.reply-handler' => 'PhabricatorMailReplyHandler',
'storage.engine-selector' => 'PhabricatorFileStorageEngineSelector',
'search.engine-selector' => 'PhabricatorSearchEngineSelector',
'differential.field-selector' => 'DifferentialFieldSelector',
'maniphest.custom-task-extensions-class' => 'ManiphestTaskExtensions',
'aphront.default-application-configuration-class' =>
'AphrontApplicationConfiguration',
'controller.oauth-registration' =>
'PhabricatorOAuthRegistrationController',
'mysql.implementation' => 'AphrontMySQLDatabaseConnectionBase',
'differential.attach-task-class' => 'DifferentialTasksAttacher',
'mysql.configuration-provider' => 'DatabaseConfigurationProvider',
'syntax-highlighter.engine' => 'PhutilSyntaxHighlighterEngine',
);
}
/**
* @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);
}
}
}
diff --git a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
index 4adbe11459..13b5d9c5e2 100644
--- a/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
+++ b/src/infrastructure/env/__tests__/PhabricatorEnvTestCase.php
@@ -1,177 +1,159 @@
<?php
final class PhabricatorEnvTestCase extends PhabricatorTestCase {
public function testLocalWebResource() {
$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,
);
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidLocalWebResource($uri),
"Valid local resource: {$uri}");
}
}
public function testRemoteWebResource() {
$map = array(
'http://example.com/' => true,
'derp://example.com/' => false,
'javascript:alert(1)' => false,
);
foreach ($map as $uri => $expect) {
$this->assertEqual(
$expect,
PhabricatorEnv::isValidRemoteWebResource($uri),
"Valid remote resource: {$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->assertEqual(true, ($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();
- $middle = PhabricatorEnv::beginScopedEnv();
$inner = PhabricatorEnv::beginScopedEnv();
$caught = null;
try {
- if (phutil_is_hiphop_runtime()) {
- $middle->__destruct();
- }
- unset($middle);
+ $outer->__destruct();
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(
true,
$caught instanceof Exception,
"Destroying a scoped environment which is not on the top of the stack ".
"should throw.");
- $caught = null;
- try {
- if (phutil_is_hiphop_runtime()) {
- $inner->__destruct();
- }
- unset($inner);
- } catch (Exception $ex) {
- $caught = $ex;
+ if (phutil_is_hiphop_runtime()) {
+ $inner->__destruct();
}
+ unset($inner);
- $this->assertEqual(
- true,
- $caught instanceof Exception,
- "Destroying a scoped environment which is not on the top of the stack ".
- "should throw.");
-
- // Although we popped the other two out-of-order, we still expect to end
- // up in the right state after handling the exceptions, so this should
- // execute without issues.
if (phutil_is_hiphop_runtime()) {
$outer->__destruct();
}
unset($outer);
}
}
diff --git a/src/infrastructure/testing/PhabricatorTestCase.php b/src/infrastructure/testing/PhabricatorTestCase.php
index 660fd17670..dbdd1ef126 100644
--- a/src/infrastructure/testing/PhabricatorTestCase.php
+++ b/src/infrastructure/testing/PhabricatorTestCase.php
@@ -1,163 +1,166 @@
<?php
abstract class PhabricatorTestCase extends ArcanistPhutilTestCase {
const NAMESPACE_PREFIX = 'phabricator_unittest_';
/**
* If true, put Lisk in process-isolated mode for the duration of the tests so
* that it will establish only isolated, side-effect-free database
* connections. Defaults to true.
*
* NOTE: You should disable this only in rare circumstances. Unit tests should
* not rely on external resources like databases, and should not produce
* side effects.
*/
const PHABRICATOR_TESTCONFIG_ISOLATE_LISK = 'isolate-lisk';
/**
* If true, build storage fixtures before running tests, and connect to them
* during test execution. This will impose a performance penalty on test
* execution (currently, it takes roughly one second to build the fixture)
* but allows you to perform tests which require data to be read from storage
* after writes. The fixture is shared across all test cases in this process.
* Defaults to false.
*
* NOTE: All connections to fixture storage open transactions when established
* and roll them back when tests complete. Each test must independently
* write data it relies on; data will not persist across tests.
*
* NOTE: Enabling this implies disabling process isolation.
*/
const PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES = 'storage-fixtures';
private $configuration;
private $env;
private static $storageFixtureReferences = 0;
private static $storageFixture;
private static $storageFixtureObjectSeed = 0;
protected function getPhabricatorTestCaseConfiguration() {
return array();
}
private function getComputedConfiguration() {
$config = $this->getPhabricatorTestCaseConfiguration() + array(
self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK => true,
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => false,
);
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
// Fixtures don't make sense with process isolation.
$config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK] = false;
}
return $config;
}
protected function willRunTests() {
$root = dirname(phutil_get_library_root('phabricator'));
require_once $root.'/scripts/__init_script__.php';
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK]) {
LiskDAO::beginIsolateAllLiskEffectsToCurrentProcess();
}
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
++self::$storageFixtureReferences;
if (!self::$storageFixture) {
self::$storageFixture = $this->newStorageFixture();
}
}
$this->env = PhabricatorEnv::beginScopedEnv();
}
protected function didRunTests() {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_ISOLATE_LISK]) {
LiskDAO::endIsolateAllLiskEffectsToCurrentProcess();
}
if (self::$storageFixture) {
self::$storageFixtureReferences--;
if (!self::$storageFixtureReferences) {
self::$storageFixture = null;
}
}
try {
+ if (phutil_is_hiphop_runtime()) {
+ $this->env->__destruct();
+ }
unset($this->env);
} catch (Exception $ex) {
throw new Exception(
"Some test called PhabricatorEnv::beginScopedEnv(), but is still ".
"holding a reference to the scoped environment!");
}
}
protected function willRunOneTest($test) {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
LiskDAO::beginIsolateAllLiskEffectsToTransactions();
}
}
protected function didRunOneTest($test) {
$config = $this->getComputedConfiguration();
if ($config[self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES]) {
LiskDAO::endIsolateAllLiskEffectsToTransactions();
}
}
protected function newStorageFixture() {
$bytes = Filesystem::readRandomCharacters(24);
$name = self::NAMESPACE_PREFIX.$bytes;
return new PhabricatorStorageFixtureScopeGuard($name);
}
protected function getLink($method) {
$phabricator_project = 'PHID-APRJ-3f1fc779edeab89b2171';
return
'https://secure.phabricator.com/diffusion/symbol/'.$method.
'/?lang=php&projects='.$phabricator_project.
'&jump=true&context='.get_class($this);
}
/**
* Returns an integer seed to use when building unique identifiers (e.g.,
* non-colliding usernames). The seed is unstable and its value will change
* between test runs, so your tests must not rely on it.
*
* @return int A unique integer.
*/
protected function getNextObjectSeed() {
self::$storageFixtureObjectSeed += mt_rand(1, 100);
return self::$storageFixtureObjectSeed;
}
protected function generateNewTestUser() {
$seed = $this->getNextObjectSeed();
$user = id(new PhabricatorUser())
->setRealName("Test User {$seed}}")
->setUserName("test{$seed}");
$email = id(new PhabricatorUserEmail())
->setAddress("testuser{$seed}@example.com")
->setIsVerified(1);
$editor = new PhabricatorUserEditor();
$editor->setActor($user);
$editor->createNewUser($user, $email);
return $user;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Apr 3, 5:38 AM (2 d, 19 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7220912
Default Alt Text
(24 KB)
Attached To
Mode
rP Phabricator
Attached
Detach File
Event Timeline
Log In to Comment