 b9a1260ef5
			
		
	
	b9a1260ef5
	
	
	
		
			
			Summary:
Depends on D20137. Ref T13250. Ref T12101. In versions of PHP beyond 7, various engine errors are gradually changing from internal fatals or internal errors to `Throwables`, a superclass of `Exception`.
This is generally a good change, but code written against PHP 5.x before `Throwable` was introduced may not catch these errors, even when the code is intended to be a top-level exception handler.
(The double-catch pattern here and elsewhere is because `Throwable` does not exist in older PHP, so `catch (Throwable $ex)` catches nothing. The `Exception $ex` clause catches everything in old PHP, the `Throwable $ex` clause catches everything in newer PHP.)
Generalize some `Exception` into `Throwable`.
Test Plan:
  - Added a bogus function call to the rendering stack.
  - Before change: got a blank page.
  - After change: nice exception page.
{F6205012}
Reviewers: amckinley
Reviewed By: amckinley
Maniphest Tasks: T13250, T12101
Differential Revision: https://secure.phabricator.com/D20138
		
	
		
			
				
	
	
		
			829 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			829 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| /**
 | |
|  * Handle request startup, before loading the environment or libraries. This
 | |
|  * class bootstraps the request state up to the point where we can enter
 | |
|  * Phabricator code.
 | |
|  *
 | |
|  * NOTE: This class MUST NOT have any dependencies. It runs before libraries
 | |
|  * load.
 | |
|  *
 | |
|  * Rate Limiting
 | |
|  * =============
 | |
|  *
 | |
|  * Phabricator limits the rate at which clients can request pages, and issues
 | |
|  * HTTP 429 "Too Many Requests" responses if clients request too many pages too
 | |
|  * quickly. Although this is not a complete defense against high-volume attacks,
 | |
|  * it can  protect an install against aggressive crawlers, security scanners,
 | |
|  * and some types of malicious activity.
 | |
|  *
 | |
|  * To perform rate limiting, each page increments a score counter for the
 | |
|  * requesting user's IP. The page can give the IP more points for an expensive
 | |
|  * request, or fewer for an authetnicated request.
 | |
|  *
 | |
|  * Score counters are kept in buckets, and writes move to a new bucket every
 | |
|  * minute. After a few minutes (defined by @{method:getRateLimitBucketCount}),
 | |
|  * the oldest bucket is discarded. This provides a simple mechanism for keeping
 | |
|  * track of scores without needing to store, access, or read very much data.
 | |
|  *
 | |
|  * Users are allowed to accumulate up to 1000 points per minute, averaged across
 | |
|  * all of the tracked buckets.
 | |
|  *
 | |
|  * @task info         Accessing Request Information
 | |
|  * @task hook         Startup Hooks
 | |
|  * @task apocalypse   In Case Of Apocalypse
 | |
|  * @task validation   Validation
 | |
|  * @task ratelimit    Rate Limiting
 | |
|  * @task phases       Startup Phase Timers
 | |
|  */
 | |
