[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * Manages the execution environment configuration, exposing APIs to read 5 * configuration settings and other similar values that are derived directly 6 * from configuration settings. 7 * 8 * 9 * = Reading Configuration = 10 * 11 * The primary role of this class is to provide an API for reading 12 * Phabricator configuration, @{method:getEnvConfig}: 13 * 14 * $value = PhabricatorEnv::getEnvConfig('some.key', $default); 15 * 16 * The class also handles some URI construction based on configuration, via 17 * the methods @{method:getURI}, @{method:getProductionURI}, 18 * @{method:getCDNURI}, and @{method:getDoclink}. 19 * 20 * For configuration which allows you to choose a class to be responsible for 21 * some functionality (e.g., which mail adapter to use to deliver email), 22 * @{method:newObjectFromConfig} provides a simple interface that validates 23 * the configured value. 24 * 25 * 26 * = Unit Test Support = 27 * 28 * In unit tests, you can use @{method:beginScopedEnv} to create a temporary, 29 * mutable environment. The method returns a scope guard object which restores 30 * the environment when it is destroyed. For example: 31 * 32 * public function testExample() { 33 * $env = PhabricatorEnv::beginScopedEnv(); 34 * $env->overrideEnv('some.key', 'new-value-for-this-test'); 35 * 36 * // Some test which depends on the value of 'some.key'. 37 * 38 * } 39 * 40 * Your changes will persist until the `$env` object leaves scope or is 41 * destroyed. 42 * 43 * You should //not// use this in normal code. 44 * 45 * 46 * @task read Reading Configuration 47 * @task uri URI Validation 48 * @task test Unit Test Support 49 * @task internal Internals 50 */ 51 final class PhabricatorEnv { 52 53 private static $sourceStack; 54 private static $repairSource; 55 private static $overrideSource; 56 private static $requestBaseURI; 57 private static $cache; 58 59 /** 60 * @phutil-external-symbol class PhabricatorStartup 61 */ 62 public static function initializeWebEnvironment() { 63 self::initializeCommonEnvironment(); 64 } 65 66 public static function initializeScriptEnvironment() { 67 self::initializeCommonEnvironment(); 68 69 // NOTE: This is dangerous in general, but we know we're in a script context 70 // and are not vulnerable to CSRF. 71 AphrontWriteGuard::allowDangerousUnguardedWrites(true); 72 73 // There are several places where we log information (about errors, events, 74 // service calls, etc.) for analysis via DarkConsole or similar. These are 75 // useful for web requests, but grow unboundedly in long-running scripts and 76 // daemons. Discard data as it arrives in these cases. 77 PhutilServiceProfiler::getInstance()->enableDiscardMode(); 78 DarkConsoleErrorLogPluginAPI::enableDiscardMode(); 79 DarkConsoleEventPluginAPI::enableDiscardMode(); 80 } 81 82 83 private static function initializeCommonEnvironment() { 84 PhutilErrorHandler::initialize(); 85 86 self::buildConfigurationSourceStack(); 87 88 // Force a valid timezone. If both PHP and Phabricator configuration are 89 // invalid, use UTC. 90 $tz = PhabricatorEnv::getEnvConfig('phabricator.timezone'); 91 if ($tz) { 92 @date_default_timezone_set($tz); 93 } 94 $ok = @date_default_timezone_set(date_default_timezone_get()); 95 if (!$ok) { 96 date_default_timezone_set('UTC'); 97 } 98 99 // Prepend '/support/bin' and append any paths to $PATH if we need to. 100 $env_path = getenv('PATH'); 101 $phabricator_path = dirname(phutil_get_library_root('phabricator')); 102 $support_path = $phabricator_path.'/support/bin'; 103 $env_path = $support_path.PATH_SEPARATOR.$env_path; 104 $append_dirs = PhabricatorEnv::getEnvConfig('environment.append-paths'); 105 if (!empty($append_dirs)) { 106 $append_path = implode(PATH_SEPARATOR, $append_dirs); 107 $env_path = $env_path.PATH_SEPARATOR.$append_path; 108 } 109 putenv('PATH='.$env_path); 110 111 // Write this back into $_ENV, too, so ExecFuture picks it up when creating 112 // subprocess environments. 113 $_ENV['PATH'] = $env_path; 114 115 PhabricatorEventEngine::initialize(); 116 117 $translation = PhabricatorEnv::newObjectFromConfig('translation.provider'); 118 PhutilTranslator::getInstance() 119 ->setLanguage($translation->getLanguage()) 120 ->addTranslations($translation->getTranslations()); 121 } 122 123 private static function buildConfigurationSourceStack() { 124 self::dropConfigCache(); 125 126 $stack = new PhabricatorConfigStackSource(); 127 self::$sourceStack = $stack; 128 129 $default_source = id(new PhabricatorConfigDefaultSource()) 130 ->setName(pht('Global Default')); 131 $stack->pushSource($default_source); 132 133 $env = self::getSelectedEnvironmentName(); 134 if ($env) { 135 $stack->pushSource( 136 id(new PhabricatorConfigFileSource($env)) 137 ->setName(pht("File '%s'", $env))); 138 } 139 140 $stack->pushSource( 141 id(new PhabricatorConfigLocalSource()) 142 ->setName(pht('Local Config'))); 143 144 // If the install overrides the database adapter, we might need to load 145 // the database adapter class before we can push on the database config. 146 // This config is locked and can't be edited from the web UI anyway. 147 foreach (PhabricatorEnv::getEnvConfig('load-libraries') as $library) { 148 phutil_load_library($library); 149 } 150 151 // If custom libraries specify config options, they won't get default 152 // values as the Default source has already been loaded, so we get it to 153 // pull in all options from non-phabricator libraries now they are loaded. 154 $default_source->loadExternalOptions(); 155 156 // If this install has site config sources, load them now. 157 $site_sources = id(new PhutilSymbolLoader()) 158 ->setAncestorClass('PhabricatorConfigSiteSource') 159 ->loadObjects(); 160 $site_sources = msort($site_sources, 'getPriority'); 161 foreach ($site_sources as $site_source) { 162 $stack->pushSource($site_source); 163 } 164 165 try { 166 $stack->pushSource( 167 id(new PhabricatorConfigDatabaseSource('default')) 168 ->setName(pht('Database'))); 169 } catch (AphrontQueryException $exception) { 170 // If the database is not available, just skip this configuration 171 // source. This happens during `bin/storage upgrade`, `bin/conf` before 172 // schema setup, etc. 173 } 174 } 175 176 public static function repairConfig($key, $value) { 177 if (!self::$repairSource) { 178 self::$repairSource = id(new PhabricatorConfigDictionarySource(array())) 179 ->setName(pht('Repaired Config')); 180 self::$sourceStack->pushSource(self::$repairSource); 181 } 182 self::$repairSource->setKeys(array($key => $value)); 183 self::dropConfigCache(); 184 } 185 186 public static function overrideConfig($key, $value) { 187 if (!self::$overrideSource) { 188 self::$overrideSource = id(new PhabricatorConfigDictionarySource(array())) 189 ->setName(pht('Overridden Config')); 190 self::$sourceStack->pushSource(self::$overrideSource); 191 } 192 self::$overrideSource->setKeys(array($key => $value)); 193 self::dropConfigCache(); 194 } 195 196 public static function getUnrepairedEnvConfig($key, $default = null) { 197 foreach (self::$sourceStack->getStack() as $source) { 198 if ($source === self::$repairSource) { 199 continue; 200 } 201 $result = $source->getKeys(array($key)); 202 if ($result) { 203 return $result[$key]; 204 } 205 } 206 return $default; 207 } 208 209 public static function getSelectedEnvironmentName() { 210 $env_var = 'PHABRICATOR_ENV'; 211 212 $env = idx($_SERVER, $env_var); 213 214 if (!$env) { 215 $env = getenv($env_var); 216 } 217 218 if (!$env) { 219 $env = idx($_ENV, $env_var); 220 } 221 222 if (!$env) { 223 $root = dirname(phutil_get_library_root('phabricator')); 224 $path = $root.'/conf/local/ENVIRONMENT'; 225 if (Filesystem::pathExists($path)) { 226 $env = trim(Filesystem::readFile($path)); 227 } 228 } 229 230 return $env; 231 } 232 233 public static function calculateEnvironmentHash() { 234 $keys = array_keys(self::getAllConfigKeys()); 235 ksort($keys); 236 237 $values = array(); 238 foreach ($keys as $key) { 239 $values[$key] = self::getEnvConfigIfExists($key); 240 } 241 return PhabricatorHash::digest(json_encode($values)); 242 } 243 244 245 /* -( Reading Configuration )---------------------------------------------- */ 246 247 248 /** 249 * Get the current configuration setting for a given key. 250 * 251 * If the key is not found, then throw an Exception. 252 * 253 * @task read 254 */ 255 public static function getEnvConfig($key) { 256 if (isset(self::$cache[$key])) { 257 return self::$cache[$key]; 258 } 259 260 if (array_key_exists($key, self::$cache)) { 261 return self::$cache[$key]; 262 } 263 264 $result = self::$sourceStack->getKeys(array($key)); 265 if (array_key_exists($key, $result)) { 266 self::$cache[$key] = $result[$key]; 267 return $result[$key]; 268 } else { 269 throw new Exception("No config value specified for key '{$key}'."); 270 } 271 } 272 273 274 /** 275 * Get the current configuration setting for a given key. If the key 276 * does not exist, return a default value instead of throwing. This is 277 * primarily useful for migrations involving keys which are slated for 278 * removal. 279 * 280 * @task read 281 */ 282 public static function getEnvConfigIfExists($key, $default = null) { 283 try { 284 return self::getEnvConfig($key); 285 } catch (Exception $ex) { 286 return $default; 287 } 288 } 289 290 291 /** 292 * Get the fully-qualified URI for a path. 293 * 294 * @task read 295 */ 296 public static function getURI($path) { 297 return rtrim(self::getAnyBaseURI(), '/').$path; 298 } 299 300 301 /** 302 * Get the fully-qualified production URI for a path. 303 * 304 * @task read 305 */ 306 public static function getProductionURI($path) { 307 // If we're passed a URI which already has a domain, simply return it 308 // unmodified. In particular, files may have URIs which point to a CDN 309 // domain. 310 $uri = new PhutilURI($path); 311 if ($uri->getDomain()) { 312 return $path; 313 } 314 315 $production_domain = self::getEnvConfig('phabricator.production-uri'); 316 if (!$production_domain) { 317 $production_domain = self::getAnyBaseURI(); 318 } 319 return rtrim($production_domain, '/').$path; 320 } 321 322 public static function getAllowedURIs($path) { 323 $uri = new PhutilURI($path); 324 if ($uri->getDomain()) { 325 return $path; 326 } 327 328 $allowed_uris = self::getEnvConfig('phabricator.allowed-uris'); 329 $return = array(); 330 foreach ($allowed_uris as $allowed_uri) { 331 $return[] = rtrim($allowed_uri, '/').$path; 332 } 333 334 return $return; 335 } 336 337 338 /** 339 * Get the fully-qualified production URI for a static resource path. 340 * 341 * @task read 342 */ 343 public static function getCDNURI($path) { 344 $alt = self::getEnvConfig('security.alternate-file-domain'); 345 if (!$alt) { 346 $alt = self::getAnyBaseURI(); 347 } 348 $uri = new PhutilURI($alt); 349 $uri->setPath($path); 350 return (string)$uri; 351 } 352 353 354 /** 355 * Get the fully-qualified production URI for a documentation resource. 356 * 357 * @task read 358 */ 359 public static function getDoclink($resource, $type = 'article') { 360 $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/'); 361 $uri->setQueryParam('name', $resource); 362 $uri->setQueryParam('type', $type); 363 $uri->setQueryParam('jump', true); 364 return (string)$uri; 365 } 366 367 368 /** 369 * Build a concrete object from a configuration key. 370 * 371 * @task read 372 */ 373 public static function newObjectFromConfig($key, $args = array()) { 374 $class = self::getEnvConfig($key); 375 return newv($class, $args); 376 } 377 378 public static function getAnyBaseURI() { 379 $base_uri = self::getEnvConfig('phabricator.base-uri'); 380 381 if (!$base_uri) { 382 $base_uri = self::getRequestBaseURI(); 383 } 384 385 if (!$base_uri) { 386 throw new Exception( 387 "Define 'phabricator.base-uri' in your configuration to continue."); 388 } 389 390 return $base_uri; 391 } 392 393 public static function getRequestBaseURI() { 394 return self::$requestBaseURI; 395 } 396 397 public static function setRequestBaseURI($uri) { 398 self::$requestBaseURI = $uri; 399 } 400 401 /* -( Unit Test Support )-------------------------------------------------- */ 402 403 404 /** 405 * @task test 406 */ 407 public static function beginScopedEnv() { 408 return new PhabricatorScopedEnv(self::pushTestEnvironment()); 409 } 410 411 412 /** 413 * @task test 414 */ 415 private static function pushTestEnvironment() { 416 self::dropConfigCache(); 417 $source = new PhabricatorConfigDictionarySource(array()); 418 self::$sourceStack->pushSource($source); 419 return spl_object_hash($source); 420 } 421 422 423 /** 424 * @task test 425 */ 426 public static function popTestEnvironment($key) { 427 self::dropConfigCache(); 428 $source = self::$sourceStack->popSource(); 429 $stack_key = spl_object_hash($source); 430 if ($stack_key !== $key) { 431 self::$sourceStack->pushSource($source); 432 throw new Exception( 433 'Scoped environments were destroyed in a diffent order than they '. 434 'were initialized.'); 435 } 436 } 437 438 439 /* -( URI Validation )----------------------------------------------------- */ 440 441 442 /** 443 * Detect if a URI satisfies either @{method:isValidLocalWebResource} or 444 * @{method:isValidRemoteWebResource}, i.e. is a page on this server or the 445 * URI of some other resource which has a valid protocol. This rejects 446 * garbage URIs and URIs with protocols which do not appear in the 447 * ##uri.allowed-protocols## configuration, notably 'javascript:' URIs. 448 * 449 * NOTE: This method is generally intended to reject URIs which it may be 450 * unsafe to put in an "href" link attribute. 451 * 452 * @param string URI to test. 453 * @return bool True if the URI identifies a web resource. 454 * @task uri 455 */ 456 public static function isValidWebResource($uri) { 457 return self::isValidLocalWebResource($uri) || 458 self::isValidRemoteWebResource($uri); 459 } 460 461 462 /** 463 * Detect if a URI identifies some page on this server. 464 * 465 * NOTE: This method is generally intended to reject URIs which it may be 466 * unsafe to issue a "Location:" redirect to. 467 * 468 * @param string URI to test. 469 * @return bool True if the URI identifies a local page. 470 * @task uri 471 */ 472 public static function isValidLocalWebResource($uri) { 473 $uri = (string)$uri; 474 475 if (!strlen($uri)) { 476 return false; 477 } 478 479 if (preg_match('/\s/', $uri)) { 480 // PHP hasn't been vulnerable to header injection attacks for a bunch of 481 // years, but we can safely reject these anyway since they're never valid. 482 return false; 483 } 484 485 // Chrome (at a minimum) interprets backslashes in Location headers and the 486 // URL bar as forward slashes. This is probably intended to reduce user 487 // error caused by confusion over which key is "forward slash" vs "back 488 // slash". 489 // 490 // However, it means a URI like "/\evil.com" is interpreted like 491 // "//evil.com", which is a protocol relative remote URI. 492 // 493 // Since we currently never generate URIs with backslashes in them, reject 494 // these unconditionally rather than trying to figure out how browsers will 495 // interpret them. 496 if (preg_match('/\\\\/', $uri)) { 497 return false; 498 } 499 500 // Valid URIs must begin with '/', followed by the end of the string or some 501 // other non-'/' character. This rejects protocol-relative URIs like 502 // "//evil.com/evil_stuff/". 503 return (bool)preg_match('@^/([^/]|$)@', $uri); 504 } 505 506 507 /** 508 * Detect if a URI identifies some valid remote resource. 509 * 510 * @param string URI to test. 511 * @return bool True if a URI idenfies a remote resource with an allowed 512 * protocol. 513 * @task uri 514 */ 515 public static function isValidRemoteWebResource($uri) { 516 $uri = (string)$uri; 517 518 $proto = id(new PhutilURI($uri))->getProtocol(); 519 if (!$proto) { 520 return false; 521 } 522 523 $allowed = self::getEnvConfig('uri.allowed-protocols'); 524 if (empty($allowed[$proto])) { 525 return false; 526 } 527 528 return true; 529 } 530 531 532 /* -( Internals )---------------------------------------------------------- */ 533 534 535 /** 536 * @task internal 537 */ 538 public static function envConfigExists($key) { 539 return array_key_exists($key, self::$sourceStack->getKeys(array($key))); 540 } 541 542 543 /** 544 * @task internal 545 */ 546 public static function getAllConfigKeys() { 547 return self::$sourceStack->getAllKeys(); 548 } 549 550 public static function getConfigSourceStack() { 551 return self::$sourceStack; 552 } 553 554 /** 555 * @task internal 556 */ 557 public static function overrideTestEnvConfig($stack_key, $key, $value) { 558 $tmp = array(); 559 560 // If we don't have the right key, we'll throw when popping the last 561 // source off the stack. 562 do { 563 $source = self::$sourceStack->popSource(); 564 array_unshift($tmp, $source); 565 if (spl_object_hash($source) == $stack_key) { 566 $source->setKeys(array($key => $value)); 567 break; 568 } 569 } while (true); 570 571 foreach ($tmp as $source) { 572 self::$sourceStack->pushSource($source); 573 } 574 575 self::dropConfigCache(); 576 } 577 578 private static function dropConfigCache() { 579 self::$cache = array(); 580 } 581 582 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |