diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -45,6 +45,7 @@ 'rsrc/css/application/config/config-template.css' => '25d446d6', 'rsrc/css/application/config/config-welcome.css' => 'b0d16200', 'rsrc/css/application/config/setup-issue.css' => '8f852bc0', + 'rsrc/css/application/config/unhandled-exception.css' => '38f08073', 'rsrc/css/application/conpherence/menu.css' => 'e1e0fdf1', 'rsrc/css/application/conpherence/message-pane.css' => '042886d1', 'rsrc/css/application/conpherence/notification.css' => '04a6e10a', @@ -816,6 +817,7 @@ 'sprite-tokens-css' => '1706b943', 'syntax-highlighting-css' => '56c1ba38', 'tokens-css' => '3d0f239e', + 'unhandled-exception-css' => '38f08073', ), 'requires' => array( '00861799' => array( diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -161,12 +161,14 @@ 'AphrontResponse' => 'aphront/response/AphrontResponse.php', 'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php', 'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php', + 'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php', 'AphrontTableView' => 'view/control/AphrontTableView.php', 'AphrontTagView' => 'view/AphrontTagView.php', 'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php', 'AphrontTwoColumnView' => 'view/layout/AphrontTwoColumnView.php', 'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php', 'AphrontURIMapper' => 'aphront/AphrontURIMapper.php', + 'AphrontUnhandledExceptionResponse' => 'aphront/response/AphrontUnhandledExceptionResponse.php', 'AphrontUsageException' => 'aphront/exception/AphrontUsageException.php', 'AphrontView' => 'view/AphrontView.php', 'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php', @@ -3217,11 +3219,13 @@ 'AphrontRequestTestCase' => 'PhabricatorTestCase', 'AphrontSideNavFilterView' => 'AphrontView', 'AphrontStackTraceView' => 'AphrontView', + 'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse', 'AphrontTableView' => 'AphrontView', 'AphrontTagView' => 'AphrontView', 'AphrontTokenizerTemplateView' => 'AphrontView', 'AphrontTwoColumnView' => 'AphrontView', 'AphrontTypeaheadTemplateView' => 'AphrontView', + 'AphrontUnhandledExceptionResponse' => 'AphrontStandaloneHTMLResponse', 'AphrontUsageException' => 'AphrontException', 'AphrontView' => array( 'Phobject', @@ -4669,7 +4673,7 @@ 'PhabricatorMarkupInterface', ), 'PhabricatorConfigProxySource' => 'PhabricatorConfigSource', - 'PhabricatorConfigResponse' => 'AphrontHTMLResponse', + 'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse', 'PhabricatorConfigSchemaQuery' => 'Phobject', 'PhabricatorConfigSchemaSpec' => 'Phobject', 'PhabricatorConfigServerSchema' => 'PhabricatorConfigStorageSchema', diff --git a/src/__tests__/PhabricatorCelerityTestCase.php b/src/__tests__/PhabricatorCelerityTestCase.php --- a/src/__tests__/PhabricatorCelerityTestCase.php +++ b/src/__tests__/PhabricatorCelerityTestCase.php @@ -15,18 +15,20 @@ $new_map = id(new CelerityResourceMapGenerator($resources)) ->generate(); - $this->assertEqual( - $new_map->getNameMap(), - $old_map->getNameMap()); - $this->assertEqual( - $new_map->getSymbolMap(), - $old_map->getSymbolMap()); - $this->assertEqual( - $new_map->getRequiresMap(), - $old_map->getRequiresMap()); - $this->assertEqual( - $new_map->getPackageMap(), - $old_map->getPackageMap()); + // Don't actually compare these values with assertEqual(), since the diff + // isn't helpful and is often enormously huge. + + $maps_are_identical = + ($new_map->getNameMap() === $old_map->getNameMap()) && + ($new_map->getSymbolMap() === $old_map->getSymbolMap()) && + ($new_map->getRequiresMap() === $old_map->getRequiresMap()) && + ($new_map->getPackageMap() === $old_map->getPackageMap()); + + $this->assertTrue( + $maps_are_identical, + pht( + 'When this test fails, it means the Celerity resource map is out '. + 'of date. Run `bin/celerity map` to rebuild it.')); } } diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -54,6 +54,192 @@ public function willBuildRequest() {} + /** + * @phutil-external-symbol class PhabricatorStartup + */ + public static function runHTTPRequest(AphrontHTTPSink $sink) { + PhabricatorEnv::initializeWebEnvironment(); + + $debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit'); + if ($debug_time_limit) { + PhabricatorStartup::setDebugTimeLimit($debug_time_limit); + } + + // This is the earliest we can get away with this, we need env config first. + PhabricatorAccessLog::init(); + $access_log = PhabricatorAccessLog::getLog(); + PhabricatorStartup::setGlobal('log.access', $access_log); + $access_log->setData( + array( + 'R' => AphrontRequest::getHTTPHeader('Referer', '-'), + 'r' => idx($_SERVER, 'REMOTE_ADDR', '-'), + 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'), + )); + + DarkConsoleXHProfPluginAPI::hookProfiler(); + DarkConsoleErrorLogPluginAPI::registerErrorHandler(); + + $response = PhabricatorSetupCheck::willProcessRequest(); + if ($response) { + PhabricatorStartup::endOutputCapture(); + $sink->writeResponse($response); + return; + } + + $host = AphrontRequest::getHTTPHeader('Host'); + $path = $_REQUEST['__path__']; + + switch ($host) { + default: + $config_key = 'aphront.default-application-configuration-class'; + $application = PhabricatorEnv::newObjectFromConfig($config_key); + break; + } + + $application->setHost($host); + $application->setPath($path); + $application->willBuildRequest(); + $request = $application->buildRequest(); + + // Build the server URI implied by the request headers. If an administrator + // has not configured "phabricator.base-uri" yet, we'll use this to generate + // links. + + $request_protocol = ($request->isHTTPS() ? 'https' : 'http'); + $request_base_uri = "{$request_protocol}://{$host}/"; + PhabricatorEnv::setRequestBaseURI($request_base_uri); + + $access_log->setData( + array( + 'U' => (string)$request->getRequestURI()->getPath(), + )); + + $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF')); + + $processing_exception = null; + try { + $response = $application->processRequest($request, $access_log, $sink); + $response_code = $response->getHTTPResponseCode(); + } catch (Exception $ex) { + $processing_exception = $ex; + $response_code = 500; + } + + $write_guard->dispose(); + + $access_log->setData( + array( + 'c' => $response_code, + 'T' => PhabricatorStartup::getMicrosecondsSinceStart(), + )); + + $access_log->write(); + + DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log); + + // Add points to the rate limits for this request. + if (isset($_SERVER['REMOTE_ADDR'])) { + $user_ip = $_SERVER['REMOTE_ADDR']; + + // The base score for a request allows users to make 30 requests per + // minute. + $score = (1000 / 30); + + // If the user was logged in, let them make more requests. + if ($request->getUser() && $request->getUser()->getPHID()) { + $score = $score / 5; + } + + PhabricatorStartup::addRateLimitScore($user_ip, $score); + } + + if ($processing_exception) { + throw $processing_exception; + } + } + + + public function processRequest( + AphrontRequest $request, + PhutilDeferredLog $access_log, + AphrontHTTPSink $sink) { + + $this->setRequest($request); + + list($controller, $uri_data) = $this->buildController(); + + $access_log->setData( + array( + 'C' => get_class($controller), + )); + + $request->setURIMap($uri_data); + $controller->setRequest($request); + + // If execution throws an exception and then trying to render that + // exception throws another exception, we want to show the original + // exception, as it is likely the root cause of the rendering exception. + $original_exception = null; + try { + $response = $controller->willBeginExecution(); + + if ($request->getUser() && $request->getUser()->getPHID()) { + $access_log->setData( + array( + 'u' => $request->getUser()->getUserName(), + 'P' => $request->getUser()->getPHID(), + )); + } + + if (!$response) { + $controller->willProcessRequest($uri_data); + $response = $controller->handleRequest($request); + } + } catch (Exception $ex) { + $original_exception = $ex; + $response = $this->handleException($ex); + } + + try { + $response = $controller->didProcessRequest($response); + $response = $this->willSendResponse($response, $controller); + $response->setRequest($request); + + $unexpected_output = PhabricatorStartup::endOutputCapture(); + if ($unexpected_output) { + $unexpected_output = pht( + "Unexpected output:\n\n%s", + $unexpected_output); + + phlog($unexpected_output); + + if ($response instanceof AphrontWebpageResponse) { + echo phutil_tag( + 'div', + array('style' => + 'background: #eeddff;'. + 'white-space: pre-wrap;'. + 'z-index: 200000;'. + 'position: relative;'. + 'padding: 8px;'. + 'font-family: monospace', + ), + $unexpected_output); + } + } + + $sink->writeResponse($response); + } catch (Exception $ex) { + if ($original_exception) { + throw $original_exception; + } + throw $ex; + } + + return $response; + } + + /* -( URI Routing )-------------------------------------------------------- */ diff --git a/src/aphront/response/AphrontStandaloneHTMLResponse.php b/src/aphront/response/AphrontStandaloneHTMLResponse.php new file mode 100644 --- /dev/null +++ b/src/aphront/response/AphrontStandaloneHTMLResponse.php @@ -0,0 +1,63 @@ +<?php + +abstract class AphrontStandaloneHTMLResponse + extends AphrontHTMLResponse { + + abstract protected function getResources(); + abstract protected function getResponseTitle(); + abstract protected function getResponseBodyClass(); + abstract protected function getResponseBody(); + abstract protected function buildPlainTextResponseString(); + + final public function buildResponseString() { + // Check to make sure we aren't requesting this via Ajax or Conduit. + if (isset($_REQUEST['__ajax__']) || isset($_REQUEST['__conduit__'])) { + return (string)hsprintf('%s', $this->buildPlainTextResponseString()); + } + + $title = $this->getResponseTitle(); + $resources = $this->buildResources(); + $body_class = $this->getResponseBodyClass(); + $body = $this->getResponseBody(); + + return (string)hsprintf( +<<<EOTEMPLATE +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8" /> + <title>%s</title> + %s + </head> + %s +</html> +EOTEMPLATE + , + $title, + $resources, + phutil_tag( + 'body', + array( + 'class' => $body_class, + ), + $body)); + } + + private function buildResources() { + $paths = $this->getResources(); + + $webroot = dirname(phutil_get_library_root('phabricator')).'/webroot/'; + + $resources = array(); + foreach ($paths as $path) { + $resources[] = phutil_tag( + 'style', + array('type' => 'text/css'), + phutil_safe_html(Filesystem::readFile($webroot.'/rsrc/'.$path))); + } + + return phutil_implode_html("\n", $resources); + } + + +} diff --git a/src/aphront/response/AphrontUnhandledExceptionResponse.php b/src/aphront/response/AphrontUnhandledExceptionResponse.php new file mode 100644 --- /dev/null +++ b/src/aphront/response/AphrontUnhandledExceptionResponse.php @@ -0,0 +1,74 @@ +<?php + +final class AphrontUnhandledExceptionResponse + extends AphrontStandaloneHTMLResponse { + + private $exception; + + public function setException(Exception $exception) { + $this->exception = $exception; + return $this; + } + + public function getHTTPResponseCode() { + return 500; + } + + protected function getResources() { + return array( + 'css/application/config/config-template.css', + 'css/application/config/unhandled-exception.css', + ); + } + + protected function getResponseTitle() { + return pht('Unhandled Exception'); + } + + protected function getResponseBodyClass() { + return 'unhandled-exception'; + } + + protected function getResponseBody() { + $ex = $this->exception; + + if ($ex instanceof AphrontUsageException) { + $title = $ex->getTitle(); + } else { + $title = get_class($ex); + } + + $body = $ex->getMessage(); + $body = phutil_escape_html_newlines($body); + + return phutil_tag( + 'div', + array( + 'class' => 'unhandled-exception-detail', + ), + array( + phutil_tag( + 'h1', + array( + 'class' => 'unhandled-exception-title', + ), + $title), + phutil_tag( + 'div', + array( + 'class' => 'unhandled-exception-body', + ), + $body), + )); + } + + protected function buildPlainTextResponseString() { + $ex = $this->exception; + + return pht( + '%s: %s', + get_class($ex), + $ex->getMessage()); + } + +} diff --git a/src/applications/config/response/PhabricatorConfigResponse.php b/src/applications/config/response/PhabricatorConfigResponse.php --- a/src/applications/config/response/PhabricatorConfigResponse.php +++ b/src/applications/config/response/PhabricatorConfigResponse.php @@ -1,6 +1,6 @@ <?php -final class PhabricatorConfigResponse extends AphrontHTMLResponse { +final class PhabricatorConfigResponse extends AphrontStandaloneHTMLResponse { private $view; @@ -9,51 +9,33 @@ return $this; } - public function buildResponseString() { - // Check to make sure we aren't requesting this via ajax or conduit - if (isset($_REQUEST['__ajax__']) || isset($_REQUEST['__conduit__'])) { - // We don't want to flood the console with html, just return a simple - // message for now. - return pht( - 'This install has a fatal setup error, access the internet web '. - 'version to view details and resolve it.'); - } - - $resources = $this->buildResources(); - - $view = $this->view->render(); - - return hsprintf( - '<!DOCTYPE html>'. - '<html>'. - '<head>'. - '<meta charset="UTF-8" />'. - '<title>Phabricator Setup</title>'. - '%s'. - '</head>'. - '<body class="setup-fatal">%s</body>'. - '</html>', - $resources, - $view); + public function getHTTPResponseCode() { + return 500; } - private function buildResources() { - $css = array( - 'application/config/config-template.css', - 'application/config/setup-issue.css', + protected function getResources() { + return array( + 'css/application/config/config-template.css', + 'css/application/config/setup-issue.css', ); + } - $webroot = dirname(phutil_get_library_root('phabricator')).'/webroot/'; + protected function getResponseTitle() { + return pht('Phabricator Setup Error'); + } - $resources = array(); - foreach ($css as $path) { - $resources[] = phutil_tag( - 'style', - array('type' => 'text/css'), - phutil_safe_html(Filesystem::readFile($webroot.'/rsrc/css/'.$path))); - } - return phutil_implode_html("\n", $resources); + protected function getResponseBodyClass() { + return 'setup-fatal'; } + protected function getResponseBody() { + return $this->view->render(); + } + + protected function buildPlainTextResponseString() { + return pht( + 'This install has a fatal setup error, access the internet web '. + 'version to view details and resolve it.'); + } } diff --git a/support/PhabricatorStartup.php b/support/PhabricatorStartup.php --- a/support/PhabricatorStartup.php +++ b/support/PhabricatorStartup.php @@ -122,6 +122,10 @@ 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); + if (isset($_SERVER['REMOTE_ADDR'])) { self::rateLimitRequest($_SERVER['REMOTE_ADDR']); } diff --git a/webroot/index.php b/webroot/index.php --- a/webroot/index.php +++ b/webroot/index.php @@ -11,174 +11,27 @@ PhabricatorStartup::didStartup(); -$show_unexpected_traces = false; try { PhabricatorStartup::loadCoreLibraries(); - - PhabricatorEnv::initializeWebEnvironment(); - - $debug_time_limit = PhabricatorEnv::getEnvConfig('debug.time-limit'); - if ($debug_time_limit) { - PhabricatorStartup::setDebugTimeLimit($debug_time_limit); - } - - $show_unexpected_traces = PhabricatorEnv::getEnvConfig( - 'phabricator.developer-mode'); - - // This is the earliest we can get away with this, we need env config first. - PhabricatorAccessLog::init(); - $access_log = PhabricatorAccessLog::getLog(); - PhabricatorStartup::setGlobal('log.access', $access_log); - $access_log->setData( - array( - 'R' => AphrontRequest::getHTTPHeader('Referer', '-'), - 'r' => idx($_SERVER, 'REMOTE_ADDR', '-'), - 'M' => idx($_SERVER, 'REQUEST_METHOD', '-'), - )); - - DarkConsoleXHProfPluginAPI::hookProfiler(); - DarkConsoleErrorLogPluginAPI::registerErrorHandler(); - $sink = new AphrontPHPHTTPSink(); - $response = PhabricatorSetupCheck::willProcessRequest(); - if ($response) { - PhabricatorStartup::endOutputCapture(); - $sink->writeResponse($response); - return; - } - - $host = AphrontRequest::getHTTPHeader('Host'); - $path = $_REQUEST['__path__']; - - switch ($host) { - default: - $config_key = 'aphront.default-application-configuration-class'; - $application = PhabricatorEnv::newObjectFromConfig($config_key); - break; - } - - $application->setHost($host); - $application->setPath($path); - $application->willBuildRequest(); - $request = $application->buildRequest(); - - // Until an administrator sets "phabricator.base-uri", assume it is the same - // as the request URI. This will work fine in most cases, it just breaks down - // when daemons need to do things. - $request_protocol = ($request->isHTTPS() ? 'https' : 'http'); - $request_base_uri = "{$request_protocol}://{$host}/"; - PhabricatorEnv::setRequestBaseURI($request_base_uri); - - $write_guard = new AphrontWriteGuard(array($request, 'validateCSRF')); - - $application->setRequest($request); - list($controller, $uri_data) = $application->buildController(); - $request->setURIMap($uri_data); - $controller->setRequest($request); - - $access_log->setData( - array( - 'U' => (string)$request->getRequestURI()->getPath(), - 'C' => get_class($controller), - )); - - // If execution throws an exception and then trying to render that exception - // throws another exception, we want to show the original exception, as it is - // likely the root cause of the rendering exception. - $original_exception = null; - try { - $response = $controller->willBeginExecution(); - - if ($request->getUser() && $request->getUser()->getPHID()) { - $access_log->setData( - array( - 'u' => $request->getUser()->getUserName(), - 'P' => $request->getUser()->getPHID(), - )); - } - - if (!$response) { - $controller->willProcessRequest($uri_data); - $response = $controller->handleRequest($request); - } - } catch (Exception $ex) { - $original_exception = $ex; - $response = $application->handleException($ex); - } - try { - $response = $controller->didProcessRequest($response); - $response = $application->willSendResponse($response, $controller); - $response->setRequest($request); - - $unexpected_output = PhabricatorStartup::endOutputCapture(); - if ($unexpected_output) { - $unexpected_output = "Unexpected output:\n\n{$unexpected_output}"; - phlog($unexpected_output); - - if ($response instanceof AphrontWebpageResponse) { - echo phutil_tag( - 'div', - array('style' => - 'background: #eeddff;'. - 'white-space: pre-wrap;'. - 'z-index: 200000;'. - 'position: relative;'. - 'padding: 8px;'. - 'font-family: monospace', - ), - $unexpected_output); - } - } - - $sink->writeResponse($response); + AphrontApplicationConfiguration::runHTTPRequest($sink); } catch (Exception $ex) { - $write_guard->dispose(); - $access_log->write(); - if ($original_exception) { - $ex = new PhutilAggregateException( - 'Multiple exceptions during processing and rendering.', - array( - $original_exception, - $ex, - )); + try { + $response = new AphrontUnhandledExceptionResponse(); + $response->setException($ex); + + PhabricatorStartup::endOutputCapture(); + $sink->writeResponse($response); + } catch (Exception $response_exception) { + // If we hit a rendering exception, ignore it and throw the original + // exception. It is generally more interesting and more likely to be + // the root cause. + throw $ex; } - PhabricatorStartup::didEncounterFatalException( - 'Rendering Exception', - $ex, - $show_unexpected_traces); - } - - $write_guard->dispose(); - - $access_log->setData( - array( - 'c' => $response->getHTTPResponseCode(), - 'T' => PhabricatorStartup::getMicrosecondsSinceStart(), - )); - - DarkConsoleXHProfPluginAPI::saveProfilerSample($access_log); - - // Add points to the rate limits for this request. - if (isset($_SERVER['REMOTE_ADDR'])) { - $user_ip = $_SERVER['REMOTE_ADDR']; - - // The base score for a request allows users to make 30 requests per - // minute. - $score = (1000 / 30); - - // If the user was logged in, let them make more requests. - if ($request->getUser() && $request->getUser()->getPHID()) { - $score = $score / 5; - } - - PhabricatorStartup::addRateLimitScore($user_ip, $score); } } catch (Exception $ex) { - PhabricatorStartup::didEncounterFatalException( - 'Core Exception', - $ex, - $show_unexpected_traces); + PhabricatorStartup::didEncounterFatalException('Core Exception', $ex, false); } diff --git a/webroot/rsrc/css/application/config/unhandled-exception.css b/webroot/rsrc/css/application/config/unhandled-exception.css new file mode 100644 --- /dev/null +++ b/webroot/rsrc/css/application/config/unhandled-exception.css @@ -0,0 +1,27 @@ +/** + * @provides unhandled-exception-css + */ + +.unhandled-exception { + background: #222228; +} + +.unhandled-exception-detail { + max-width: 760px; + margin: 16px auto; + background: #f7f7f7; + border: 2px solid #ffffff; +} + +.unhandled-exception-detail .unhandled-exception-title { + font-size: 15px; + font-weight: bold; + margin: 0; + padding: 16px; + background: #DFE0E2; +} + +.unhandled-exception-detail .unhandled-exception-body { + padding: 16px; + color: #4B4D51; +}