| final class PhabricatorStartup {
 | |
| 
 | |
|   private static $startTime;
 | |
|   private static $debugTimeLimit;
 | |
|   private static $accessLog;
 | |
|   private static $capturingOutput;
 | |
|   private static $rawInput;
 | |
|   private static $oldMemoryLimit;
 | |
|   private static $phases;
 | |
| 
 | |
|   private static $limits = array();
 | |
| 
 | |
| 
 | |
| /* -(  Accessing Request Information  )-------------------------------------- */
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @task info
 | |
|    */
 | |
|   public static function getStartTime() {
 | |
|     return self::$startTime;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @task info
 | |
|    */
 | |
|   public static function getMicrosecondsSinceStart() {
 | |
|     // This is the same as "phutil_microseconds_since()", but we may not have
 | |
|     // loaded libphutil yet.
 | |
|     return (int)(1000000 * (microtime(true) - self::getStartTime()));
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @task info
 | |
|    */
 | |
|   public static function setAccessLog($access_log) {
 | |
|     self::$accessLog = $access_log;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @task info
 | |
|    */
 | |
|   public static function getRawInput() {
 | |
|     if (self::$rawInput === null) {
 | |
|       $stream = new AphrontRequestStream();
 | |
| 
 | |
|       if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) {
 | |
|         $encoding = trim($_SERVER['HTTP_CONTENT_ENCODING']);
 | |
|         $stream->setEncoding($encoding);
 | |
|       }
 | |
| 
 | |
|       $input = '';
 | |
|       do {
 | |
|         $bytes = $stream->readData();
 | |
|         if ($bytes === null) {
 | |
|           break;
 | |
|         }
 | |
|         $input .= $bytes;
 | |
|       } while (true);
 | |
| 
 | |
|       self::$rawInput = $input;
 | |
|     }
 | |
| 
 | |
|     return self::$rawInput;
 | |
|   }
 | |
| 
 | |
| 
 | |
| /* -(  Startup Hooks  )------------------------------------------------------ */
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @param float Request start time, from `microtime(true)`.
 | |
|    * @task hook
 | |
|    */
 | |
|   public static function didStartup($start_time) {
 | |
|     self::$startTime = $start_time;
 | |
| 
 | |
|     self::$phases = array();
 | |
| 
 | |
|     self::$accessLog = null;
 | |
| 
 | |
|     static $registered;
 | |
|     if (!$registered) {
 | |
|       // NOTE: This protects us against multiple calls to didStartup() in the
 | |
|       // same request, but also against repeated requests to the same
 | |
|       // interpreter state, which we may implement in the future.
 | |
|       register_shutdown_function(array(__CLASS__, 'didShutdown'));
 | |
|       $registered = true;
 | |
|     }
 | |
| 
 | |
|     self::setupPHP();
 | |
|     self::verifyPHP();
 | |
| 
 | |
|     // If we've made it this far, the environment isn't completely broken so
 | |
|     // we can switch over to relying on our own exception recovery mechanisms.
 | |
|     ini_set('display_errors', 0);
 | |
| 
 | |
|     self::connectRateLimits();
 | |
| 
 | |
|     self::normalizeInput();
 | |
| 
 | |
|     self::verifyRewriteRules();
 | |
| 
 | |
|     self::detectPostMaxSizeTriggered();
 | |
| 
 | |
|     self::beginOutputCapture();
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @task hook
 | |
|    */
 | |
|   public static function didShutdown() {
 | |
|     // Disconnect any active rate limits before we shut down. If we don't do
 | |
|     // this, requests which exit early will lock a slot in any active
 | |
|     // connection limits, and won't count for rate limits.
 | |
|     self::disconnectRateLimits(array());
 | |
| 
 | |
|     $event = error_get_last();
 | |
| 
 | |
|     if (!$event) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     switch ($event['type']) {
 | |
|       case E_ERROR:
 | |
|       case E_PARSE:
 | |
|       case E_COMPILE_ERROR:
 | |
|         break;
 | |
|       default:
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     $msg = ">>> UNRECOVERABLE FATAL ERROR <<<\n\n";
 | |
|     if ($event) {
 | |
|       // Even though we should be emitting this as text-plain, escape things
 | |
|       // just to be sure since we can't really be sure what the program state
 | |
|       // is when we get here.
 | |
|       $msg .= htmlspecialchars(
 | |
|         $event['message']."\n\n".$event['file'].':'.$event['line'],
 | |
|         ENT_QUOTES,
 | |
|         'UTF-8');
 | |
|     }
 | |
| 
 | |
|     // flip dem tables
 | |
|     $msg .= "\n\n\n";
 | |
|     $msg .= "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb\x20\xef\xb8\xb5\x20\xc2\xaf".
 | |
|             "\x5c\x5f\x28\xe3\x83\x84\x29\x5f\x2f\xc2\xaf\x20\xef\xb8\xb5\x20".
 | |
|             "\xe2\x94\xbb\xe2\x94\x81\xe2\x94\xbb";
 | |
| 
 | |
|     self::didFatal($msg);
 | |
|   }
 | |
| 
 | |
|   public static function loadCoreLibraries() {
 | |
|     $phabricator_root = dirname(dirname(dirname(__FILE__)));
 | |
|     $libraries_root = dirname($phabricator_root);
 | |
| 
 | |
|     $root = null;
 | |
|     if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
 | |
|       $root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
 | |
|     }
 | |
| 
 | |
|     ini_set(
 | |
|       'include_path',
 | |
|       $libraries_root.PATH_SEPARATOR.ini_get('include_path'));
 | |
| 
 | |
|     @include_once $root.'libphutil/src/__phutil_library_init__.php';
 | |
|     if (!@constant('__LIBPHUTIL__')) {
 | |
|       self::didFatal(
 | |
|         "Unable to load libphutil. Put libphutil/ next to phabricator/, or ".
 | |
|         "update your PHP 'include_path' to include the parent directory of ".
 | |
|         "libphutil/.");
 | |
|     }
 | |
| 
 | |
|     phutil_load_library('arcanist/src');
 | |
| 
 | |
|     // Load Phabricator itself using the absolute path, so we never end up doing
 | |
|     // anything surprising (loading index.php and libraries from different
 | |
|     // directories).
 | |
|     phutil_load_library($phabricator_root.'/src');
 | |
|   }
 | |
| 
 | |
| /* -(  Output Capture  )----------------------------------------------------- */
 | |
| 
 | |
| 
 | |
|   public static function beginOutputCapture() {
 | |
|     if (self::$capturingOutput) {
 | |
|       self::didFatal('Already capturing output!');
 | |
|     }
 | |
|     self::$capturingOutput = true;
 | |
|     ob_start();
 | |
|   }
 | |
| 
 | |
| 
 | |
|   public static function endOutputCapture() {
 | |
|     if (!self::$capturingOutput) {
 | |
|       return null;
 | |
|     }
 | |
|     self::$capturingOutput = false;
 | |
|     return ob_get_clean();
 | |
|   }
 | |
| 
 | |
| 
 | |
| /* -(  Debug Time Limit  )--------------------------------------------------- */
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Set a time limit (in seconds) for the current script. After time expires,
 | |
|    * the script fatals.
 | |
|    *
 | |
|    * This works like `max_execution_time`, but prints out a useful stack trace
 | |
|    * when the time limit expires. This is primarily intended to make it easier
 | |
|    * to debug pages which hang by allowing extraction of a stack trace: set a
 | |
|    * short debug limit, then use the trace to figure out what's happening.
 | |
|    *
 | |
|    * The limit is implemented with a tick function, so enabling it implies
 | |
|    * some accounting overhead.
 | |
|    *
 | |
|    * @param int Time limit in seconds.
 | |
|    * @return void
 | |
|    */
 | |
|   public static function setDebugTimeLimit($limit) {
 | |
|     self::$debugTimeLimit = $limit;
 | |
| 
 | |
|     static $initialized;
 | |
|     if (!$initialized) {
 | |
|       declare(ticks=1);
 | |
|       register_tick_function(array(__CLASS__, 'onDebugTick'));
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Callback tick function used by @{method:setDebugTimeLimit}.
 | |
|    *
 | |
|    * Fatals with a useful stack trace after the time limit expires.
 | |
|    *
 | |
|    * @return void
 | |
|    */
 | |
|   public static function onDebugTick() {
 | |
|     $limit = self::$debugTimeLimit;
 | |
|     if (!$limit) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     $elapsed = (microtime(true) - self::getStartTime());
 | |
|     if ($elapsed > $limit) {
 | |
|       $frames = array();
 | |
|       foreach (debug_backtrace() as $frame) {
 | |
|         $file = isset($frame['file']) ? $frame['file'] : '-';
 | |
|         $file = basename($file);
 | |
| 
 | |
|         $line = isset($frame['line']) ? $frame['line'] : '-';
 | |
|         $class = isset($frame['class']) ? $frame['class'].'->' : null;
 | |
|         $func = isset($frame['function']) ? $frame['function'].'()' : '?';
 | |
| 
 | |
|         $frames[] = "{$file}:{$line} {$class}{$func}";
 | |
|       }
 | |
| 
 | |
|       self::didFatal(
 | |
|         "Request aborted by debug time limit after {$limit} seconds.\n\n".
 | |
|         "STACK TRACE\n".
 | |
|         implode("\n", $frames));
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
| /* -(  In Case of Apocalypse  )---------------------------------------------- */
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Fatal the request completely in response to an exception, sending a plain
 | |
|    * text message to the client. Calls @{method:didFatal} internally.
 | |
|    *
 | |
|    * @param   string    Brief description of the exception context, like
 | |
|    *                    `"Rendering Exception"`.
 | |
|    * @param   Throwable The exception itself.
 | |
|    * @param   bool      True if it's okay to show the exception's stack trace
 | |
|    *                    to the user. The trace will always be logged.
 | |
|    * @return  exit      This method **does not return**.
 | |
|    *
 | |
|    * @task apocalypse
 | |
|    */
 | |
|   public static function didEncounterFatalException(
 | |
|     $note,
 | |
|     $ex,
 | |
|     $show_trace) {
 | |
| 
 | |
|     $message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage();
 | |
| 
 | |
|     $full_message = $message;
 | |
|     $full_message .= "\n\n";
 | |
|     $full_message .= $ex->getTraceAsString();
 | |
| 
 | |
|     if ($show_trace) {
 | |
|       $message = $full_message;
 | |
|     }
 | |
| 
 | |
|     self::didFatal($message, $full_message);
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Fatal the request completely, sending a plain text message to the client.
 | |
|    *
 | |
|    * @param   string  Plain text message to send to the client.
 | |
|    * @param   string  Plain text message to send to the error log. If not
 | |
|    *                  provided, the client message is used. You can pass a more
 | |
|    *                  detailed message here (e.g., with stack traces) to avoid
 | |
|    *                  showing it to users.
 | |
|    * @return  exit    This method **does not return**.
 | |
|    *
 | |
|    * @task apocalypse
 | |
|    */
 | |
|   public static function didFatal($message, $log_message = null) {
 | |
|     if ($log_message === null) {
 | |
|       $log_message = $message;
 | |
|     }
 | |
| 
 | |
|     self::endOutputCapture();
 | |
|     $access_log = self::$accessLog;
 | |
|     if ($access_log) {
 | |
|       // We may end up here before the access log is initialized, e.g. from
 | |
|       // verifyPHP().
 | |
|       $access_log->setData(
 | |
|         array(
 | |
|           'c' => 500,
 | |
|         ));
 | |
|       $access_log->write();
 | |
|     }
 | |
| 
 | |
|     header(
 | |
|       'Content-Type: text/plain; charset=utf-8',
 | |
|       $replace = true,
 | |
|       $http_error = 500);
 | |
| 
 | |
|     error_log($log_message);
 | |
|     echo $message."\n";
 | |
| 
 | |
|     exit(1);
 | |
|   }
 | |
| 
 | |
| 
 | |
| /* -(  Validation  )--------------------------------------------------------- */
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @task validation
 | |
|    */
 | |
|   private static function setupPHP() {
 | |
|     error_reporting(E_ALL | E_STRICT);
 | |
|     self::$oldMemoryLimit = ini_get('memory_limit');
 | |
|     ini_set('memory_limit', -1);
 | |
| 
 | |
|     // If we have libxml, disable the incredibly dangerous entity loader.
 | |
|     if (function_exists('libxml_disable_entity_loader')) {
 | |
|       libxml_disable_entity_loader(true);
 | |
|     }
 | |
| 
 | |
|     // See T13060. If the locale for this process (the parent process) is not
 | |
|     // a UTF-8 locale we can encounter problems when launching subprocesses
 | |
|     // which receive UTF-8 parameters in their command line argument list.
 | |
|     @setlocale(LC_ALL, 'en_US.UTF-8');
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @task validation
 | |
|    */
 | |
|   public static function getOldMemoryLimit() {
 | |
|     return self::$oldMemoryLimit;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @task validation
 | |
|    */
 | |
|   private static function normalizeInput() {
 | |
|     // Replace superglobals with unfiltered versions, disrespect php.ini (we
 | |
|     // filter ourselves).
 | |
| 
 | |
|     // NOTE: We don't filter INPUT_SERVER because we don't want to overwrite
 | |
|     // changes made in "preamble.php".
 | |
| 
 | |
|     // NOTE: WE don't filter INPUT_POST because we may be constructing it
 | |
|     // lazily if "enable_post_data_reading" is disabled.
 | |
| 
 | |
|     $filter = array(
 | |
|       INPUT_GET,
 | |
|       INPUT_ENV,
 | |
|       INPUT_COOKIE,
 | |
|     );
 | |
|     foreach ($filter as $type) {
 | |
|       $filtered = filter_input_array($type, FILTER_UNSAFE_RAW);
 | |
|       if (!is_array($filtered)) {
 | |
|         continue;
 | |
|       }
 | |
|       switch ($type) {
 | |
|         case INPUT_GET:
 | |
|           $_GET = array_merge($_GET, $filtered);
 | |
|           break;
 | |
|         case INPUT_COOKIE:
 | |
|           $_COOKIE = array_merge($_COOKIE, $filtered);
 | |
|           break;
 | |
|         case INPUT_ENV;
 | |
|           $env = array_merge($_ENV, $filtered);
 | |
|           $_ENV = self::filterEnvSuperglobal($env);
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     self::rebuildRequest();
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @task validation
 | |
|    */
 | |
|   public static function rebuildRequest() {
 | |
|     // Rebuild $_REQUEST, respecting order declared in ".ini" files.
 | |
|     $order = ini_get('request_order');
 | |
| 
 | |
|     if (!$order) {
 | |
|       $order = ini_get('variables_order');
 | |
|     }
 | |
| 
 | |
|     if (!$order) {
 | |
|       // $_REQUEST will be empty, so leave it alone.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     $_REQUEST = array();
 | |
|     for ($ii = 0; $ii < strlen($order); $ii++) {
 | |
|       switch ($order[$ii]) {
 | |
|         case 'G':
 | |
|           $_REQUEST = array_merge($_REQUEST, $_GET);
 | |
|           break;
 | |
|         case 'P':
 | |
|           $_REQUEST = array_merge($_REQUEST, $_POST);
 | |
|           break;
 | |
|         case 'C':
 | |
|           $_REQUEST = array_merge($_REQUEST, $_COOKIE);
 | |
|           break;
 | |
|         default:
 | |
|           // $_ENV and $_SERVER never go into $_REQUEST.
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Adjust `$_ENV` before execution.
 | |
|    *
 | |
|    * Adjustments here primarily impact the environment as seen by subprocesses.
 | |
|    * The environment is forwarded explicitly by @{class:ExecFuture}.
 | |
|    *
 | |
|    * @param map<string, wild> Input `$_ENV`.
 | |
|    * @return map<string, string> Suitable `$_ENV`.
 | |
|    * @task validation
 | |
|    */
 | |
|   private static function filterEnvSuperglobal(array $env) {
 | |
| 
 | |
|     // In some configurations, we may get "argc" and "argv" set in $_ENV.
 | |
|     // These are not real environmental variables, and "argv" may have an array
 | |
|     // value which can not be forwarded to subprocesses. Remove these from the
 | |
|     // environment if they are present.
 | |
|     unset($env['argc']);
 | |
|     unset($env['argv']);
 | |
| 
 | |
|     return $env;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @task validation
 | |
|    */
 | |
|   private static function verifyPHP() {
 | |
|     $required_version = '5.2.3';
 | |
|     if (version_compare(PHP_VERSION, $required_version) < 0) {
 | |
|       self::didFatal(
 | |
|         "You are running PHP version '".PHP_VERSION."', which is older than ".
 | |
|         "the minimum version, '{$required_version}'. Update to at least ".
 | |
|         "'{$required_version}'.");
 | |
|     }
 | |
| 
 | |
|     if (get_magic_quotes_gpc()) {
 | |
|       self::didFatal(
 | |
|         "Your server is configured with PHP 'magic_quotes_gpc' enabled. This ".
 | |
|         "feature is 'highly discouraged' by PHP's developers and you must ".
 | |
|         "disable it to run Phabricator. Consult the PHP manual for ".
 | |
|         "instructions.");
 | |
|     }
 | |
| 
 | |
|     if (extension_loaded('apc')) {
 | |
|       $apc_version = phpversion('apc');
 | |
|       $known_bad = array(
 | |
|         '3.1.14' => true,
 | |
|         '3.1.15' => true,
 | |
|         '3.1.15-dev' => true,
 | |
|       );
 | |
|       if (isset($known_bad[$apc_version])) {
 | |
|         self::didFatal(
 | |
|           "You have APC {$apc_version} installed. This version of APC is ".
 | |
|           "known to be bad, and does not work with Phabricator (it will ".
 | |
|           "cause Phabricator to fatal unrecoverably with nonsense errors). ".
 | |
|           "Downgrade to version 3.1.13.");
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (isset($_SERVER['HTTP_PROXY'])) {
 | |
|       self::didFatal(
 | |
|         'This HTTP request included a "Proxy:" header, poisoning the '.
 | |
|         'environment (CVE-2016-5385 / httpoxy). Declining to process this '.
 | |
|         'request. For details, see: https://phurl.io/u/httpoxy');
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * @task validation
 | |
|    */
 | |
|   private static function verifyRewriteRules() {
 | |
|     if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (php_sapi_name() == 'cli-server') {
 | |
|       // Compatibility with PHP 5.4+ built-in web server.
 | |
|       $url = parse_url($_SERVER['REQUEST_URI']);
 | |
|       $_REQUEST['__path__'] = $url['path'];
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (!isset($_REQUEST['__path__'])) {
 | |
|       self::didFatal(
 | |
|         "Request parameter '__path__' is not set. Your rewrite rules ".
 | |
|         "are not configured correctly.");
 | |
|     }
 | |
| 
 | |
|     if (!strlen($_REQUEST['__path__'])) {
 | |
|       self::didFatal(
 | |
|         "Request parameter '__path__' is set, but empty. Your rewrite rules ".
 | |
|         "are not configured correctly. The '__path__' should always ".
 | |
|         "begin with a '/'.");
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Detect if this request has had its POST data stripped by exceeding the
 | |
|    * 'post_max_size' PHP configuration limit.
 | |
|    *
 | |
|    * PHP has a setting called 'post_max_size'. If a POST request arrives with
 | |
|    * a body larger than the limit, PHP doesn't generate $_POST but processes
 | |
|    * the request anyway, and provides no formal way to detect that this
 | |
|    * happened.
 | |
|    *
 | |
|    * We can still read the entire body out of `php://input`. However according
 | |
|    * to the documentation the stream isn't available for "multipart/form-data"
 | |
|    * (on nginx + php-fpm it appears that it is available, though, at least) so
 | |
|    * any attempt to generate $_POST would be fragile.
 | |
|    *
 | |
|    * @task validation
 | |
|    */
 | |
|   private static function detectPostMaxSizeTriggered() {
 | |
|     // If this wasn't a POST, we're fine.
 | |
|     if ($_SERVER['REQUEST_METHOD'] != 'POST') {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // If "enable_post_data_reading" is off, we won't have $_POST and this
 | |
|     // condition is effectively impossible.
 | |
|     if (!ini_get('enable_post_data_reading')) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // If there's POST data, clearly we're in good shape.
 | |
|     if ($_POST) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // For HTML5 drag-and-drop file uploads, Safari submits the data as
 | |
|     // "application/x-www-form-urlencoded". For most files this generates
 | |
|     // something in POST because most files decode to some nonempty (albeit
 | |
|     // meaningless) value. However, some files (particularly small images)
 | |
|     // don't decode to anything. If we know this is a drag-and-drop upload,
 | |
|     // we can skip this check.
 | |
|     if (isset($_REQUEST['__upload__'])) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // PHP generates $_POST only for two content types. This routing happens
 | |
|     // in `main/php_content_types.c` in PHP. Normally, all forms use one of
 | |
|     // these content types, but some requests may not -- for example, Firefox
 | |
|     // submits files sent over HTML5 XMLHTTPRequest APIs with the Content-Type
 | |
|     // of the file itself. If we don't have a recognized content type, we
 | |
|     // don't need $_POST.
 | |
|     //
 | |
|     // NOTE: We use strncmp() because the actual content type may be something
 | |
|     // like "multipart/form-data; boundary=...".
 | |
|     //
 | |
|     // NOTE: Chrome sometimes omits this header, see some discussion in T1762
 | |
|     // and http://code.google.com/p/chromium/issues/detail?id=6800
 | |
|     $content_type = isset($_SERVER['CONTENT_TYPE'])
 | |
|       ? $_SERVER['CONTENT_TYPE']
 | |
|       : '';
 | |
| 
 | |
|     $parsed_types = array(
 | |
|       'application/x-www-form-urlencoded',
 | |
|       'multipart/form-data',
 | |
|     );
 | |
| 
 | |
|     $is_parsed_type = false;
 | |
|     foreach ($parsed_types as $parsed_type) {
 | |
|       if (strncmp($content_type, $parsed_type, strlen($parsed_type)) === 0) {
 | |
|         $is_parsed_type = true;
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!$is_parsed_type) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Check for 'Content-Length'. If there's no data, we don't expect $_POST
 | |
|     // to exist.
 | |
|     $length = (int)$_SERVER['CONTENT_LENGTH'];
 | |
|     if (!$length) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Time to fatal: we know this was a POST with data that should have been
 | |
|     // populated into $_POST, but it wasn't.
 | |
| 
 | |
|     $config = ini_get('post_max_size');
 | |
|     self::didFatal(
 | |
|       "As received by the server, this request had a nonzero content length ".
 | |
|       "but no POST data.\n\n".
 | |
|       "Normally, this indicates that it exceeds the 'post_max_size' setting ".
 | |
|       "in the PHP configuration on the server. Increase the 'post_max_size' ".
 | |
|       "setting or reduce the size of the request.\n\n".
 | |
|       "Request size according to 'Content-Length' was '{$length}', ".
 | |
|       "'post_max_size' is set to '{$config}'.");
 | |
|   }
 | |
| 
 | |
| 
 | |
| /* -(  Rate Limiting  )------------------------------------------------------ */
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Add a new client limits.
 | |
|    *
 | |
|    * @param PhabricatorClientLimit New limit.
 | |
|    * @return PhabricatorClientLimit The limit.
 | |
|    */
 | |
|   public static function addRateLimit(PhabricatorClientLimit $limit) {
 | |
|     self::$limits[] = $limit;
 | |
|     return $limit;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Apply configured rate limits.
 | |
|    *
 | |
|    * If any limit is exceeded, this method terminates the request.
 | |
|    *
 | |
|    * @return void
 | |
|    * @task ratelimit
 | |
|    */
 | |
|   private static function connectRateLimits() {
 | |
|     $limits = self::$limits;
 | |
| 
 | |
|     $reason = null;
 | |
|     $connected = array();
 | |
|     foreach ($limits as $limit) {
 | |
|       $reason = $limit->didConnect();
 | |
|       $connected[] = $limit;
 | |
|       if ($reason !== null) {
 | |
|         break;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // If we're killing the request here, disconnect any limits that we
 | |
|     // connected to try to keep the accounting straight.
 | |
|     if ($reason !== null) {
 | |
|       foreach ($connected as $limit) {
 | |
|         $limit->didDisconnect(array());
 | |
|       }
 | |
| 
 | |
|       self::didRateLimit($reason);
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Tear down rate limiting and allow limits to score the request.
 | |
|    *
 | |
|    * @param map<string, wild> Additional, freeform request state.
 | |
|    * @return void
 | |
|    * @task ratelimit
 | |
|    */
 | |
|   public static function disconnectRateLimits(array $request_state) {
 | |
|     $limits = self::$limits;
 | |
| 
 | |
|     // Remove all limits before disconnecting them so this works properly if
 | |
|     // it runs twice. (We run this automatically as a shutdown handler.)
 | |
|     self::$limits = array();
 | |
| 
 | |
|     foreach ($limits as $limit) {
 | |
|       $limit->didDisconnect($request_state);
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Emit an HTTP 429 "Too Many Requests" response (indicating that the user
 | |
|    * has exceeded application rate limits) and exit.
 | |
|    *
 | |
|    * @return exit This method **does not return**.
 | |
|    * @task ratelimit
 | |
|    */
 | |
|   private static function didRateLimit($reason) {
 | |
|     header(
 | |
|       'Content-Type: text/plain; charset=utf-8',
 | |
|       $replace = true,
 | |
|       $http_error = 429);
 | |
| 
 | |
|     echo $reason;
 | |
| 
 | |
|     exit(1);
 | |
|   }
 | |
| 
 | |
| 
 | |
| /* -(  Startup Timers  )----------------------------------------------------- */
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Record the beginning of a new startup phase.
 | |
|    *
 | |
|    * For phases which occur before @{class:PhabricatorStartup} loads, save the
 | |
|    * time and record it with @{method:recordStartupPhase} after the class is
 | |
|    * available.
 | |
|    *
 | |
|    * @param string Phase name.
 | |
|    * @task phases
 | |
|    */
 | |
|   public static function beginStartupPhase($phase) {
 | |
|     self::recordStartupPhase($phase, microtime(true));
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Record the start time of a previously executed startup phase.
 | |
|    *
 | |
|    * For startup phases which occur after @{class:PhabricatorStartup} loads,
 | |
|    * use @{method:beginStartupPhase} instead. This method can be used to
 | |
|    * record a time before the class loads, then hand it over once the class
 | |
|    * becomes available.
 | |
|    *
 | |
|    * @param string Phase name.
 | |
|    * @param float Phase start time, from `microtime(true)`.
 | |
|    * @task phases
 | |
|    */
 | |
|   public static function recordStartupPhase($phase, $time) {
 | |
|     self::$phases[$phase] = $time;
 | |
|   }
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Get information about startup phase timings.
 | |
|    *
 | |
|    * Sometimes, performance problems can occur before we start the profiler.
 | |
|    * Since the profiler can't examine these phases, it isn't useful in
 | |
|    * understanding their performance costs.
 | |
|    *
 | |
|    * Instead, the startup process marks when it enters various phases using
 | |
|    * @{method:beginStartupPhase}. A later call to this method can retrieve this
 | |
|    * information, which can be examined to gain greater insight into where
 | |
|    * time was spent. The output is still crude, but better than nothing.
 | |
|    *
 | |
|    * @task phases
 | |
|    */
 | |
|   public static function getPhases() {
 | |
|     return self::$phases;
 | |
|   }
 | |
| 
 | |
| }
 |