diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index e81f77294f..483aaaf1e6 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -1,272 +1,269 @@ getResourceURIMapRules() + array( '/~/' => array( '' => 'DarkConsoleController', 'data/(?P[^/]+)/' => 'DarkConsoleDataController', ), ); } protected function getResourceURIMapRules() { return array( '/res/' => array( '(?:(?P[0-9]+)T/)?'. '(?P[^/]+)/'. '(?P[a-f0-9]{8})/'. '(?P.+\.(?:css|js|jpg|png|swf|gif|woff))' => 'CelerityPhabricatorResourceController', ), ); } /** * @phutil-external-symbol class PhabricatorStartup */ public function buildRequest() { $parser = new PhutilQueryStringParser(); $data = array(); // If the request has "multipart/form-data" content, we can't use // PhutilQueryStringParser to parse it, and the raw data supposedly is not // available anyway (according to the PHP documentation, "php://input" is // not available for "multipart/form-data" requests). However, it is // available at least some of the time (see T3673), so double check that // we aren't trying to parse data we won't be able to parse correctly by // examining the Content-Type header. $content_type = idx($_SERVER, 'CONTENT_TYPE'); $is_form_data = preg_match('@^multipart/form-data@i', $content_type); $raw_input = PhabricatorStartup::getRawInput(); if (strlen($raw_input) && !$is_form_data) { $data += $parser->parseQueryString($raw_input); } else if ($_POST) { $data += $_POST; } $data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', '')); $cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix'); $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($data); $request->setApplicationConfiguration($this); $request->setCookiePrefix($cookie_prefix); 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->isJavelinWorkflow()) { // 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; } $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $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) { - $error_view = id(new AphrontErrorView()) - ->setErrors(array(pht('You are being rate limited.'))); - $dialog = id(new AphrontDialogView()) ->setTitle(pht('Slow Down!')) ->setUser($user) - ->appendChild($error_view) + ->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 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($request); $auth_app_class = 'PhabricatorApplicationAuth'; $auth_app = PhabricatorApplication::getByClass($auth_app_class); $login_controller->setCurrentApplication($auth_app); return $login_controller->processRequest(); } $list = $ex->getMoreInfo(); foreach ($list as $key => $item) { $list[$key] = phutil_tag('li', array(), $item); } if ($list) { $list = phutil_tag('ul', array(), $list); } $content = array( phutil_tag( 'div', array( 'class' => 'aphront-policy-rejection', ), $ex->getRejection()), phutil_tag( 'div', array( 'class' => 'aphront-capability-details', ), pht('Users with the "%s" capability:', $ex->getCapabilityName())), $list, ); $dialog = new AphrontDialogView(); $dialog ->setTitle($ex->getTitle()) ->setClass('aphront-access-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', 'Close'); } else { $dialog->addCancelButton('/', $is_serious ? 'OK' : 'Away With Thee'); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof AphrontUsageException) { $error = new AphrontErrorView(); $error->setTitle($ex->getTitle()); $error->appendChild($ex->getMessage()); $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->appendChild($error); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); $response->setHTTPResponseCode(500); return $response; } // Always log the unhandled exception. phlog($ex); $class = get_class($ex); $message = $ex->getMessage(); if ($ex instanceof AphrontQuerySchemaException) { $message .= "\n\n". "NOTE: This usually indicates that the MySQL schema has not been ". "properly upgraded. Run 'bin/storage upgrade' to ensure your ". "schema is up to date."; } 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('Unhandled Exception ("'.$class.'")') ->setClass('aphront-exception-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', 'Close'); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); $response->setHTTPResponseCode(500); return $response; } public function willSendResponse(AphrontResponse $response) { return $response; } public function build404Controller() { return array(new Phabricator404Controller($this->getRequest()), array()); } public function buildRedirectController($uri) { return array( new PhabricatorRedirectController($this->getRequest()), array( 'uri' => $uri, )); } } diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index c4f1f55916..2b631c313e 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -1,348 +1,346 @@ conpherenceID = $conpherence_id; return $this; } public function getConpherenceID() { return $this->conpherenceID; } public function willProcessRequest(array $data) { $this->setConpherenceID(idx($data, 'id')); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $conpherence_id = $this->getConpherenceID(); if (!$conpherence_id) { return new Aphront404Response(); } $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->withIDs(array($conpherence_id)) ->needFilePHIDs(true) ->executeOne(); $action = $request->getStr('action', ConpherenceUpdateActions::METADATA); $latest_transaction_id = null; $response_mode = 'ajax'; $error_view = null; $e_file = array(); $errors = array(); $delete_draft = false; $xactions = array(); if ($request->isFormPost()) { $editor = id(new ConpherenceEditor()) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContentSourceFromRequest($request) ->setActor($user); switch ($action) { case ConpherenceUpdateActions::DRAFT: $draft = PhabricatorDraft::newFromUserAndKey( $user, $conpherence->getPHID()); $draft->setDraft($request->getStr('text')); $draft->replaceOrDelete(); return new AphrontAjaxResponse(); case ConpherenceUpdateActions::MESSAGE: $message = $request->getStr('text'); $xactions = $editor->generateTransactionsFromText( $conpherence, $message); $delete_draft = true; break; case ConpherenceUpdateActions::ADD_PERSON: $person_phids = $request->getArr('add_person'); if (!empty($person_phids)) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceTransactionType::TYPE_PARTICIPANTS) ->setNewValue(array('+' => $person_phids)); } break; case ConpherenceUpdateActions::REMOVE_PERSON: if (!$request->isContinueRequest()) { // do nothing; we'll display a confirmation dialogue instead break; } $person_phid = $request->getStr('remove_person'); if ($person_phid && $person_phid == $user->getPHID()) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceTransactionType::TYPE_PARTICIPANTS) ->setNewValue(array('-' => array($person_phid))); $response_mode = 'go-home'; } break; case ConpherenceUpdateActions::NOTIFICATIONS: $notifications = $request->getStr('notifications'); $participant = $conpherence->getParticipant($user->getPHID()); $participant->setSettings(array('notifications' => $notifications)); $participant->save(); $result = pht( 'Updated notification settings to "%s".', ConpherenceSettings::getHumanString($notifications)); return id(new AphrontAjaxResponse()) ->setContent($result); break; case ConpherenceUpdateActions::METADATA: $updated = false; // all metadata updates are continue requests if (!$request->isContinueRequest()) { break; } $title = $request->getStr('title'); if ($title != $conpherence->getTitle()) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType(ConpherenceTransactionType::TYPE_TITLE) ->setNewValue($title); $updated = true; $response_mode = 'redirect'; } if (!$updated) { $errors[] = pht( 'That was a non-update. Try cancel.'); } break; default: throw new Exception('Unknown action: '.$action); break; } if ($xactions) { try { $xactions = $editor->applyTransactions($conpherence, $xactions); if ($delete_draft) { $draft = PhabricatorDraft::newFromUserAndKey( $user, $conpherence->getPHID()); $draft->delete(); } } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($this->getApplicationURI($conpherence_id.'/')) ->setException($ex); } switch ($response_mode) { case 'ajax': $latest_transaction_id = $request->getInt('latest_transaction_id'); $content = $this->loadAndRenderUpdates( $action, $conpherence_id, $latest_transaction_id); return id(new AphrontAjaxResponse()) ->setContent($content); break; case 'go-home': return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI()); break; case 'redirect': default: return id(new AphrontRedirectResponse()) ->setURI($this->getApplicationURI($conpherence->getID().'/')); break; } } } if ($errors) { $error_view = id(new AphrontErrorView()) - ->setTitle(pht('Errors editing conpherence.')) - ->setInsideDialogue(true) ->setErrors($errors); } switch ($action) { case ConpherenceUpdateActions::ADD_PERSON: $dialogue = $this->renderAddPersonDialogue($conpherence); break; case ConpherenceUpdateActions::REMOVE_PERSON: $dialogue = $this->renderRemovePersonDialogue($conpherence); break; case ConpherenceUpdateActions::METADATA: default: $dialogue = $this->renderMetadataDialogue($conpherence, $error_view); break; } return id(new AphrontDialogResponse()) ->setDialog($dialogue ->setUser($user) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setSubmitURI($this->getApplicationURI('update/'.$conpherence_id.'/')) ->addSubmitButton() ->addCancelButton($this->getApplicationURI($conpherence->getID().'/'))); } private function renderAddPersonDialogue( ConpherenceThread $conpherence) { $request = $this->getRequest(); $user = $request->getUser(); $add_person = $request->getStr('add_person'); $form = id(new PHUIFormLayoutView()) ->setUser($user) ->setFullWidth(true) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName('add_person') ->setUser($user) ->setDatasource('/typeahead/common/users/')); require_celerity_resource('conpherence-update-css'); return id(new AphrontDialogView()) ->setTitle(pht('Add Participants')) ->addHiddenInput('action', 'add_person') ->appendChild($form); } private function renderRemovePersonDialogue( ConpherenceThread $conpherence) { $request = $this->getRequest(); $user = $request->getUser(); $remove_person = $request->getStr('remove_person'); $participants = $conpherence->getParticipants(); $message = pht( 'Are you sure you want to remove yourself from this conpherence? '); if (count($participants) == 1) { $message .= pht( 'The conpherence will be inaccessible forever and ever.'); } else { $message .= pht( 'Someone else in the conpherence can add you back later.'); } $body = phutil_tag( 'p', array( ), $message); require_celerity_resource('conpherence-update-css'); return id(new AphrontDialogView()) ->setTitle(pht('Remove Participants')) ->addHiddenInput('action', 'remove_person') ->addHiddenInput('__continue__', true) ->addHiddenInput('remove_person', $remove_person) ->appendChild($body); } private function renderMetadataDialogue( ConpherenceThread $conpherence, $error_view) { $form = id(new PHUIFormLayoutView()) ->appendChild($error_view) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setName('title') ->setValue($conpherence->getTitle())); require_celerity_resource('conpherence-update-css'); return id(new AphrontDialogView()) ->setTitle(pht('Update Conpherence')) ->addHiddenInput('action', 'metadata') ->addHiddenInput('__continue__', true) ->appendChild($form); } private function loadAndRenderUpdates( $action, $conpherence_id, $latest_transaction_id) { $need_widget_data = false; $need_transactions = false; switch ($action) { case ConpherenceUpdateActions::METADATA: $need_transactions = true; break; case ConpherenceUpdateActions::MESSAGE: case ConpherenceUpdateActions::ADD_PERSON: $need_transactions = true; $need_widget_data = true; break; case ConpherenceUpdateActions::REMOVE_PERSON: case ConpherenceUpdateActions::NOTIFICATIONS: default: break; } $user = $this->getRequest()->getUser(); $conpherence = id(new ConpherenceThreadQuery()) ->setViewer($user) ->setAfterTransactionID($latest_transaction_id) ->needWidgetData($need_widget_data) ->needTransactions($need_transactions) ->withIDs(array($conpherence_id)) ->executeOne(); if ($need_transactions) { $data = $this->renderConpherenceTransactions($conpherence); } else { $data = array(); } $rendered_transactions = idx($data, 'transactions'); $new_latest_transaction_id = idx($data, 'latest_transaction_id'); $widget_uri = $this->getApplicationURI('update/'.$conpherence->getID().'/'); $nav_item = null; $header = null; $people_widget = null; $file_widget = null; switch ($action) { case ConpherenceUpdateActions::METADATA: $header = $this->buildHeaderPaneContent($conpherence); $nav_item = id(new ConpherenceThreadListView()) ->setUser($user) ->setBaseURI($this->getApplicationURI()) ->renderSingleThread($conpherence); break; case ConpherenceUpdateActions::MESSAGE: $file_widget = id(new ConpherenceFileWidgetView()) ->setUser($this->getRequest()->getUser()) ->setConpherence($conpherence) ->setUpdateURI($widget_uri); break; case ConpherenceUpdateActions::ADD_PERSON: $people_widget = id(new ConpherencePeopleWidgetView()) ->setUser($user) ->setConpherence($conpherence) ->setUpdateURI($widget_uri); break; case ConpherenceUpdateActions::REMOVE_PERSON: case ConpherenceUpdateActions::NOTIFICATIONS: default: break; } $people_html = null; if ($people_widget) { $people_html = hsprintf('%s', $people_widget->render()); } $content = array( 'transactions' => hsprintf('%s', $rendered_transactions), 'latest_transaction_id' => $new_latest_transaction_id, 'nav_item' => hsprintf('%s', $nav_item), 'conpherence_phid' => $conpherence->getPHID(), 'header' => hsprintf('%s', $header), 'file_widget' => $file_widget ? $file_widget->render() : null, 'people_widget' => $people_html, ); return $content; } } diff --git a/src/applications/system/engine/PhabricatorSystemActionEngine.php b/src/applications/system/engine/PhabricatorSystemActionEngine.php index 4b38cf4840..770faf284e 100644 --- a/src/applications/system/engine/PhabricatorSystemActionEngine.php +++ b/src/applications/system/engine/PhabricatorSystemActionEngine.php @@ -1,119 +1,122 @@ = 0) { $blocked = self::loadBlockedActors($actors, $action, $score); if ($blocked) { foreach ($blocked as $actor => $actor_score) { throw new PhabricatorSystemActionRateLimitException( $action, - $actor_score + ($score / self::getWindow())); + $actor_score); } } } self::recordAction($actors, $action, $score); } public static function loadBlockedActors( array $actors, - PhabricatorSystemAction $action) { + PhabricatorSystemAction $action, + $score) { $scores = self::loadScores($actors, $action); + $window = self::getWindow(); $blocked = array(); - foreach ($scores as $actor => $score) { - if ($action->shouldBlockActor($actor, $score)) { - $blocked[$actor] = $score; + foreach ($scores as $actor => $actor_score) { + $actor_score = $actor_score + ($score / $window); + if ($action->shouldBlockActor($actor, $actor_score)) { + $blocked[$actor] = $actor_score; } } return $blocked; } public static function loadScores( array $actors, PhabricatorSystemAction $action) { if (!$actors) { return array(); } $actor_hashes = array(); foreach ($actors as $actor) { $actor_hashes[] = PhabricatorHash::digestForIndex($actor); } $log = new PhabricatorSystemActionLog(); $window = self::getWindow(); $conn_r = $log->establishConnection('r'); $scores = queryfx_all( $conn_r, 'SELECT actorIdentity, SUM(score) totalScore FROM %T WHERE action = %s AND actorHash IN (%Ls) AND epoch >= %d GROUP BY actorHash', $log->getTableName(), $action->getActionConstant(), $actor_hashes, (time() - $window)); $scores = ipull($scores, 'totalScore', 'actorIdentity'); foreach ($scores as $key => $score) { $scores[$key] = $score / $window; } $scores = $scores + array_fill_keys($actors, 0); return $scores; } private static function recordAction( array $actors, PhabricatorSystemAction $action, $score) { $log = new PhabricatorSystemActionLog(); $conn_w = $log->establishConnection('w'); $sql = array(); foreach ($actors as $actor) { $sql[] = qsprintf( $conn_w, '(%s, %s, %s, %f, %d)', PhabricatorHash::digestForIndex($actor), $actor, $action->getActionConstant(), $score, time()); } foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (actorHash, actorIdentity, action, score, epoch) VALUES %Q', $log->getTableName(), $chunk); } } private static function getWindow() { // Limit queries to the last hour of data so we don't need to look at as // many rows. We can use an arbitrarily larger window instead (we normalize // scores to actions per second) but all the actions we care about limiting // have a limit much higher than one action per hour. return phutil_units('1 hour in seconds'); } } diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index fbf5d6747a..4b2b824897 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -1,305 +1,317 @@ method = $method; return $this; } public function setIsStandalone($is_standalone) { $this->isStandalone = $is_standalone; return $this; } + public function setErrors(array $errors) { + $this->errors = $errors; + return $this; + } + public function getIsStandalone() { return $this->isStandalone; } public function setSubmitURI($uri) { $this->submitURI = $uri; return $this; } public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } public function setShortTitle($short_title) { $this->shortTitle = $short_title; return $this; } public function getShortTitle() { return $this->shortTitle; } public function addSubmitButton($text = null) { if (!$text) { $text = pht('Okay'); } $this->submitButton = $text; return $this; } public function addCancelButton($uri, $text = null) { if (!$text) { $text = pht('Cancel'); } $this->cancelURI = $uri; $this->cancelText = $text; return $this; } public function addFooter($footer) { $this->footers[] = $footer; return $this; } public function addHiddenInput($key, $value) { if (is_array($value)) { foreach ($value as $hidden_key => $hidden_value) { $this->hidden[] = array($key.'['.$hidden_key.']', $hidden_value); } } else { $this->hidden[] = array($key, $value); } return $this; } public function setClass($class) { $this->class = $class; return $this; } public function setRenderDialogAsDiv() { // TODO: This API is awkward. $this->renderAsForm = false; return $this; } public function setFormID($id) { $this->formID = $id; return $this; } public function setWidth($width) { $this->width = $width; return $this; } public function setHeaderColor($color) { $this->headerColor = $color; return $this; } public function appendParagraph($paragraph) { return $this->appendChild( phutil_tag( 'p', array( 'class' => 'aphront-dialog-view-paragraph', ), $paragraph)); } public function setDisableWorkflowOnSubmit($disable_workflow_on_submit) { $this->disableWorkflowOnSubmit = $disable_workflow_on_submit; return $this; } public function getDisableWorkflowOnSubmit() { return $this->disableWorkflowOnSubmit; } public function setDisableWorkflowOnCancel($disable_workflow_on_cancel) { $this->disableWorkflowOnCancel = $disable_workflow_on_cancel; return $this; } public function getDisableWorkflowOnCancel() { return $this->disableWorkflowOnCancel; } final public function render() { require_celerity_resource('aphront-dialog-view-css'); $buttons = array(); if ($this->submitButton) { $meta = array(); if ($this->disableWorkflowOnSubmit) { $meta['disableWorkflow'] = true; } $buttons[] = javelin_tag( 'button', array( 'name' => '__submit__', 'sigil' => '__default__', 'type' => 'submit', 'meta' => $meta, ), $this->submitButton); } if ($this->cancelURI) { $meta = array(); if ($this->disableWorkflowOnCancel) { $meta['disableWorkflow'] = true; } $buttons[] = javelin_tag( 'a', array( 'href' => $this->cancelURI, 'class' => 'button grey', 'name' => '__cancel__', 'sigil' => 'jx-workflow-button', 'meta' => $meta, ), $this->cancelText); } if (!$this->user) { throw new Exception( pht("You must call setUser() when rendering an AphrontDialogView.")); } $more = $this->class; switch ($this->width) { case self::WIDTH_FORM: case self::WIDTH_FULL: $more .= ' aphront-dialog-view-width-'.$this->width; break; case self::WIDTH_DEFAULT: break; default: throw new Exception("Unknown dialog width '{$this->width}'!"); } if ($this->isStandalone) { $more .= ' aphront-dialog-view-standalone'; } $attributes = array( 'class' => 'aphront-dialog-view '.$more, 'sigil' => 'jx-dialog', ); $form_attributes = array( 'action' => $this->submitURI, 'method' => $this->method, 'id' => $this->formID, ); $hidden_inputs = array(); $hidden_inputs[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => '__dialog__', 'value' => '1', )); foreach ($this->hidden as $desc) { list($key, $value) = $desc; $hidden_inputs[] = javelin_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, 'sigil' => 'aphront-dialog-application-input' )); } if (!$this->renderAsForm) { $buttons = array(phabricator_form( $this->user, $form_attributes, array_merge($hidden_inputs, $buttons))); } $children = $this->renderChildren(); + if ($this->errors) { + $children = array( + id(new AphrontErrorView())->setErrors($this->errors), + $children); + } + $header = new PhabricatorActionHeaderView(); $header->setHeaderTitle($this->title); $header->setHeaderColor($this->headerColor); $footer = null; if ($this->footers) { $footer = phutil_tag( 'div', array( 'class' => 'aphront-dialog-foot', ), $this->footers); } $content = array( phutil_tag( 'div', array( 'class' => 'aphront-dialog-head', ), $header), phutil_tag('div', array( 'class' => 'aphront-dialog-body grouped', ), $children), phutil_tag( 'div', array( 'class' => 'aphront-dialog-tail grouped', ), array( $buttons, $footer, )), ); if ($this->renderAsForm) { return phabricator_form( $this->user, $form_attributes + $attributes, array($hidden_inputs, $content)); } else { return javelin_tag( 'div', $attributes, $content); } } } diff --git a/src/view/form/AphrontErrorView.php b/src/view/form/AphrontErrorView.php index 938f0ae9ec..393cbe9739 100644 --- a/src/view/form/AphrontErrorView.php +++ b/src/view/form/AphrontErrorView.php @@ -1,114 +1,96 @@ insideDialogue = $inside_dialogue; - return $this; - } - public function getInsideDialogue() { - return $this->insideDialogue; - } public function setTitle($title) { $this->title = $title; return $this; } public function setSeverity($severity) { $this->severity = $severity; return $this; } public function setErrors(array $errors) { $this->errors = $errors; return $this; } public function setID($id) { $this->id = $id; return $this; } - private function getBaseClass() { - if ($this->getInsideDialogue()) { - $class = 'aphront-error-view-dialogue'; - } else { - $class = 'aphront-error-view'; - } - return $class; - } - final public function render() { require_celerity_resource('aphront-error-view-css'); $errors = $this->errors; if ($errors) { $list = array(); foreach ($errors as $error) { $list[] = phutil_tag( 'li', array(), $error); } $list = phutil_tag( 'ul', array( 'class' => 'aphront-error-view-list', ), $list); } else { $list = null; } $title = $this->title; if (strlen($title)) { $title = phutil_tag( 'h1', array( 'class' => 'aphront-error-view-head', ), $title); } else { $title = null; } $this->severity = nonempty($this->severity, self::SEVERITY_ERROR); $classes = array(); - $classes[] = $this->getBaseClass(); + $classes[] = 'aphront-error-view'; $classes[] = 'aphront-error-severity-'.$this->severity; $classes = implode(' ', $classes); $children = $this->renderChildren(); $children[] = $list; return phutil_tag( 'div', array( 'id' => $this->id, 'class' => $classes, ), array( $title, phutil_tag( 'div', array( 'class' => 'aphront-error-view-body', ), $children), )); } } diff --git a/webroot/rsrc/css/aphront/error-view.css b/webroot/rsrc/css/aphront/error-view.css index f6568c75d5..0d1696b33d 100644 --- a/webroot/rsrc/css/aphront/error-view.css +++ b/webroot/rsrc/css/aphront/error-view.css @@ -1,86 +1,89 @@ /** * @provides aphront-error-view-css */ -.aphront-error-view, -.aphront-error-view-dialogue { +.aphront-error-view { border-style: solid; border-width: 1px; } -form.aphront-dialog-view .aphront-error-view { - margin: 0 0 12px 0; -} - .aphront-error-view { margin: 16px; } -.aphront-error-view-dialogue { - margin: 0 0 16px 0; -} .device-phone .aphront-error-view { margin: 8px; } .aphront-error-view .phui-form-view { padding: 0; } .aphront-error-view-body { padding: 12px; } .aphront-panel-plain .aphront-error-view { margin-left: 0; margin-right: 0; } h1.aphront-error-view-head { padding: 12px 8px 0 12px; font-weight: bold; font-size: 15px; color: {$darkgreytext}; } .aphront-error-view-list { margin: 0 0 0 16px; list-style: disc; } .aphront-error-severity-error { color: {$red}; border-color: {$red}; background: {$lightred}; } .aphront-error-severity-error .aphront-error-view-head { color: {$red}; } .aphront-error-severity-warning { color: #bc7837; border-color: {$yellow}; background: {$lightyellow}; } .aphront-error-severity-warning .aphront-error-view-head { color: #bc7837; } .aphront-error-severity-notice { color: {$blue}; border-color: {$blue}; background: {$lightblue}; } .aphront-error-severity-notice .aphront-error-view-head { color: {$blue}; } .aphront-error-severity-nodata { border-color: {$lightgreyborder}; border-bottom: 1px solid {$greyborder}; color: {$greytext}; background-color: #fff; } + +.aphront-dialog-body .aphront-error-view { + margin: -16px -16px 16px -16px; + border-width: 0 0 1px 0; + border-bottom: 1px solid {$lightblueborder}; +} + +.aphront-dialog-body .aphront-error-view .aphront-error-view-list { + margin: 0 0 0 16px; + list-style: disc; +}