| 
									
										
										
										
											2012-12-25 06:11:39 -08:00
										 |  |  | <?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. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * @task info         Accessing Request Information | 
					
						
							|  |  |  |  * @task hook         Startup Hooks | 
					
						
							|  |  |  |  * @task apocalypse   In Case Of Apocalypse | 
					
						
							|  |  |  |  * @task validation   Validation | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | final class PhabricatorStartup { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   private static $startTime; | 
					
						
							|  |  |  |   private static $globals = array(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* -(  Accessing Request Information  )-------------------------------------- */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task info | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public static function getStartTime() { | 
					
						
							|  |  |  |     return self::$startTime; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task info | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public static function setGlobal($key, $value) { | 
					
						
							|  |  |  |     self::validateGlobal($key); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     self::$globals[$key] = $value; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task info | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public static function getGlobal($key, $default = null) { | 
					
						
							|  |  |  |     self::validateGlobal($key); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!array_key_exists($key, self::$globals)) { | 
					
						
							|  |  |  |       return $default; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     return self::$globals[$key]; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* -(  Startup Hooks  )------------------------------------------------------ */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task hook | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public static function didStartup() { | 
					
						
							|  |  |  |     self::$startTime = microtime(true); | 
					
						
							|  |  |  |     self::$globals = array(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     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; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2012-12-25 06:15:28 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     self::setupPHP(); | 
					
						
							|  |  |  |     self::verifyPHP(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     self::verifyRewriteRules(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     self::detectPostMaxSizeTriggered(); | 
					
						
							| 
									
										
										
										
											2012-12-25 06:11:39 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task hook | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public static function didShutdown() { | 
					
						
							|  |  |  |     $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); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-12-25 06:15:28 -08:00
										 |  |  |   public static function loadCoreLibraries() { | 
					
						
							|  |  |  |     $phabricator_root = 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'); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-12-25 06:11:39 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  | /* -(  In Case of Apocalypse  )---------------------------------------------- */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task apocalypse | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   public static function didFatal($message) { | 
					
						
							|  |  |  |     $access_log = self::getGlobal('log.access'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if ($access_log) { | 
					
						
							|  |  |  |       try { | 
					
						
							|  |  |  |         $access_log->setData( | 
					
						
							|  |  |  |           array( | 
					
						
							|  |  |  |             'c' => 500, | 
					
						
							|  |  |  |           )); | 
					
						
							|  |  |  |         $access_log->write(); | 
					
						
							|  |  |  |       } catch (Exception $ex) { | 
					
						
							|  |  |  |         $message .= "\n(Moreover, unable to write to access log.)"; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     header( | 
					
						
							|  |  |  |       'Content-Type: text/plain; charset=utf-8', | 
					
						
							|  |  |  |       $replace = true, | 
					
						
							|  |  |  |       $http_error = 500); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     error_log($message); | 
					
						
							|  |  |  |     echo $message; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     exit(1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /* -(  Validation  )--------------------------------------------------------- */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task valiation | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private static function setupPHP() { | 
					
						
							|  |  |  |     error_reporting(E_ALL | E_STRICT); | 
					
						
							|  |  |  |     ini_set('memory_limit', -1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task valiation | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   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."); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task valiation | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private static function verifyRewriteRules() { | 
					
						
							| 
									
										
										
										
											2013-01-22 17:17:37 -08:00
										 |  |  |     if (isset($_REQUEST['__path__']) && strlen($_REQUEST['__path__'])) { | 
					
						
							| 
									
										
										
										
											2012-12-25 06:11:39 -08:00
										 |  |  |       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']; | 
					
						
							| 
									
										
										
										
											2013-01-22 17:17:37 -08:00
										 |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (!isset($_REQUEST['__path__'])) { | 
					
						
							| 
									
										
										
										
											2012-12-25 06:11:39 -08:00
										 |  |  |       self::didFatal( | 
					
						
							|  |  |  |         "Request parameter '__path__' is not set. Your rewrite rules ". | 
					
						
							|  |  |  |         "are not configured correctly."); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2013-01-22 17:17:37 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     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 '/'."); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2012-12-25 06:11:39 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * @task valiation | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private static function validateGlobal($key) { | 
					
						
							|  |  |  |     static $globals = array( | 
					
						
							|  |  |  |       'log.access' => true, | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (empty($globals[$key])) { | 
					
						
							|  |  |  |       throw new Exception("Access to unknown startup global '{$key}'!"); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-12-25 06:15:28 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * 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 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'); | 
					
						
							|  |  |  |     PhabricatorStartup::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}'."); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2012-12-25 06:11:39 -08:00
										 |  |  | } |