Page MenuHomePhabricator

D14049.id33963.diff
No OneTemporary

D14049.id33963.diff

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
@@ -157,6 +157,7 @@
'AphrontRedirectResponseTestCase' => 'aphront/response/__tests__/AphrontRedirectResponseTestCase.php',
'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php',
'AphrontRequest' => 'aphront/AphrontRequest.php',
+ 'AphrontRequestExceptionHandler' => 'aphront/handler/AphrontRequestExceptionHandler.php',
'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php',
'AphrontResponse' => 'aphront/response/AphrontResponse.php',
'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
@@ -1484,6 +1485,7 @@
'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php',
'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php',
'PhabricatorAdministratorsPolicyRule' => 'applications/policy/rule/PhabricatorAdministratorsPolicyRule.php',
+ 'PhabricatorAjaxRequestExceptionHandler' => 'aphront/handler/PhabricatorAjaxRequestExceptionHandler.php',
'PhabricatorAlmanacApplication' => 'applications/almanac/application/PhabricatorAlmanacApplication.php',
'PhabricatorAmazonAuthProvider' => 'applications/auth/provider/PhabricatorAmazonAuthProvider.php',
'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php',
@@ -1786,6 +1788,7 @@
'PhabricatorConduitLogQuery' => 'applications/conduit/query/PhabricatorConduitLogQuery.php',
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
+ 'PhabricatorConduitRequestExceptionHandler' => 'aphront/handler/PhabricatorConduitRequestExceptionHandler.php',
'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php',
'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php',
'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php',
@@ -1838,6 +1841,7 @@
'PhabricatorConfigOptionType' => 'applications/config/custom/PhabricatorConfigOptionType.php',
'PhabricatorConfigPHIDModule' => 'applications/config/module/PhabricatorConfigPHIDModule.php',
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
+ 'PhabricatorConfigRequestExceptionHandlerModule' => 'applications/config/module/PhabricatorConfigRequestExceptionHandlerModule.php',
'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
'PhabricatorConfigSchemaQuery' => 'applications/config/schema/PhabricatorConfigSchemaQuery.php',
'PhabricatorConfigSchemaSpec' => 'applications/config/schema/PhabricatorConfigSchemaSpec.php',
@@ -1993,6 +1997,7 @@
'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
'PhabricatorDateTimeSettingsPanel' => 'applications/settings/panel/PhabricatorDateTimeSettingsPanel.php',
'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php',
+ 'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php',
'PhabricatorDesktopNotificationsSettingsPanel' => 'applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php',
'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php',
'PhabricatorDestructionEngine' => 'applications/system/engine/PhabricatorDestructionEngine.php',
@@ -2183,6 +2188,7 @@
'PhabricatorHelpEditorProtocolController' => 'applications/help/controller/PhabricatorHelpEditorProtocolController.php',
'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php',
'PhabricatorHeraldApplication' => 'applications/herald/application/PhabricatorHeraldApplication.php',
+ 'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php',
'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php',
'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php',
'PhabricatorHomeMainController' => 'applications/home/controller/PhabricatorHomeMainController.php',
@@ -2580,6 +2586,7 @@
'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php',
'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php',
'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php',
+ 'PhabricatorPolicyRequestExceptionHandler' => 'aphront/handler/PhabricatorPolicyRequestExceptionHandler.php',
'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php',
'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php',
'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php',
@@ -2667,6 +2674,7 @@
'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php',
'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php',
'PhabricatorQueryOrderVector' => 'infrastructure/query/order/PhabricatorQueryOrderVector.php',
+ 'PhabricatorRateLimitRequestExceptionHandler' => 'aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php',
'PhabricatorRecaptchaConfigOptions' => 'applications/config/option/PhabricatorRecaptchaConfigOptions.php',
'PhabricatorRecipientHasBadgeEdgeType' => 'applications/badges/edge/PhabricatorRecipientHasBadgeEdgeType.php',
'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php',
@@ -2758,6 +2766,7 @@
'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php',
'PhabricatorRepositoryVersion' => 'applications/repository/constants/PhabricatorRepositoryVersion.php',
+ 'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php',
'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php',
'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php',
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
@@ -3788,6 +3797,7 @@
'AphrontRedirectResponseTestCase' => 'PhabricatorTestCase',
'AphrontReloadResponse' => 'AphrontRedirectResponse',
'AphrontRequest' => 'Phobject',
+ 'AphrontRequestExceptionHandler' => 'Phobject',
'AphrontRequestTestCase' => 'PhabricatorTestCase',
'AphrontResponse' => 'Phobject',
'AphrontRoutingMap' => 'Phobject',
@@ -5303,6 +5313,7 @@
'PhabricatorActionView' => 'AphrontView',
'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorAdministratorsPolicyRule' => 'PhabricatorPolicyRule',
+ 'PhabricatorAjaxRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorAlmanacApplication' => 'PhabricatorApplication',
'PhabricatorAmazonAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorAnchorView' => 'AphrontView',
@@ -5667,6 +5678,7 @@
'PhabricatorPolicyInterface',
),
'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorConduitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorConduitTestCase' => 'PhabricatorTestCase',
'PhabricatorConduitToken' => array(
@@ -5729,6 +5741,7 @@
'PhabricatorConfigOptionType' => 'Phobject',
'PhabricatorConfigPHIDModule' => 'PhabricatorConfigModule',
'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
+ 'PhabricatorConfigRequestExceptionHandlerModule' => 'PhabricatorConfigModule',
'PhabricatorConfigResponse' => 'AphrontStandaloneHTMLResponse',
'PhabricatorConfigSchemaQuery' => 'Phobject',
'PhabricatorConfigSchemaSpec' => 'Phobject',
@@ -5913,6 +5926,7 @@
'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorDateTimeSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorDebugController' => 'PhabricatorController',
+ 'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorDesktopNotificationsSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorDestructionEngine' => 'Phobject',
'PhabricatorDeveloperConfigOptions' => 'PhabricatorApplicationConfigOptions',
@@ -6138,6 +6152,7 @@
'PhabricatorHelpEditorProtocolController' => 'PhabricatorHelpController',
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
'PhabricatorHeraldApplication' => 'PhabricatorApplication',
+ 'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorHomeApplication' => 'PhabricatorApplication',
'PhabricatorHomeController' => 'PhabricatorController',
'PhabricatorHomeMainController' => 'PhabricatorHomeController',
@@ -6587,6 +6602,7 @@
'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType',
'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+ 'PhabricatorPolicyRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorPolicyRule' => 'Phobject',
'PhabricatorPolicyTestCase' => 'PhabricatorTestCase',
'PhabricatorPolicyTestObject' => array(
@@ -6702,6 +6718,7 @@
'Phobject',
'Iterator',
),
+ 'PhabricatorRateLimitRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler',
'PhabricatorRecaptchaConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorRecipientHasBadgeEdgeType' => 'PhabricatorEdgeType',
'PhabricatorRedirectController' => 'PhabricatorController',
@@ -6828,6 +6845,7 @@
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO',
'PhabricatorRepositoryVersion' => 'Phobject',
+ 'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler',
'PhabricatorResourceSite' => 'PhabricatorSite',
'PhabricatorRobotsController' => 'PhabricatorController',
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
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
@@ -3,6 +3,7 @@
/**
* @task routing URI Routing
* @task response Response Handling
+ * @task exception Exception Handling
*/
abstract class AphrontApplicationConfiguration extends Phobject {
@@ -11,7 +12,6 @@
private $path;
private $console;
- abstract public function getApplicationName();
abstract public function buildRequest();
abstract public function build404Controller();
abstract public function buildRedirectController($uri, $external);
@@ -482,7 +482,7 @@
/**
- * Verifies that the erturn value from an
+ * Verifies that the return value from an
* @{class:AphrontResponseProducerInterface} is of an allowed type.
*
* @param AphrontResponseProducerInterface Object which produced
@@ -512,6 +512,36 @@
/**
+ * Verifies that the return value from an
+ * @{class:AphrontRequestExceptionHandler} is of an allowed type.
+ *
+ * @param AphrontRequestExceptionHandler Object which produced this
+ * response.
+ * @param wild Supposedly valid response.
+ * @return void
+ * @task response
+ */
+ private function validateErrorHandlerResponse(
+ AphrontRequestExceptionHandler $handler,
+ $response) {
+
+ if ($this->isValidResponseObject($response)) {
+ return;
+ }
+
+ throw new Exception(
+ pht(
+ 'Exception handler "%s" returned an invalid response from call to '.
+ '"%s". This method must return an object of class "%s", or an object '.
+ 'which implements the "%s" interface.',
+ get_class($handler),
+ 'handleRequestException()',
+ 'AphrontResponse',
+ 'AphrontResponseProducerInterface'));
+ }
+
+
+ /**
* Resolves a response object into an @{class:AphrontResponse}.
*
* Controllers are permitted to return actual responses of class
@@ -572,4 +602,34 @@
}
+/* -( Error Handling )----------------------------------------------------- */
+
+
+ /**
+ * Convert an exception which has escaped the controller into a response.
+ *
+ * This method delegates exception handling to available subclasses of
+ * @{class:AphrontRequestExceptionHandler}.
+ *
+ * @param Exception Exception which needs to be handled.
+ * @return wild Response or response producer, or null if no available
+ * handler can produce a response.
+ * @task exception
+ */
+ private function handleException(Exception $ex) {
+ $handlers = AphrontRequestExceptionHandler::getAllHandlers();
+
+ $request = $this->getRequest();
+ foreach ($handlers as $handler) {
+ if ($handler->canHandleRequestException($request, $ex)) {
+ $response = $handler->handleRequestException($request, $ex);
+ $this->validateErrorHandlerResponse($handler, $response);
+ return $response;
+ }
+ }
+
+ throw $ex;
+ }
+
+
}
diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
--- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
@@ -8,12 +8,6 @@
class AphrontDefaultApplicationConfiguration
extends AphrontApplicationConfiguration {
- public function __construct() {}
-
- public function getApplicationName() {
- return 'aphront-default';
- }
-
/**
* @phutil-external-symbol class PhabricatorStartup
*/
@@ -50,213 +44,6 @@
return $request;
}
- public function handleException(Exception $ex) {
- $request = $this->getRequest();
-
- // For Conduit requests, return a Conduit response.
- if ($request->isConduit()) {
- $response = new ConduitAPIResponse();
- $response->setErrorCode(get_class($ex));
- $response->setErrorInfo($ex->getMessage());
-
- return id(new AphrontJSONResponse())
- ->setAddJSONShield(false)
- ->setContent($response->toDictionary());
- }
-
- // For non-workflow requests, return a Ajax response.
- if ($request->isAjax() && !$request->isWorkflow()) {
- // Log these; they don't get shown on the client and can be difficult
- // to debug.
- phlog($ex);
-
- $response = new AphrontAjaxResponse();
- $response->setError(
- array(
- 'code' => get_class($ex),
- 'info' => $ex->getMessage(),
- ));
- return $response;
- }
-
- $user = $request->getUser();
- if (!$user) {
- // If we hit an exception very early, we won't have a user.
- $user = new PhabricatorUser();
- }
-
- if ($ex instanceof PhabricatorSystemActionRateLimitException) {
- $dialog = id(new AphrontDialogView())
- ->setTitle(pht('Slow Down!'))
- ->setUser($user)
- ->setErrors(array(pht('You are being rate limited.')))
- ->appendParagraph($ex->getMessage())
- ->appendParagraph($ex->getRateExplanation())
- ->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...'));
-
- $response = new AphrontDialogResponse();
- $response->setDialog($dialog);
- return $response;
- }
-
- if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) {
-
- $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
- $ex->getFactors(),
- $ex->getFactorValidationResults(),
- $user,
- $request);
-
- $dialog = id(new AphrontDialogView())
- ->setUser($user)
- ->setTitle(pht('Entering High Security'))
- ->setShortTitle(pht('Security Checkpoint'))
- ->setWidth(AphrontDialogView::WIDTH_FORM)
- ->addHiddenInput(AphrontRequest::TYPE_HISEC, true)
- ->setErrors(
- array(
- pht(
- 'You are taking an action which requires you to enter '.
- 'high security.'),
- ))
- ->appendParagraph(
- pht(
- 'High security mode helps protect your account from security '.
- 'threats, like session theft or someone messing with your stuff '.
- 'while you\'re grabbing a coffee. To enter high security mode, '.
- 'confirm your credentials.'))
- ->appendChild($form->buildLayoutView())
- ->appendParagraph(
- pht(
- 'Your account will remain in high security mode for a short '.
- 'period of time. When you are finished taking sensitive '.
- 'actions, you should leave high security.'))
- ->setSubmitURI($request->getPath())
- ->addCancelButton($ex->getCancelURI())
- ->addSubmitButton(pht('Enter High Security'));
-
- $request_parameters = $request->getPassthroughRequestParameters(
- $respect_quicksand = true);
- foreach ($request_parameters as $key => $value) {
- $dialog->addHiddenInput($key, $value);
- }
-
- $response = new AphrontDialogResponse();
- $response->setDialog($dialog);
- return $response;
- }
-
- if ($ex instanceof PhabricatorPolicyException) {
- if (!$user->isLoggedIn()) {
- // If the user isn't logged in, just give them a login form. This is
- // probably a generally more useful response than a policy dialog that
- // they have to click through to get a login form.
- //
- // Possibly we should add a header here like "you need to login to see
- // the thing you are trying to look at".
- $login_controller = new PhabricatorAuthStartController();
- $login_controller->setRequest($request);
-
- $auth_app_class = 'PhabricatorAuthApplication';
- $auth_app = PhabricatorApplication::getByClass($auth_app_class);
- $login_controller->setCurrentApplication($auth_app);
-
- return $login_controller->handleRequest($request);
- }
-
- $content = array(
- phutil_tag(
- 'div',
- array(
- 'class' => 'aphront-policy-rejection',
- ),
- $ex->getRejection()),
- );
-
- $list = null;
- if ($ex->getCapabilityName()) {
- $list = $ex->getMoreInfo();
- foreach ($list as $key => $item) {
- $list[$key] = $item;
- }
-
- $content[] = phutil_tag(
- 'div',
- array(
- 'class' => 'aphront-capability-details',
- ),
- pht('Users with the "%s" capability:', $ex->getCapabilityName()));
-
- }
-
- $dialog = id(new AphrontDialogView())
- ->setTitle($ex->getTitle())
- ->setClass('aphront-access-dialog')
- ->setUser($user)
- ->appendChild($content);
-
- if ($list) {
- $dialog->appendList($list);
- }
-
- if ($this->getRequest()->isAjax()) {
- $dialog->addCancelButton('/', pht('Close'));
- } else {
- $dialog->addCancelButton('/', pht('OK'));
- }
-
- $response = new AphrontDialogResponse();
- $response->setDialog($dialog);
- return $response;
- }
-
- // Always log the unhandled exception.
- phlog($ex);
-
- $class = get_class($ex);
- $message = $ex->getMessage();
-
- if ($ex instanceof AphrontSchemaQueryException) {
- $message .= "\n\n".pht(
- "NOTE: This usually indicates that the MySQL schema has not been ".
- "properly upgraded. Run '%s' to ensure your schema is up to date.",
- 'bin/storage upgrade');
- }
-
- if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
- $trace = id(new AphrontStackTraceView())
- ->setUser($user)
- ->setTrace($ex->getTrace());
- } else {
- $trace = null;
- }
-
- $content = phutil_tag(
- 'div',
- array('class' => 'aphront-unhandled-exception'),
- array(
- phutil_tag('div', array('class' => 'exception-message'), $message),
- $trace,
- ));
-
- $dialog = new AphrontDialogView();
- $dialog
- ->setTitle(pht('Unhandled Exception ("%s")', $class))
- ->setClass('aphront-exception-dialog')
- ->setUser($user)
- ->appendChild($content);
-
- if ($this->getRequest()->isAjax()) {
- $dialog->addCancelButton('/', pht('Close'));
- }
-
- $response = new AphrontDialogResponse();
- $response->setDialog($dialog);
- $response->setHTTPResponseCode(500);
-
- return $response;
- }
-
public function build404Controller() {
return array(new Phabricator404Controller(), array());
}
diff --git a/src/aphront/handler/AphrontRequestExceptionHandler.php b/src/aphront/handler/AphrontRequestExceptionHandler.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/handler/AphrontRequestExceptionHandler.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * React to an unhandled exception escaping request handling in a controller
+ * and convert it into a response.
+ *
+ * These handlers are generally used to render error pages, but they may
+ * also perform more specialized handling in situations where an error page
+ * is not appropriate.
+ */
+abstract class AphrontRequestExceptionHandler extends Phobject {
+
+ abstract public function getRequestExceptionHandlerPriority();
+
+ public function shouldLogException(
+ AphrontRequest $request,
+ Exception $ex) {
+ return null;
+ }
+
+ abstract public function canHandleRequestException(
+ AphrontRequest $request,
+ Exception $ex);
+
+ abstract public function handleRequestException(
+ AphrontRequest $request,
+ Exception $ex);
+
+ final public static function getAllHandlers() {
+ return id(new PhutilClassMapQuery())
+ ->setAncestorClass(__CLASS__)
+ ->setSortMethod('getRequestExceptionHandlerPriority')
+ ->execute();
+ }
+
+}
diff --git a/src/aphront/handler/PhabricatorAjaxRequestExceptionHandler.php b/src/aphront/handler/PhabricatorAjaxRequestExceptionHandler.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/handler/PhabricatorAjaxRequestExceptionHandler.php
@@ -0,0 +1,38 @@
+<?php
+
+final class PhabricatorAjaxRequestExceptionHandler
+ extends PhabricatorRequestExceptionHandler {
+
+ public function getRequestExceptionHandlerPriority() {
+ return 110000;
+ }
+
+ public function getRequestExceptionHandlerDescription() {
+ return pht('Responds to requests made by AJAX clients.');
+ }
+
+ public function canHandleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+ // For non-workflow requests, return a Ajax response.
+ return ($request->isAjax() && !$request->isWorkflow());
+ }
+
+ public function handleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ // Log these; they don't get shown on the client and can be difficult
+ // to debug.
+ phlog($ex);
+
+ $response = new AphrontAjaxResponse();
+ $response->setError(
+ array(
+ 'code' => get_class($ex),
+ 'info' => $ex->getMessage(),
+ ));
+ return $response;
+ }
+
+}
diff --git a/src/aphront/handler/PhabricatorConduitRequestExceptionHandler.php b/src/aphront/handler/PhabricatorConduitRequestExceptionHandler.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/handler/PhabricatorConduitRequestExceptionHandler.php
@@ -0,0 +1,33 @@
+<?php
+
+final class PhabricatorConduitRequestExceptionHandler
+ extends PhabricatorRequestExceptionHandler {
+
+ public function getRequestExceptionHandlerPriority() {
+ return 100000;
+ }
+
+ public function getRequestExceptionHandlerDescription() {
+ return pht('Responds to requests made by Conduit clients.');
+ }
+
+ public function canHandleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+ return $request->isConduit();
+ }
+
+ public function handleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ $response = id(new ConduitAPIResponse())
+ ->setErrorCode(get_class($ex))
+ ->setErrorInfo($ex->getMessage());
+
+ return id(new AphrontJSONResponse())
+ ->setAddJSONShield(false)
+ ->setContent($response->toDictionary());
+ }
+
+}
diff --git a/src/aphront/handler/PhabricatorDefaultRequestExceptionHandler.php b/src/aphront/handler/PhabricatorDefaultRequestExceptionHandler.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/handler/PhabricatorDefaultRequestExceptionHandler.php
@@ -0,0 +1,76 @@
+<?php
+
+final class PhabricatorDefaultRequestExceptionHandler
+ extends PhabricatorRequestExceptionHandler {
+
+ public function getRequestExceptionHandlerPriority() {
+ return 900000;
+ }
+
+ public function getRequestExceptionHandlerDescription() {
+ return pht('Handles all other exceptions.');
+ }
+
+ public function canHandleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ if (!$this->isPhabricatorSite($request)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function handleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ $viewer = $this->getViewer($request);
+
+ // Always log the unhandled exception.
+ phlog($ex);
+
+ $class = get_class($ex);
+ $message = $ex->getMessage();
+
+ if ($ex instanceof AphrontSchemaQueryException) {
+ $message .= "\n\n".pht(
+ "NOTE: This usually indicates that the MySQL schema has not been ".
+ "properly upgraded. Run '%s' to ensure your schema is up to date.",
+ 'bin/storage upgrade');
+ }
+
+ if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
+ $trace = id(new AphrontStackTraceView())
+ ->setUser($viewer)
+ ->setTrace($ex->getTrace());
+ } else {
+ $trace = null;
+ }
+
+ $content = phutil_tag(
+ 'div',
+ array('class' => 'aphront-unhandled-exception'),
+ array(
+ phutil_tag('div', array('class' => 'exception-message'), $message),
+ $trace,
+ ));
+
+ $dialog = new AphrontDialogView();
+ $dialog
+ ->setTitle(pht('Unhandled Exception ("%s")', $class))
+ ->setClass('aphront-exception-dialog')
+ ->setUser($viewer)
+ ->appendChild($content);
+
+ if ($request->isAjax()) {
+ $dialog->addCancelButton('/', pht('Close'));
+ }
+
+ return id(new AphrontDialogResponse())
+ ->setDialog($dialog)
+ ->setHTTPResponseCode(500);
+ }
+
+}
diff --git a/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php
@@ -0,0 +1,76 @@
+<?php
+
+final class PhabricatorHighSecurityRequestExceptionHandler
+ extends PhabricatorRequestExceptionHandler {
+
+ public function getRequestExceptionHandlerPriority() {
+ return 310000;
+ }
+
+ public function getRequestExceptionHandlerDescription() {
+ return pht(
+ 'Handles high security exceptions which occur when a user needs '.
+ 'to present MFA credentials to take an action.');
+ }
+
+ public function canHandleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ if (!$this->isPhabricatorSite($request)) {
+ return false;
+ }
+
+ return ($ex instanceof PhabricatorAuthHighSecurityRequiredException);
+ }
+
+ public function handleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ $viewer = $this->getViewer($request);
+
+ $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm(
+ $ex->getFactors(),
+ $ex->getFactorValidationResults(),
+ $viewer,
+ $request);
+
+ $dialog = id(new AphrontDialogView())
+ ->setUser($viewer)
+ ->setTitle(pht('Entering High Security'))
+ ->setShortTitle(pht('Security Checkpoint'))
+ ->setWidth(AphrontDialogView::WIDTH_FORM)
+ ->addHiddenInput(AphrontRequest::TYPE_HISEC, true)
+ ->setErrors(
+ array(
+ pht(
+ 'You are taking an action which requires you to enter '.
+ 'high security.'),
+ ))
+ ->appendParagraph(
+ pht(
+ 'High security mode helps protect your account from security '.
+ 'threats, like session theft or someone messing with your stuff '.
+ 'while you\'re grabbing a coffee. To enter high security mode, '.
+ 'confirm your credentials.'))
+ ->appendChild($form->buildLayoutView())
+ ->appendParagraph(
+ pht(
+ 'Your account will remain in high security mode for a short '.
+ 'period of time. When you are finished taking sensitive '.
+ 'actions, you should leave high security.'))
+ ->setSubmitURI($request->getPath())
+ ->addCancelButton($ex->getCancelURI())
+ ->addSubmitButton(pht('Enter High Security'));
+
+ $request_parameters = $request->getPassthroughRequestParameters(
+ $respect_quicksand = true);
+ foreach ($request_parameters as $key => $value) {
+ $dialog->addHiddenInput($key, $value);
+ }
+
+ return $dialog;
+ }
+
+}
diff --git a/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php b/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/handler/PhabricatorPolicyRequestExceptionHandler.php
@@ -0,0 +1,93 @@
+<?php
+
+final class PhabricatorPolicyRequestExceptionHandler
+ extends PhabricatorRequestExceptionHandler {
+
+ public function getRequestExceptionHandlerPriority() {
+ return 320000;
+ }
+
+ public function getRequestExceptionHandlerDescription() {
+ return pht(
+ 'Handles policy exceptions which occur when a user tries to '.
+ 'do something they do not have permission to do.');
+ }
+
+ public function canHandleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ if (!$this->isPhabricatorSite($request)) {
+ return false;
+ }
+
+ return ($ex instanceof PhabricatorPolicyException);
+ }
+
+ public function handleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ $viewer = $this->getViewer($request);
+
+ if (!$viewer->isLoggedIn()) {
+ // If the user isn't logged in, just give them a login form. This is
+ // probably a generally more useful response than a policy dialog that
+ // they have to click through to get a login form.
+ //
+ // Possibly we should add a header here like "you need to login to see
+ // the thing you are trying to look at".
+ $auth_app_class = 'PhabricatorAuthApplication';
+ $auth_app = PhabricatorApplication::getByClass($auth_app_class);
+
+ return id(new PhabricatorAuthStartController())
+ ->setRequest($request)
+ ->setCurrentApplication($auth_app)
+ ->handleRequest($request);
+ }
+
+ $content = array(
+ phutil_tag(
+ 'div',
+ array(
+ 'class' => 'aphront-policy-rejection',
+ ),
+ $ex->getRejection()),
+ );
+
+ $list = null;
+ if ($ex->getCapabilityName()) {
+ $list = $ex->getMoreInfo();
+ foreach ($list as $key => $item) {
+ $list[$key] = $item;
+ }
+
+ $content[] = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'aphront-capability-details',
+ ),
+ pht('Users with the "%s" capability:', $ex->getCapabilityName()));
+
+ }
+
+ $dialog = id(new AphrontDialogView())
+ ->setTitle($ex->getTitle())
+ ->setClass('aphront-access-dialog')
+ ->setUser($viewer)
+ ->appendChild($content);
+
+ if ($list) {
+ $dialog->appendList($list);
+ }
+
+ if ($request->isAjax()) {
+ $dialog->addCancelButton('/', pht('Close'));
+ } else {
+ $dialog->addCancelButton('/', pht('OK'));
+ }
+
+ return $dialog;
+ }
+
+}
diff --git a/src/aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php b/src/aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/handler/PhabricatorRateLimitRequestExceptionHandler.php
@@ -0,0 +1,42 @@
+<?php
+
+final class PhabricatorRateLimitRequestExceptionHandler
+ extends PhabricatorRequestExceptionHandler {
+
+ public function getRequestExceptionHandlerPriority() {
+ return 300000;
+ }
+
+ public function getRequestExceptionHandlerDescription() {
+ return pht(
+ 'Handles action rate limiting exceptions which occur when a user '.
+ 'does something too frequently.');
+ }
+
+ public function canHandleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ if (!$this->isPhabricatorSite($request)) {
+ return false;
+ }
+
+ return ($ex instanceof PhabricatorSystemActionRateLimitException);
+ }
+
+ public function handleRequestException(
+ AphrontRequest $request,
+ Exception $ex) {
+
+ $viewer = $this->getViewer($request);
+
+ return id(new AphrontDialogView())
+ ->setTitle(pht('Slow Down!'))
+ ->setUser($viewer)
+ ->setErrors(array(pht('You are being rate limited.')))
+ ->appendParagraph($ex->getMessage())
+ ->appendParagraph($ex->getRateExplanation())
+ ->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...'));
+ }
+
+}
diff --git a/src/aphront/handler/PhabricatorRequestExceptionHandler.php b/src/aphront/handler/PhabricatorRequestExceptionHandler.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/handler/PhabricatorRequestExceptionHandler.php
@@ -0,0 +1,26 @@
+<?php
+
+abstract class PhabricatorRequestExceptionHandler
+ extends AphrontRequestExceptionHandler {
+
+ protected function isPhabricatorSite(AphrontRequest $request) {
+ $site = $request->getSite();
+ if (!$site) {
+ return false;
+ }
+
+ return ($site instanceof PhabricatorSite);
+ }
+
+ protected function getViewer(AphrontRequest $request) {
+ $viewer = $request->getUser();
+
+ if ($viewer) {
+ return $viewer;
+ }
+
+ // If we hit an exception very early, we won't have a user yet.
+ return new PhabricatorUser();
+ }
+
+}
diff --git a/src/applications/config/module/PhabricatorConfigEdgeModule.php b/src/applications/config/module/PhabricatorConfigEdgeModule.php
--- a/src/applications/config/module/PhabricatorConfigEdgeModule.php
+++ b/src/applications/config/module/PhabricatorConfigEdgeModule.php
@@ -41,7 +41,7 @@
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Edge Types'))
- ->appendChild($table);
+ ->setTable($table);
}
}
diff --git a/src/applications/config/module/PhabricatorConfigPHIDModule.php b/src/applications/config/module/PhabricatorConfigPHIDModule.php
--- a/src/applications/config/module/PhabricatorConfigPHIDModule.php
+++ b/src/applications/config/module/PhabricatorConfigPHIDModule.php
@@ -41,7 +41,7 @@
return id(new PHUIObjectBoxView())
->setHeaderText(pht('PHID Types'))
- ->appendChild($table);
+ ->setTable($table);
}
}
diff --git a/src/applications/config/module/PhabricatorConfigSiteModule.php b/src/applications/config/module/PhabricatorConfigRequestExceptionHandlerModule.php
copy from src/applications/config/module/PhabricatorConfigSiteModule.php
copy to src/applications/config/module/PhabricatorConfigRequestExceptionHandlerModule.php
--- a/src/applications/config/module/PhabricatorConfigSiteModule.php
+++ b/src/applications/config/module/PhabricatorConfigRequestExceptionHandlerModule.php
@@ -1,26 +1,27 @@
<?php
-final class PhabricatorConfigSiteModule extends PhabricatorConfigModule {
+final class PhabricatorConfigRequestExceptionHandlerModule
+ extends PhabricatorConfigModule {
public function getModuleKey() {
- return 'site';
+ return 'exception-handler';
}
public function getModuleName() {
- return pht('Sites');
+ return pht('Exception Handlers');
}
public function renderModuleStatus(AphrontRequest $request) {
$viewer = $request->getViewer();
- $sites = AphrontSite::getAllSites();
+ $handlers = AphrontRequestExceptionHandler::getAllHandlers();
$rows = array();
- foreach ($sites as $key => $site) {
+ foreach ($handlers as $key => $handler) {
$rows[] = array(
- $site->getPriority(),
+ $handler->getRequestExceptionHandlerPriority(),
$key,
- $site->getDescription(),
+ $handler->getRequestExceptionHandlerDescription(),
);
}
@@ -39,8 +40,8 @@
));
return id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Sites'))
- ->appendChild($table);
+ ->setHeaderText(pht('Exception Handlers'))
+ ->setTable($table);
}
}
diff --git a/src/applications/config/module/PhabricatorConfigSiteModule.php b/src/applications/config/module/PhabricatorConfigSiteModule.php
--- a/src/applications/config/module/PhabricatorConfigSiteModule.php
+++ b/src/applications/config/module/PhabricatorConfigSiteModule.php
@@ -40,7 +40,7 @@
return id(new PHUIObjectBoxView())
->setHeaderText(pht('Sites'))
- ->appendChild($table);
+ ->setTable($table);
}
}

File Metadata

Mime Type
text/plain
Expires
Wed, May 8, 7:06 PM (3 w, 9 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6268351
Default Alt Text
D14049.id33963.diff (37 KB)

Event Timeline