Remove product literal strings in "pht()", part 6
[phabricator.git] / support / startup / PhabricatorStartup.php
blob58031013ad37148af5309580d0f84dc9afee3068
1 <?php
3 /**
4 * Handle request startup, before loading the environment or libraries. This
5 * class bootstraps the request state up to the point where we can enter
6 * Phabricator code.
8 * NOTE: This class MUST NOT have any dependencies. It runs before libraries
9 * load.
11 * Rate Limiting
12 * =============
14 * Phabricator limits the rate at which clients can request pages, and issues
15 * HTTP 429 "Too Many Requests" responses if clients request too many pages too
16 * quickly. Although this is not a complete defense against high-volume attacks,
17 * it can protect an install against aggressive crawlers, security scanners,
18 * and some types of malicious activity.
20 * To perform rate limiting, each page increments a score counter for the
21 * requesting user's IP. The page can give the IP more points for an expensive
22 * request, or fewer for an authetnicated request.
24 * Score counters are kept in buckets, and writes move to a new bucket every
25 * minute. After a few minutes (defined by @{method:getRateLimitBucketCount}),
26 * the oldest bucket is discarded. This provides a simple mechanism for keeping
27 * track of scores without needing to store, access, or read very much data.
29 * Users are allowed to accumulate up to 1000 points per minute, averaged across
30 * all of the tracked buckets.
32 * @task info Accessing Request Information
33 * @task hook Startup Hooks
34 * @task apocalypse In Case Of Apocalypse
35 * @task validation Validation
36 * @task ratelimit Rate Limiting
37 * @task phases Startup Phase Timers
38 * @task request-path Request Path
40 final class PhabricatorStartup {
42 private static $startTime;
43 private static $debugTimeLimit;
44 private static $accessLog;
45 private static $capturingOutput;
46 private static $rawInput;
47 private static $oldMemoryLimit;
48 private static $phases;
50 private static $limits = array();
51 private static $requestPath;
54 /* -( Accessing Request Information )-------------------------------------- */
57 /**
58 * @task info
60 public static function getStartTime() {
61 return self::$startTime;
65 /**
66 * @task info
68 public static function getMicrosecondsSinceStart() {
69 // This is the same as "phutil_microseconds_since()", but we may not have
70 // loaded libraries yet.
71 return (int)(1000000 * (microtime(true) - self::getStartTime()));
75 /**
76 * @task info
78 public static function setAccessLog($access_log) {
79 self::$accessLog = $access_log;
83 /**
84 * @task info
86 public static function getRawInput() {
87 if (self::$rawInput === null) {
88 $stream = new AphrontRequestStream();
90 if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
91 $encoding = trim($_SERVER['HTTP_CONTENT_ENCODING']);
92 $stream->setEncoding($encoding);
95 $input = '';
96 do {
97 $bytes = $stream->readData();
98 if ($bytes === null) {
99 break;
101 $input .= $bytes;
102 } while (true);
104 self::$rawInput = $input;
107 return self::$rawInput;
111 /* -( Startup Hooks )------------------------------------------------------ */
115 * @param float Request start time, from `microtime(true)`.
116 * @task hook
118 public static function didStartup($start_time) {
119 self::$startTime = $start_time;
121 self::$phases = array();
123 self::$accessLog = null;
124 self::$requestPath = null;
126 static $registered;
127 if (!$registered) {
128 // NOTE: This protects us against multiple calls to didStartup() in the
129 // same request, but also against repeated requests to the same
130 // interpreter state, which we may implement in the future.
131 register_shutdown_function(array(__CLASS__, 'didShutdown'));
132 $registered = true;
135 self::setupPHP();
136 self::verifyPHP();
138 // If we've made it this far, the environment isn't completely broken so
139 // we can switch over to relying on our own exception recovery mechanisms.
140 ini_set('display_errors', 0);
142 self::connectRateLimits();
144 self::normalizeInput();
146 self::readRequestPath();
148 self::beginOutputCapture();
153 * @task hook
155 public static function didShutdown() {
156 // Disconnect any active rate limits before we shut down. If we don't do
157 // this, requests which exit early will lock a slot in any active
158 // connection limits, and won't count for rate limits.
159 self::disconnectRateLimits(array());
161 $event = error_get_last();
163 if (!$event) {
164 return;
167 switch ($event['type']) {
168 case E_ERROR:
169 case E_PARSE:
170 case E_COMPILE_ERROR:
171 break;
172 default:
173 return;
176 $msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
177 if ($event) {
178 // Even though we should be emitting this as text-plain, escape things
179 // just to be sure since we can't really be sure what the program state
180 // is when we get here.
181 $msg .= htmlspecialchars(
182 $event['message']."\n\n".$event['file'].':'.$event['line'],
183 ENT_QUOTES,
184 'UTF-8');
187 // flip dem tables
188 $msg .= "\n\n\n";
189 $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf".
190 "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20".
191 "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb";
193 self::didFatal($msg);
196 public static function loadCoreLibraries() {
197 $phabricator_root = dirname(dirname(dirname(__FILE__)));
198 $libraries_root = dirname($phabricator_root);
200 $root = null;
201 if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
202 $root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
205 ini_set(
206 'include_path',
207 $libraries_root.PATH_SEPARATOR.ini_get('include_path'));
209 $ok = @include_once $root.'arcanist/src/init/init-library.php';
210 if (!$ok) {
211 self::didFatal(
212 'Unable to load the "Arcanist" library. Put "arcanist/" next to '.
213 '"phabricator/" on disk.');
216 // Load Phabricator itself using the absolute path, so we never end up doing
217 // anything surprising (loading index.php and libraries from different
218 // directories).
219 phutil_load_library($phabricator_root.'/src');
222 /* -( Output Capture )----------------------------------------------------- */
225 public static function beginOutputCapture() {
226 if (self::$capturingOutput) {
227 self::didFatal('Already capturing output!');
229 self::$capturingOutput = true;
230 ob_start();
234 public static function endOutputCapture() {
235 if (!self::$capturingOutput) {
236 return null;
238 self::$capturingOutput = false;
239 return ob_get_clean();
243 /* -( Debug Time Limit )--------------------------------------------------- */
247 * Set a time limit (in seconds) for the current script. After time expires,
248 * the script fatals.
250 * This works like `max_execution_time`, but prints out a useful stack trace
251 * when the time limit expires. This is primarily intended to make it easier
252 * to debug pages which hang by allowing extraction of a stack trace: set a
253 * short debug limit, then use the trace to figure out what's happening.
255 * The limit is implemented with a tick function, so enabling it implies
256 * some accounting overhead.
258 * @param int Time limit in seconds.
259 * @return void
261 public static function setDebugTimeLimit($limit) {
262 self::$debugTimeLimit = $limit;
264 static $initialized;
265 if (!$initialized) {
266 declare(ticks=1);
267 register_tick_function(array(__CLASS__, 'onDebugTick'));
273 * Callback tick function used by @{method:setDebugTimeLimit}.
275 * Fatals with a useful stack trace after the time limit expires.
277 * @return void
279 public static function onDebugTick() {
280 $limit = self::$debugTimeLimit;
281 if (!$limit) {
282 return;
285 $elapsed = (microtime(true) - self::getStartTime());
286 if ($elapsed > $limit) {
287 $frames = array();
288 foreach (debug_backtrace() as $frame) {
289 $file = isset($frame['file']) ? $frame['file'] : '-';
290 $file = basename($file);
292 $line = isset($frame['line']) ? $frame['line'] : '-';
293 $class = isset($frame['class']) ? $frame['class'].'->' : null;
294 $func = isset($frame['function']) ? $frame['function'].'()' : '?';
296 $frames[] = "{$file}:{$line} {$class}{$func}";
299 self::didFatal(
300 "Request aborted by debug time limit after {$limit} seconds.\n\n".
301 "STACK TRACE\n".
302 implode("\n", $frames));
307 /* -( In Case of Apocalypse )---------------------------------------------- */
311 * Fatal the request completely in response to an exception, sending a plain
312 * text message to the client. Calls @{method:didFatal} internally.
314 * @param string Brief description of the exception context, like
315 * `"Rendering Exception"`.
316 * @param Throwable The exception itself.
317 * @param bool True if it's okay to show the exception's stack trace
318 * to the user. The trace will always be logged.
319 * @return exit This method **does not return**.
321 * @task apocalypse
323 public static function didEncounterFatalException(
324 $note,
325 $ex,
326 $show_trace) {
328 $message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage();
330 $full_message = $message;
331 $full_message .= "\n\n";
332 $full_message .= $ex->getTraceAsString();
334 if ($show_trace) {
335 $message = $full_message;
338 self::didFatal($message, $full_message);
343 * Fatal the request completely, sending a plain text message to the client.
345 * @param string Plain text message to send to the client.
346 * @param string Plain text message to send to the error log. If not
347 * provided, the client message is used. You can pass a more
348 * detailed message here (e.g., with stack traces) to avoid
349 * showing it to users.
350 * @return exit This method **does not return**.
352 * @task apocalypse
354 public static function didFatal($message, $log_message = null) {
355 if ($log_message === null) {
356 $log_message = $message;
359 self::endOutputCapture();
360 $access_log = self::$accessLog;
361 if ($access_log) {
362 // We may end up here before the access log is initialized, e.g. from
363 // verifyPHP().
364 $access_log->setData(
365 array(
366 'c' => 500,
368 $access_log->write();
371 header(
372 'Content-Type: text/plain; charset=utf-8',
373 $replace = true,
374 $http_error = 500);
376 error_log($log_message);
377 echo $message."\n";
379 exit(1);
383 /* -( Validation )--------------------------------------------------------- */
387 * @task validation
389 private static function setupPHP() {
390 error_reporting(E_ALL | E_STRICT);
391 self::$oldMemoryLimit = ini_get('memory_limit');
392 ini_set('memory_limit', -1);
394 // If we have libxml, disable the incredibly dangerous entity loader.
395 // PHP 8 deprecates this function and disables this by default; remove once
396 // PHP 7 is no longer supported or a future version has removed the function
397 // entirely.
398 if (function_exists('libxml_disable_entity_loader')) {
399 @libxml_disable_entity_loader(true);
402 // See T13060. If the locale for this process (the parent process) is not
403 // a UTF-8 locale we can encounter problems when launching subprocesses
404 // which receive UTF-8 parameters in their command line argument list.
405 @setlocale(LC_ALL, 'en_US.UTF-8');
407 $config_map = array(
408 // See PHI1894. Keep "args" in exception backtraces.
409 'zend.exception_ignore_args' => 0,
411 // See T13100. We'd like the regex engine to fail, rather than segfault,
412 // if handed a pathological regular expression.
413 'pcre.backtrack_limit' => 10000,
414 'pcre.recusion_limit' => 10000,
416 // NOTE: Arcanist applies a similar set of startup options for CLI
417 // environments in "init-script.php". Changes here may also be
418 // appropriate to apply there.
421 foreach ($config_map as $config_key => $config_value) {
422 ini_set($config_key, $config_value);
428 * @task validation
430 public static function getOldMemoryLimit() {
431 return self::$oldMemoryLimit;
435 * @task validation
437 private static function normalizeInput() {
438 // Replace superglobals with unfiltered versions, disrespect php.ini (we
439 // filter ourselves).
441 // NOTE: We don't filter INPUT_SERVER because we don't want to overwrite
442 // changes made in "preamble.php".
444 // NOTE: WE don't filter INPUT_POST because we may be constructing it
445 // lazily if "enable_post_data_reading" is disabled.
447 $filter = array(
448 INPUT_GET,
449 INPUT_ENV,
450 INPUT_COOKIE,
452 foreach ($filter as $type) {
453 $filtered = filter_input_array($type, FILTER_UNSAFE_RAW);
454 if (!is_array($filtered)) {
455 continue;
457 switch ($type) {
458 case INPUT_GET:
459 $_GET = array_merge($_GET, $filtered);
460 break;
461 case INPUT_COOKIE:
462 $_COOKIE = array_merge($_COOKIE, $filtered);
463 break;
464 case INPUT_ENV;
465 $env = array_merge($_ENV, $filtered);
466 $_ENV = self::filterEnvSuperglobal($env);
467 break;
471 self::rebuildRequest();
475 * @task validation
477 public static function rebuildRequest() {
478 // Rebuild $_REQUEST, respecting order declared in ".ini" files.
479 $order = ini_get('request_order');
481 if (!$order) {
482 $order = ini_get('variables_order');
485 if (!$order) {
486 // $_REQUEST will be empty, so leave it alone.
487 return;
490 $_REQUEST = array();
491 for ($ii = 0; $ii < strlen($order); $ii++) {
492 switch ($order[$ii]) {
493 case 'G':
494 $_REQUEST = array_merge($_REQUEST, $_GET);
495 break;
496 case 'P':
497 $_REQUEST = array_merge($_REQUEST, $_POST);
498 break;
499 case 'C':
500 $_REQUEST = array_merge($_REQUEST, $_COOKIE);
501 break;
502 default:
503 // $_ENV and $_SERVER never go into $_REQUEST.
504 break;
511 * Adjust `$_ENV` before execution.
513 * Adjustments here primarily impact the environment as seen by subprocesses.
514 * The environment is forwarded explicitly by @{class:ExecFuture}.
516 * @param map<string, wild> Input `$_ENV`.
517 * @return map<string, string> Suitable `$_ENV`.
518 * @task validation
520 private static function filterEnvSuperglobal(array $env) {
522 // In some configurations, we may get "argc" and "argv" set in $_ENV.
523 // These are not real environmental variables, and "argv" may have an array
524 // value which can not be forwarded to subprocesses. Remove these from the
525 // environment if they are present.
526 unset($env['argc']);
527 unset($env['argv']);
529 return $env;
534 * @task validation
536 private static function verifyPHP() {
537 $required_version = '5.2.3';
538 if (version_compare(PHP_VERSION, $required_version) < 0) {
539 self::didFatal(
540 "You are running PHP version '".PHP_VERSION."', which is older than ".
541 "the minimum version, '{$required_version}'. Update to at least ".
542 "'{$required_version}'.");
545 if (function_exists('get_magic_quotes_gpc')) {
546 if (@get_magic_quotes_gpc()) {
547 self::didFatal(
548 'Your server is configured with the PHP language feature '.
549 '"magic_quotes_gpc" enabled.'.
550 "\n\n".
551 'This feature is "highly discouraged" by PHP\'s developers, and '.
552 'has been removed entirely in PHP8.'.
553 "\n\n".
554 'You must disable "magic_quotes_gpc" to run Phabricator. Consult '.
555 'the PHP manual for instructions.');
559 if (extension_loaded('apc')) {
560 $apc_version = phpversion('apc');
561 $known_bad = array(
562 '3.1.14' => true,
563 '3.1.15' => true,
564 '3.1.15-dev' => true,
566 if (isset($known_bad[$apc_version])) {
567 self::didFatal(
568 "You have APC {$apc_version} installed. This version of APC is ".
569 "known to be bad, and does not work with Phabricator (it will ".
570 "cause Phabricator to fatal unrecoverably with nonsense errors). ".
571 "Downgrade to version 3.1.13.");
575 if (isset($_SERVER['HTTP_PROXY'])) {
576 self::didFatal(
577 'This HTTP request included a "Proxy:" header, poisoning the '.
578 'environment (CVE-2016-5385 / httpoxy). Declining to process this '.
579 'request. For details, see: https://phurl.io/u/httpoxy');
585 * @task request-path
587 private static function readRequestPath() {
589 // See T13575. The request path may be provided in:
591 // - the "$_GET" parameter "__path__" (normal for Apache and nginx); or
592 // - the "$_SERVER" parameter "REQUEST_URI" (normal for the PHP builtin
593 // webserver).
595 // Locate it wherever it is, and store it for later use. Note that writing
596 // to "$_REQUEST" here won't always work, because later code may rebuild
597 // "$_REQUEST" from other sources.
599 if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) {
600 self::setRequestPath($_REQUEST['__path__']);
601 return;
604 // Compatibility with PHP 5.4+ built-in web server.
605 if (php_sapi_name() == 'cli-server') {
606 $path = parse_url($_SERVER['REQUEST_URI']);
607 self::setRequestPath($path['path']);
608 return;
611 if (!isset($_REQUEST['__path__'])) {
612 self::didFatal(
613 "Request parameter '__path__' is not set. Your rewrite rules ".
614 "are not configured correctly.");
617 if (!strlen($_REQUEST['__path__'])) {
618 self::didFatal(
619 "Request parameter '__path__' is set, but empty. Your rewrite rules ".
620 "are not configured correctly. The '__path__' should always ".
621 "begin with a '/'.");
626 * @task request-path
628 public static function getRequestPath() {
629 $path = self::$requestPath;
631 if ($path === null) {
632 self::didFatal(
633 'Request attempted to access request path, but no request path is '.
634 'available for this request. You may be calling web request code '.
635 'from a non-request context, or your webserver may not be passing '.
636 'a request path to Phabricator in a format that it understands.');
639 return $path;
643 * @task request-path
645 public static function setRequestPath($path) {
646 self::$requestPath = $path;
650 /* -( Rate Limiting )------------------------------------------------------ */
654 * Add a new client limits.
656 * @param PhabricatorClientLimit New limit.
657 * @return PhabricatorClientLimit The limit.
659 public static function addRateLimit(PhabricatorClientLimit $limit) {
660 self::$limits[] = $limit;
661 return $limit;
666 * Apply configured rate limits.
668 * If any limit is exceeded, this method terminates the request.
670 * @return void
671 * @task ratelimit
673 private static function connectRateLimits() {
674 $limits = self::$limits;
676 $reason = null;
677 $connected = array();
678 foreach ($limits as $limit) {
679 $reason = $limit->didConnect();
680 $connected[] = $limit;
681 if ($reason !== null) {
682 break;
686 // If we're killing the request here, disconnect any limits that we
687 // connected to try to keep the accounting straight.
688 if ($reason !== null) {
689 foreach ($connected as $limit) {
690 $limit->didDisconnect(array());
693 self::didRateLimit($reason);
699 * Tear down rate limiting and allow limits to score the request.
701 * @param map<string, wild> Additional, freeform request state.
702 * @return void
703 * @task ratelimit
705 public static function disconnectRateLimits(array $request_state) {
706 $limits = self::$limits;
708 // Remove all limits before disconnecting them so this works properly if
709 // it runs twice. (We run this automatically as a shutdown handler.)
710 self::$limits = array();
712 foreach ($limits as $limit) {
713 $limit->didDisconnect($request_state);
720 * Emit an HTTP 429 "Too Many Requests" response (indicating that the user
721 * has exceeded application rate limits) and exit.
723 * @return exit This method **does not return**.
724 * @task ratelimit
726 private static function didRateLimit($reason) {
727 header(
728 'Content-Type: text/plain; charset=utf-8',
729 $replace = true,
730 $http_error = 429);
732 echo $reason;
734 exit(1);
738 /* -( Startup Timers )----------------------------------------------------- */
742 * Record the beginning of a new startup phase.
744 * For phases which occur before @{class:PhabricatorStartup} loads, save the
745 * time and record it with @{method:recordStartupPhase} after the class is
746 * available.
748 * @param string Phase name.
749 * @task phases
751 public static function beginStartupPhase($phase) {
752 self::recordStartupPhase($phase, microtime(true));
757 * Record the start time of a previously executed startup phase.
759 * For startup phases which occur after @{class:PhabricatorStartup} loads,
760 * use @{method:beginStartupPhase} instead. This method can be used to
761 * record a time before the class loads, then hand it over once the class
762 * becomes available.
764 * @param string Phase name.
765 * @param float Phase start time, from `microtime(true)`.
766 * @task phases
768 public static function recordStartupPhase($phase, $time) {
769 self::$phases[$phase] = $time;
774 * Get information about startup phase timings.
776 * Sometimes, performance problems can occur before we start the profiler.
777 * Since the profiler can't examine these phases, it isn't useful in
778 * understanding their performance costs.
780 * Instead, the startup process marks when it enters various phases using
781 * @{method:beginStartupPhase}. A later call to this method can retrieve this
782 * information, which can be examined to gain greater insight into where
783 * time was spent. The output is still crude, but better than nothing.
785 * @task phases
787 public static function getPhases() {
788 return self::$phases;