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