@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
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 */
51final class PhabricatorEnv extends Phobject {
52
53 private static $sourceStack;
54 private static $repairSource;
55 private static $overrideSource;
56 private static $requestBaseURI;
57 private static $cache;
58 private static $localeCode;
59 private static $readOnly;
60 private static $readOnlyReason;
61
62 const READONLY_CONFIG = 'config';
63 const READONLY_UNREACHABLE = 'unreachable';
64 const READONLY_SEVERED = 'severed';
65 const READONLY_MASTERLESS = 'masterless';
66
67 /**
68 * @phutil-external-symbol class PhabricatorStartup
69 */
70 public static function initializeWebEnvironment() {
71 self::initializeCommonEnvironment(false, false);
72
73 // Set up en_US locale for now so that, for instance, if you haven't
74 // set up your database at all it says "Run this command" rather than
75 // "Run these 1 command(s)"
76 // If there aren't any setup problems, then this will get overwritten with
77 // the logged-in user's locale or the locale specified in global default
78 // settings by PhabricatorAuthSessionEngine::willServeRequestForUser
79 // which is called from PhabricatorController:willBeginExecution
80 self::setLocaleCode('en_US');
81
82 }
83
84 public static function initializeScriptEnvironment(
85 $config_optional,
86 $no_extensions) {
87 self::initializeCommonEnvironment($config_optional, $no_extensions);
88
89 // Set the default locale for command-line scripts
90 self::setLocaleCode(self::getEnvConfig('locale.command'));
91
92 // If a script has a --locale argument then go through our system for
93 // setting locales
94 PhutilArgumentParser::setLocaleCallback(array(__CLASS__, 'setLocaleCode'));
95
96 // NOTE: This is dangerous in general, but we know we're in a script context
97 // and are not vulnerable to CSRF.
98 AphrontWriteGuard::allowDangerousUnguardedWrites(true);
99
100 // There are several places where we log information (about errors, events,
101 // service calls, etc.) for analysis via DarkConsole or similar. These are
102 // useful for web requests, but grow unboundedly in long-running scripts and
103 // daemons. Discard data as it arrives in these cases.
104 PhutilServiceProfiler::getInstance()->enableDiscardMode();
105 DarkConsoleErrorLogPluginAPI::enableDiscardMode();
106 DarkConsoleEventPluginAPI::enableDiscardMode();
107 }
108
109
110 private static function initializeCommonEnvironment(
111 $config_optional,
112 $no_extensions) {
113 PhutilErrorHandler::initialize();
114
115 self::resetUmask();
116 self::buildConfigurationSourceStack($config_optional, $no_extensions);
117
118 // Force a valid timezone. If both PHP and Phabricator configuration are
119 // invalid, use UTC.
120 $tz = self::getEnvConfig('phabricator.timezone');
121 if ($tz) {
122 @date_default_timezone_set($tz);
123 }
124 $ok = @date_default_timezone_set(date_default_timezone_get());
125 if (!$ok) {
126 date_default_timezone_set('UTC');
127 }
128
129 // Prepend '/support/bin' and append any paths to $PATH if we need to.
130 $env_path = getenv('PATH');
131 $phabricator_path = dirname(phutil_get_library_root('phabricator'));
132 $support_path = $phabricator_path.'/support/bin';
133 $env_path = $support_path.PATH_SEPARATOR.$env_path;
134 $append_dirs = self::getEnvConfig('environment.append-paths');
135 if (!empty($append_dirs)) {
136 $append_path = implode(PATH_SEPARATOR, $append_dirs);
137 $env_path = $env_path.PATH_SEPARATOR.$append_path;
138 }
139 putenv('PATH='.$env_path);
140
141 // Write this back into $_ENV, too, so ExecFuture picks it up when creating
142 // subprocess environments.
143 $_ENV['PATH'] = $env_path;
144
145
146 // If an instance identifier is defined, write it into the environment so
147 // it's available to subprocesses.
148 $instance = self::getEnvConfig('cluster.instance');
149 if (phutil_nonempty_string($instance)) {
150 putenv('PHABRICATOR_INSTANCE='.$instance);
151 $_ENV['PHABRICATOR_INSTANCE'] = $instance;
152 }
153
154 PhabricatorEventEngine::initialize();
155
156 // Load the preamble utility library if we haven't already. On web
157 // requests this loaded earlier, but we want to load it for non-web
158 // requests so that unit tests can call these functions.
159 require_once $phabricator_path.'/support/startup/preamble-utils.php';
160 }
161
162 public static function beginScopedLocale($locale_code) {
163 return new PhabricatorLocaleScopeGuard($locale_code);
164 }
165
166 public static function getLocaleCode() {
167 return self::$localeCode;
168 }
169
170 public static function setLocaleCode($locale_code) {
171 if (!$locale_code) {
172 return;
173 }
174
175 if ($locale_code == self::$localeCode) {
176 return;
177 }
178
179 try {
180 $locale = PhutilLocale::loadLocale($locale_code);
181 $translations = PhutilTranslation::getTranslationMapForLocale(
182 $locale_code);
183
184 $override = self::getEnvConfig('translation.override');
185 if (!is_array($override)) {
186 $override = array();
187 }
188
189 PhutilTranslator::getInstance()
190 ->setLocale($locale)
191 ->setTranslations($override + $translations);
192
193 self::$localeCode = $locale_code;
194 } catch (Exception $ex) {
195 // Just ignore this; the user likely has an out-of-date locale code.
196 }
197 }
198
199 private static function buildConfigurationSourceStack(
200 $config_optional, $no_extensions) {
201 self::dropConfigCache();
202
203 $stack = new PhabricatorConfigStackSource();
204 self::$sourceStack = $stack;
205
206 $default_source = id(new PhabricatorConfigDefaultSource())
207 ->setName(pht('Global Default'));
208 $stack->pushSource($default_source);
209
210 $env = self::getSelectedEnvironmentName();
211 if ($env) {
212 $stack->pushSource(
213 id(new PhabricatorConfigFileSource($env))
214 ->setName(pht("File '%s'", $env)));
215 }
216
217 $stack->pushSource(
218 id(new PhabricatorConfigLocalSource())
219 ->setName(pht('Local Config')));
220
221 // If the install overrides the database adapter, we might need to load
222 // the database adapter class before we can push on the database config.
223 // This config is locked and can't be edited from the web UI anyway.
224 if (!$no_extensions) {
225 foreach (self::getEnvConfig('load-libraries') as $library) {
226 phutil_load_library($library);
227 }
228 }
229
230 // Drop any class map caches, since they will have generated without
231 // any classes from libraries. Without this, preflight setup checks can
232 // cause generation of a setup check cache that omits checks defined in
233 // libraries, for example.
234 PhutilClassMapQuery::deleteCaches();
235
236 // If custom libraries specify config options, they won't get default
237 // values as the Default source has already been loaded, so we get it to
238 // pull in all options from non-phabricator libraries now they are loaded.
239 $default_source->loadExternalOptions();
240
241 // If this install has site config sources, load them now.
242 $site_sources = id(new PhutilClassMapQuery())
243 ->setAncestorClass(PhabricatorConfigSiteSource::class)
244 ->setSortMethod('getPriority')
245 ->execute();
246
247 foreach ($site_sources as $site_source) {
248 $stack->pushSource($site_source);
249
250 // If the site source did anything which reads config, throw it away
251 // to make sure any additional site sources get clean reads.
252 self::dropConfigCache();
253 }
254
255 $masters = PhabricatorDatabaseRef::getMasterDatabaseRefs();
256 if (!$masters) {
257 self::setReadOnly(true, self::READONLY_MASTERLESS);
258 } else {
259 // If any master is severed, we drop to readonly mode. In theory we
260 // could try to continue if we're only missing some applications, but
261 // this is very complex and we're unlikely to get it right.
262
263 foreach ($masters as $master) {
264 // Give severed masters one last chance to get healthy.
265 if ($master->isSevered()) {
266 $master->checkHealth();
267 }
268
269 if ($master->isSevered()) {
270 self::setReadOnly(true, self::READONLY_SEVERED);
271 break;
272 }
273 }
274 }
275
276 try {
277 // See T13403. If we're starting up in "config optional" mode, suppress
278 // messages about connection retries.
279 if ($config_optional) {
280 $database_source = @new PhabricatorConfigDatabaseSource('default');
281 } else {
282 $database_source = new PhabricatorConfigDatabaseSource('default');
283 }
284
285 $database_source->setName(pht('Database'));
286
287 $stack->pushSource($database_source);
288 } catch (AphrontSchemaQueryException $exception) {
289 // If the database is not available, just skip this configuration
290 // source. This happens during `bin/storage upgrade`, `bin/conf` before
291 // schema setup, etc.
292 } catch (PhabricatorClusterStrandedException $ex) {
293 // This means we can't connect to any database host. That's fine as
294 // long as we're running a setup script like `bin/storage`.
295 if (!$config_optional) {
296 throw $ex;
297 }
298 }
299
300 // Drop the config cache one final time to make sure we're getting clean
301 // reads now that we've finished building the stack.
302 self::dropConfigCache();
303 }
304
305 public static function repairConfig($key, $value) {
306 if (!self::$repairSource) {
307 self::$repairSource = id(new PhabricatorConfigDictionarySource(array()))
308 ->setName(pht('Repaired Config'));
309 self::$sourceStack->pushSource(self::$repairSource);
310 }
311 self::$repairSource->setKeys(array($key => $value));
312 self::dropConfigCache();
313 }
314
315 public static function overrideConfig($key, $value) {
316 if (!self::$overrideSource) {
317 self::$overrideSource = id(new PhabricatorConfigDictionarySource(array()))
318 ->setName(pht('Overridden Config'));
319 self::$sourceStack->pushSource(self::$overrideSource);
320 }
321 self::$overrideSource->setKeys(array($key => $value));
322 self::dropConfigCache();
323 }
324
325 public static function getUnrepairedEnvConfig($key, $default = null) {
326 foreach (self::$sourceStack->getStack() as $source) {
327 if ($source === self::$repairSource) {
328 continue;
329 }
330 $result = $source->getKeys(array($key));
331 if ($result) {
332 return $result[$key];
333 }
334 }
335 return $default;
336 }
337
338 public static function getSelectedEnvironmentName() {
339 $env_var = 'PHABRICATOR_ENV';
340
341 $env = idx($_SERVER, $env_var);
342
343 if (!$env) {
344 $env = getenv($env_var);
345 }
346
347 if (!$env) {
348 $env = idx($_ENV, $env_var);
349 }
350
351 if (!$env) {
352 $root = dirname(phutil_get_library_root('phabricator'));
353 $path = $root.'/conf/local/ENVIRONMENT';
354 if (Filesystem::pathExists($path)) {
355 $env = trim(Filesystem::readFile($path));
356 }
357 }
358
359 return $env;
360 }
361
362
363/* -( Reading Configuration )---------------------------------------------- */
364
365
366 /**
367 * Get the current configuration setting for a given key.
368 *
369 * If the key is not found, then throw an Exception.
370 *
371 * @task read
372 */
373 public static function getEnvConfig($key) {
374 if (!self::$sourceStack) {
375 throw new Exception(
376 pht(
377 'Trying to read configuration "%s" before configuration has been '.
378 'initialized.',
379 $key));
380 }
381
382 if (isset(self::$cache[$key])) {
383 return self::$cache[$key];
384 }
385
386 if (array_key_exists($key, self::$cache)) {
387 return self::$cache[$key];
388 }
389
390 $result = self::$sourceStack->getKeys(array($key));
391 if (array_key_exists($key, $result)) {
392 self::$cache[$key] = $result[$key];
393 return $result[$key];
394 } else {
395 throw new Exception(
396 pht(
397 "No config value specified for key '%s'.",
398 $key));
399 }
400 }
401
402 /**
403 * Get the current configuration setting for a given key. If the key
404 * does not exist, return a default value instead of throwing. This is
405 * primarily useful for migrations involving keys which are slated for
406 * removal.
407 *
408 * @task read
409 */
410 public static function getEnvConfigIfExists($key, $default = null) {
411 try {
412 return self::getEnvConfig($key);
413 } catch (Exception $ex) {
414 return $default;
415 }
416 }
417
418
419 /**
420 * Get the fully-qualified URI for a path.
421 *
422 * @task read
423 */
424 public static function getURI($path) {
425 return rtrim(self::getAnyBaseURI(), '/').$path;
426 }
427
428
429 /**
430 * Get the fully-qualified production URI for a path.
431 *
432 * @task read
433 */
434 public static function getProductionURI($path) {
435 // If we're passed a URI which already has a domain, simply return it
436 // unmodified. In particular, files may have URIs which point to a CDN
437 // domain.
438 $uri = new PhutilURI($path);
439 if ($uri->getDomain()) {
440 return $path;
441 }
442
443 $production_domain = self::getEnvConfig('phabricator.production-uri');
444 if (!$production_domain) {
445 $production_domain = self::getAnyBaseURI();
446 }
447 return rtrim($production_domain, '/').$path;
448 }
449
450
451 public static function isSelfURI($raw_uri) {
452 $uri = new PhutilURI($raw_uri);
453
454 $host = $uri->getDomain();
455 if (!phutil_nonempty_string($host)) {
456 return true;
457 }
458
459 $host = phutil_utf8_strtolower($host);
460
461 $self_map = self::getSelfURIMap();
462 return isset($self_map[$host]);
463 }
464
465 private static function getSelfURIMap() {
466 $self_uris = array();
467 $self_uris[] = self::getProductionURI('/');
468 $self_uris[] = self::getURI('/');
469
470 $allowed_uris = self::getEnvConfig('phabricator.allowed-uris');
471 foreach ($allowed_uris as $allowed_uri) {
472 $self_uris[] = $allowed_uri;
473 }
474
475 $self_map = array();
476 foreach ($self_uris as $self_uri) {
477 $host = id(new PhutilURI($self_uri))->getDomain();
478 if (!phutil_nonempty_string($host)) {
479 continue;
480 }
481
482 $host = phutil_utf8_strtolower($host);
483 $self_map[$host] = $host;
484 }
485
486 return $self_map;
487 }
488
489 /**
490 * Get the fully-qualified production URI for a static resource path.
491 *
492 * @task read
493 */
494 public static function getCDNURI($path) {
495 $alt = self::getEnvConfig('security.alternate-file-domain');
496 if (!$alt) {
497 $alt = self::getAnyBaseURI();
498 }
499 $uri = new PhutilURI($alt);
500 $uri->setPath($path);
501 return (string)$uri;
502 }
503
504
505 /**
506 * Get the fully-qualified production URI for a documentation resource.
507 *
508 * @task read
509 */
510 public static function getDoclink($resource, $type = 'article') {
511 $params = array(
512 'name' => $resource,
513 'type' => $type,
514 'jump' => true,
515 );
516
517 $uri = new PhutilURI(
518 'https://we.phorge.it/diviner/find/',
519 $params);
520
521 return phutil_string_cast($uri);
522 }
523
524
525 /**
526 * Build a concrete object from a configuration key.
527 *
528 * @task read
529 */
530 public static function newObjectFromConfig($key, $args = array()) {
531 $class = self::getEnvConfig($key);
532 return newv($class, $args);
533 }
534
535 public static function getAnyBaseURI() {
536 $base_uri = self::getEnvConfig('phabricator.base-uri');
537
538 if (!$base_uri) {
539 $base_uri = self::getRequestBaseURI();
540 }
541
542 if (!$base_uri) {
543 throw new Exception(
544 pht(
545 "Define '%s' in your configuration to continue.",
546 'phabricator.base-uri'));
547 }
548
549 return $base_uri;
550 }
551
552 public static function getRequestBaseURI() {
553 return self::$requestBaseURI;
554 }
555
556 public static function setRequestBaseURI($uri) {
557 self::$requestBaseURI = $uri;
558 }
559
560 public static function isReadOnly() {
561 if (self::$readOnly !== null) {
562 return self::$readOnly;
563 }
564 return self::getEnvConfig('cluster.read-only');
565 }
566
567 public static function setReadOnly($read_only, $reason) {
568 self::$readOnly = $read_only;
569 self::$readOnlyReason = $reason;
570 }
571
572 public static function getReadOnlyMessage() {
573 $reason = self::getReadOnlyReason();
574 switch ($reason) {
575 case self::READONLY_MASTERLESS:
576 return pht(
577 'This server is in read-only mode (no writable database '.
578 'is configured).');
579 case self::READONLY_UNREACHABLE:
580 return pht(
581 'This server is in read-only mode (unreachable master).');
582 case self::READONLY_SEVERED:
583 return pht(
584 'This server is in read-only mode (major interruption).');
585 }
586
587 return pht('This server is in read-only mode.');
588 }
589
590 public static function getReadOnlyURI() {
591 return urisprintf(
592 '/readonly/%s/',
593 self::getReadOnlyReason());
594 }
595
596 public static function getReadOnlyReason() {
597 if (!self::isReadOnly()) {
598 return null;
599 }
600
601 if (self::$readOnlyReason !== null) {
602 return self::$readOnlyReason;
603 }
604
605 return self::READONLY_CONFIG;
606 }
607
608
609/* -( Unit Test Support )-------------------------------------------------- */
610
611
612 /**
613 * @task test
614 */
615 public static function beginScopedEnv() {
616 return new PhabricatorScopedEnv(self::pushTestEnvironment());
617 }
618
619
620 /**
621 * @task test
622 */
623 private static function pushTestEnvironment() {
624 self::dropConfigCache();
625 $source = new PhabricatorConfigDictionarySource(array());
626 self::$sourceStack->pushSource($source);
627 return spl_object_hash($source);
628 }
629
630
631 /**
632 * @task test
633 */
634 public static function popTestEnvironment($key) {
635 self::dropConfigCache();
636 $source = self::$sourceStack->popSource();
637 $stack_key = spl_object_hash($source);
638 if ($stack_key !== $key) {
639 self::$sourceStack->pushSource($source);
640 throw new Exception(
641 pht(
642 'Scoped environments were destroyed in a different order than they '.
643 'were initialized.'));
644 }
645 }
646
647
648/* -( URI Validation )----------------------------------------------------- */
649
650
651 /**
652 * Detect if a URI satisfies either @{method:isValidLocalURIForLink} or
653 * @{method:isValidRemoteURIForLink}, i.e. is a page on this server or the
654 * URI of some other resource which has a valid protocol. This rejects
655 * garbage URIs and URIs with protocols which do not appear in the
656 * `uri.allowed-protocols` configuration, notably 'javascript:' URIs.
657 *
658 * NOTE: This method is generally intended to reject URIs which it may be
659 * unsafe to put in an "href" link attribute.
660 *
661 * @param string $uri URI to test.
662 * @return bool True if the URI identifies a web resource.
663 * @task uri
664 */
665 public static function isValidURIForLink($uri) {
666 return self::isValidLocalURIForLink($uri) ||
667 self::isValidRemoteURIForLink($uri);
668 }
669
670
671 /**
672 * Detect if a URI identifies some page on this server.
673 *
674 * NOTE: This method is generally intended to reject URIs which it may be
675 * unsafe to issue a "Location:" redirect to.
676 *
677 * @param string $uri URI to test.
678 * @return bool True if the URI identifies a local page.
679 * @task uri
680 */
681 public static function isValidLocalURIForLink($uri) {
682 $uri = (string)$uri;
683
684 if (!phutil_nonempty_string($uri)) {
685 return false;
686 }
687
688 if (preg_match('/\s/', $uri)) {
689 // PHP hasn't been vulnerable to header injection attacks for a bunch of
690 // years, but we can safely reject these anyway since they're never valid.
691 return false;
692 }
693
694 // Chrome (at a minimum) interprets backslashes in Location headers and the
695 // URL bar as forward slashes. This is probably intended to reduce user
696 // error caused by confusion over which key is "forward slash" vs "back
697 // slash".
698 //
699 // However, it means a URI like "/\evil.com" is interpreted like
700 // "//evil.com", which is a protocol relative remote URI.
701 //
702 // Since we currently never generate URIs with backslashes in them, reject
703 // these unconditionally rather than trying to figure out how browsers will
704 // interpret them.
705 if (preg_match('/\\\\/', $uri)) {
706 return false;
707 }
708
709 // Valid URIs must begin with '/', followed by the end of the string or some
710 // other non-'/' character. This rejects protocol-relative URIs like
711 // "//evil.com/evil_stuff/".
712 return (bool)preg_match('@^/([^/]|$)@', $uri);
713 }
714
715
716 /**
717 * Detect if a URI identifies some valid linkable remote resource.
718 *
719 * @param string $uri URI to test.
720 * @return bool True if a URI identifies a remote resource with an allowed
721 * protocol.
722 * @task uri
723 */
724 public static function isValidRemoteURIForLink($uri) {
725 try {
726 self::requireValidRemoteURIForLink($uri);
727 return true;
728 } catch (Exception $ex) {
729 return false;
730 }
731 }
732
733
734 /**
735 * Detect if a URI identifies a valid linkable remote resource, throwing a
736 * detailed message if it does not.
737 *
738 * A valid linkable remote resource can be safely linked or redirected to.
739 * This is primarily a protocol whitelist check.
740 *
741 * @param string $raw_uri URI to test.
742 * @return void
743 * @task uri
744 */
745 public static function requireValidRemoteURIForLink($raw_uri) {
746 $uri = new PhutilURI($raw_uri);
747
748 $proto = $uri->getProtocol();
749 if (!$proto) {
750 throw new Exception(
751 pht(
752 'URI "%s" is not a valid linkable resource. A valid linkable '.
753 'resource URI must specify a protocol.',
754 $raw_uri));
755 }
756
757 $protocols = self::getEnvConfig('uri.allowed-protocols');
758 if (!isset($protocols[$proto])) {
759 throw new Exception(
760 pht(
761 'URI "%s" is not a valid linkable resource. A valid linkable '.
762 'resource URI must use one of these protocols: %s.',
763 $raw_uri,
764 implode(', ', array_keys($protocols))));
765 }
766
767 $domain = $uri->getDomain();
768 if (!$domain) {
769 throw new Exception(
770 pht(
771 'URI "%s" is not a valid linkable resource. A valid linkable '.
772 'resource URI must specify a domain.',
773 $raw_uri));
774 }
775 }
776
777
778 /**
779 * Detect if a URI identifies a valid fetchable remote resource.
780 *
781 * @param string $uri URI to test.
782 * @param list<string> $protocols Allowed protocols.
783 * @return bool True if the URI is a valid fetchable remote resource.
784 * @task uri
785 */
786 public static function isValidRemoteURIForFetch($uri, array $protocols) {
787 try {
788 self::requireValidRemoteURIForFetch($uri, $protocols);
789 return true;
790 } catch (Exception $ex) {
791 return false;
792 }
793 }
794
795
796 /**
797 * Detect if a URI identifies a valid fetchable remote resource, throwing
798 * a detailed message if it does not.
799 *
800 * A valid fetchable remote resource can be safely fetched using a request
801 * originating on this server. This is a primarily an address check against
802 * the outbound address blacklist.
803 *
804 * @param string $raw_uri URI to test.
805 * @param list<string> $protocols Allowed protocols.
806 * @return array<string, string> Pre-resolved URI and domain.
807 * @task uri
808 */
809 public static function requireValidRemoteURIForFetch(
810 $raw_uri,
811 array $protocols) {
812
813 $uri = new PhutilURI($raw_uri);
814
815 $proto = $uri->getProtocol();
816 if (!$proto) {
817 throw new Exception(
818 pht(
819 'URI "%s" is not a valid fetchable resource. A valid fetchable '.
820 'resource URI must specify a protocol.',
821 $raw_uri));
822 }
823
824 $protocols = array_fuse($protocols);
825 if (!isset($protocols[$proto])) {
826 throw new Exception(
827 pht(
828 'URI "%s" is not a valid fetchable resource. A valid fetchable '.
829 'resource URI must use one of these protocols: %s.',
830 $raw_uri,
831 implode(', ', array_keys($protocols))));
832 }
833
834 $domain = $uri->getDomain();
835 if (!$domain) {
836 throw new Exception(
837 pht(
838 'URI "%s" is not a valid fetchable resource. A valid fetchable '.
839 'resource URI must specify a domain.',
840 $raw_uri));
841 }
842
843 $addresses = gethostbynamel($domain);
844 if (!$addresses) {
845 throw new Exception(
846 pht(
847 'URI "%s" is not a valid fetchable resource. The domain "%s" could '.
848 'not be resolved.',
849 $raw_uri,
850 $domain));
851 }
852
853 foreach ($addresses as $address) {
854 if (self::isBlacklistedOutboundAddress($address)) {
855 throw new Exception(
856 pht(
857 'URI "%s" is not a valid fetchable resource. The domain "%s" '.
858 'resolves to the address "%s", which is blacklisted for '.
859 'outbound requests.',
860 $raw_uri,
861 $domain,
862 $address));
863 }
864 }
865
866 $resolved_uri = clone $uri;
867 $resolved_uri->setDomain(head($addresses));
868
869 return array($resolved_uri, $domain);
870 }
871
872
873 /**
874 * Determine if an IP address is in the outbound address blacklist.
875 *
876 * @param string $address IP address.
877 * @return bool True if the address is blacklisted.
878 */
879 public static function isBlacklistedOutboundAddress($address) {
880 $blacklist = self::getEnvConfig('security.outbound-blacklist');
881
882 return PhutilCIDRList::newList($blacklist)->containsAddress($address);
883 }
884
885 public static function isClusterRemoteAddress() {
886 $cluster_addresses = self::getEnvConfig('cluster.addresses');
887 if (!$cluster_addresses) {
888 return false;
889 }
890
891 $address = self::getRemoteAddress();
892 if (!$address) {
893 throw new Exception(
894 pht(
895 'Unable to test remote address against cluster whitelist: '.
896 'REMOTE_ADDR is not defined or not valid.'));
897 }
898
899 return self::isClusterAddress($address);
900 }
901
902 public static function isClusterAddress($address) {
903 $cluster_addresses = self::getEnvConfig('cluster.addresses');
904 if (!$cluster_addresses) {
905 throw new Exception(
906 pht(
907 'This server is not configured to serve cluster requests. '.
908 'Set `cluster.addresses` in the configuration to whitelist '.
909 'cluster hosts before sending requests that use a cluster '.
910 'authentication mechanism.'));
911 }
912
913 return PhutilCIDRList::newList($cluster_addresses)
914 ->containsAddress($address);
915 }
916
917 public static function getRemoteAddress() {
918 $address = idx($_SERVER, 'REMOTE_ADDR');
919 if (!$address) {
920 return null;
921 }
922
923 try {
924 return PhutilIPAddress::newAddress($address);
925 } catch (Exception $ex) {
926 return null;
927 }
928 }
929
930/* -( Internals )---------------------------------------------------------- */
931
932
933 /**
934 * @task internal
935 */
936 public static function envConfigExists($key) {
937 return array_key_exists($key, self::$sourceStack->getKeys(array($key)));
938 }
939
940
941 /**
942 * @task internal
943 */
944 public static function getAllConfigKeys() {
945 return self::$sourceStack->getAllKeys();
946 }
947
948 public static function getConfigSourceStack() {
949 return self::$sourceStack;
950 }
951
952 /**
953 * @task internal
954 */
955 public static function overrideTestEnvConfig($stack_key, $key, $value) {
956 $tmp = array();
957
958 // If we don't have the right key, we'll throw when popping the last
959 // source off the stack.
960 do {
961 $source = self::$sourceStack->popSource();
962 array_unshift($tmp, $source);
963 if (spl_object_hash($source) == $stack_key) {
964 $source->setKeys(array($key => $value));
965 break;
966 }
967 } while (true);
968
969 foreach ($tmp as $source) {
970 self::$sourceStack->pushSource($source);
971 }
972
973 self::dropConfigCache();
974 }
975
976 private static function dropConfigCache() {
977 self::$cache = array();
978 }
979
980 private static function resetUmask() {
981 // Reset the umask to the common standard umask. The umask controls default
982 // permissions when files are created and propagates to subprocesses.
983
984 // "022" is the most common umask, but sometimes it is set to something
985 // unusual by the calling environment.
986
987 // Since various things rely on this umask to work properly and we are
988 // not aware of any legitimate reasons to adjust it, unconditionally
989 // normalize it until such reasons arise. See T7475 for discussion.
990 umask(022);
991 }
992
993
994 /**
995 * Get the path to an empty directory which is readable by all of the system
996 * user accounts that Phabricator acts as.
997 *
998 * In some cases, a binary needs some valid HOME or CWD to continue, but not
999 * all user accounts have valid home directories and even if they do they
1000 * may not be readable after a `sudo` operation.
1001 *
1002 * @return string Path to an empty directory suitable for use as a CWD.
1003 */
1004 public static function getEmptyCWD() {
1005 $root = dirname(phutil_get_library_root('phabricator'));
1006 return $root.'/support/empty/';
1007 }
1008
1009
1010}