mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 00:32:42 +01:00
Some acutal conduit authentication.
This commit is contained in:
parent
1c586db9be
commit
605268f9aa
17 changed files with 379 additions and 22 deletions
|
@ -22,6 +22,10 @@ return array(
|
|||
// Example: "http://phabricator.example.com/"
|
||||
'phabricator.base-uri' => null,
|
||||
|
||||
// The Conduit URI for API access to this install. Normally this is just
|
||||
// the 'base-uri' plus "/api/" (e.g. "http://phabricator.example.com/api/"),
|
||||
// but make sure you specify 'https' if you have HTTPS configured.
|
||||
'phabricator.conduit-uri' => null,
|
||||
|
||||
'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3',
|
||||
|
||||
|
|
|
@ -197,6 +197,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base',
|
||||
'PhabricatorUser' => 'applications/people/storage/user',
|
||||
'PhabricatorUserDAO' => 'applications/people/storage/base',
|
||||
'PhabricatorUserSettingsController' => 'applications/people/controller/settings',
|
||||
'PhabricatorXHProfController' => 'applications/xhprof/controller/base',
|
||||
'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/profile',
|
||||
'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/symbol',
|
||||
|
@ -373,6 +374,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
|
||||
'PhabricatorUser' => 'PhabricatorUserDAO',
|
||||
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
|
||||
'PhabricatorUserSettingsController' => 'PhabricatorPeopleController',
|
||||
'PhabricatorXHProfController' => 'PhabricatorController',
|
||||
'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController',
|
||||
'PhabricatorXHProfProfileSymbolView' => 'AphrontView',
|
||||
|
|
|
@ -135,6 +135,10 @@ class AphrontDefaultApplicationConfiguration
|
|||
),
|
||||
|
||||
'/~/' => 'DarkConsoleController',
|
||||
|
||||
'/settings/' => array(
|
||||
'(?:page/(?<page>[^/]+)/)?$' => 'PhabricatorUserSettingsController',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -146,4 +146,8 @@ class AphrontRequest {
|
|||
return id(new PhutilURI($this->getPath()))->setQueryParams($get);
|
||||
}
|
||||
|
||||
final public function isDialogFormPost() {
|
||||
return $this->isFormPost() && $this->getStr('__dialog__');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -81,6 +81,35 @@ class PhabricatorConduitAPIController
|
|||
|
||||
$api_request = new ConduitAPIRequest($params);
|
||||
|
||||
if ($method_handler->shouldRequireAuthentication()) {
|
||||
$session_key = idx($metadata, 'sessionKey');
|
||||
if (!$session_key) {
|
||||
$auth_okay = false;
|
||||
$error_code = 'ERR-NO-CERTIFICATE';
|
||||
$error_info = "This server requires authentication but your client ".
|
||||
"is not configured with an authentication certificate.";
|
||||
} else {
|
||||
$user = new PhabricatorUser();
|
||||
$session = queryfx_one(
|
||||
$user->establishConnection('r'),
|
||||
'SELECT * FROM %T WHERE sessionKey = %s',
|
||||
PhabricatorUser::SESSION_TABLE,
|
||||
$session_key);
|
||||
if (!$session) {
|
||||
$auth_okay = false;
|
||||
$error_code = 'ERR-INVALID-SESSION';
|
||||
$error_info = 'Session key is invalid.';
|
||||
} else {
|
||||
// TODO: Make sessions timeout.
|
||||
$auth_okay = true;
|
||||
}
|
||||
}
|
||||
// TODO: When we session, read connectionID from the session table.
|
||||
} else {
|
||||
$auth_okay = true;
|
||||
}
|
||||
|
||||
if ($auth_okay) {
|
||||
try {
|
||||
$result = $method_handler->executeMethod($api_request);
|
||||
$error_code = null;
|
||||
|
@ -90,6 +119,7 @@ class PhabricatorConduitAPIController
|
|||
$error_code = $ex->getMessage();
|
||||
$error_info = $method_handler->getErrorDescription($error_code);
|
||||
}
|
||||
}
|
||||
} catch (Exception $ex) {
|
||||
$result = null;
|
||||
$error_code = 'ERR-CONDUIT-CORE';
|
||||
|
|
|
@ -11,6 +11,8 @@ phutil_require_module('phabricator', 'applications/conduit/controller/base');
|
|||
phutil_require_module('phabricator', 'applications/conduit/method/base');
|
||||
phutil_require_module('phabricator', 'applications/conduit/protocol/request');
|
||||
phutil_require_module('phabricator', 'applications/conduit/storage/methodcalllog');
|
||||
phutil_require_module('phabricator', 'applications/people/storage/user');
|
||||
phutil_require_module('phabricator', 'storage/queryfx');
|
||||
phutil_require_module('phabricator', 'view/control/table');
|
||||
phutil_require_module('phabricator', 'view/layout/panel');
|
||||
|
||||
|
|
|
@ -45,6 +45,10 @@ abstract class ConduitAPIMethod {
|
|||
return 'ConduitAPI_'.$method_fragment.'_Method';
|
||||
}
|
||||
|
||||
public function shouldRequireAuthentication() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getAPIMethodNameFromClassName($class_name) {
|
||||
$match = null;
|
||||
$is_valid = preg_match(
|
||||
|
|
|
@ -18,6 +18,10 @@
|
|||
|
||||
class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
|
||||
|
||||
public function shouldRequireAuthentication() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getMethodDescription() {
|
||||
return "Connect a session-based client.";
|
||||
}
|
||||
|
@ -28,6 +32,8 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
|
|||
'clientVersion' => 'required int',
|
||||
'clientDescription' => 'optional string',
|
||||
'user' => 'optional string',
|
||||
'authToken' => 'optional int',
|
||||
'authSignature' => 'optional string',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -47,6 +53,17 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
|
|||
"a Facebook host, or see ".
|
||||
"<http://www.intern.facebook.com/intern/wiki/index.php/Arcanist> for ".
|
||||
"laptop instructions.",
|
||||
"ERR-INVALID-USER" =>
|
||||
"The username you are attempting to authenticate with is not valid.",
|
||||
"ERR-INVALID-CERTIFICATE" =>
|
||||
"Your authentication certificate for this server is invalid.",
|
||||
"ERR-INVALID-TOKEN" =>
|
||||
"The challenge token you are authenticating with is outside of the ".
|
||||
"allowed time range. Either your system clock is out of whack or ".
|
||||
"you're executing a replay attack.",
|
||||
"ERR-NO-CERTIFICATE" =>
|
||||
"This server requires authentication but your client is not ".
|
||||
"configured with an authentication certificate."
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -55,13 +72,14 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
|
|||
$client = $request->getValue('client');
|
||||
$client_version = (int)$request->getValue('clientVersion');
|
||||
$client_description = (string)$request->getValue('clientDescription');
|
||||
$username = (string)$request->getValue('user');
|
||||
|
||||
// Log the connection, regardless of the outcome of checks below.
|
||||
$connection = new PhabricatorConduitConnectionLog();
|
||||
$connection->setClient($client);
|
||||
$connection->setClientVersion($client_version);
|
||||
$connection->setClientDescription($client_description);
|
||||
$connection->setUsername((string)$request->getValue('user'));
|
||||
$connection->setUsername($username);
|
||||
$connection->save();
|
||||
|
||||
switch ($client) {
|
||||
|
@ -80,8 +98,58 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod {
|
|||
throw new ConduitException('ERR-UNKNOWN-CLIENT');
|
||||
}
|
||||
|
||||
$token = $request->getValue('authToken');
|
||||
$signature = $request->getValue('authSignature');
|
||||
|
||||
$user = id(new PhabricatorUser())->loadOneWhere(
|
||||
'username = %s',
|
||||
$username);
|
||||
if (!$user) {
|
||||
throw new ConduitException('ERR-INVALID-USER');
|
||||
}
|
||||
|
||||
$session_key = null;
|
||||
if ($token && $signature) {
|
||||
if (abs($token - time()) > 60 * 15) {
|
||||
throw new ConduitException('ERR-INVALID-TOKEN');
|
||||
}
|
||||
$valid = sha1($token.$user->getConduitCertificate());
|
||||
if ($valid != $signature) {
|
||||
throw new ConduitException('ERR-INVALID-CERTIFICATE');
|
||||
}
|
||||
|
||||
$sessions = queryfx_all(
|
||||
$user->establishConnection('r'),
|
||||
'SELECT * FROM %T WHERE userPHID = %s AND type LIKE %>',
|
||||
PhabricatorUser::SESSION_TABLE,
|
||||
$user->getPHID(),
|
||||
'conduit-');
|
||||
|
||||
$session_type = null;
|
||||
|
||||
$sessions = ipull($sessions, null, 'type');
|
||||
for ($ii = 1; $ii <= 3; $ii++) {
|
||||
if (empty($sessions['conduit-'.$ii])) {
|
||||
$session_type = 'conduit-'.$ii;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$session_type) {
|
||||
$sessions = isort($sessions, 'sessionStart');
|
||||
$oldest = reset($sessions);
|
||||
$session_type = $oldest['type'];
|
||||
}
|
||||
|
||||
$session_key = $user->establishSession($session_type);
|
||||
} else {
|
||||
throw new ConduitException('ERR-NO-CERTIFICATE');
|
||||
}
|
||||
|
||||
return array(
|
||||
'connectionID' => $connection->getID(),
|
||||
'sessionKey' => $session_key,
|
||||
'userPHID' => $user->getPHID(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
phutil_require_module('phabricator', 'applications/conduit/method/base');
|
||||
phutil_require_module('phabricator', 'applications/conduit/protocol/exception');
|
||||
phutil_require_module('phabricator', 'applications/conduit/storage/connectionlog');
|
||||
phutil_require_module('phabricator', 'applications/people/storage/user');
|
||||
phutil_require_module('phabricator', 'storage/queryfx');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('ConduitAPI_conduit_connect_Method.php');
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class PhabricatorUserSettingsController extends PhabricatorPeopleController {
|
||||
|
||||
private $page;
|
||||
|
||||
public function willProcessRequest(array $data) {
|
||||
$this->page = idx($data, 'page');
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
$pages = array(
|
||||
// 'personal' => 'Profile',
|
||||
// 'password' => 'Password',
|
||||
// 'facebook' => 'Facebook Account',
|
||||
'arcanist' => 'Arcanist Certificate',
|
||||
);
|
||||
|
||||
if (empty($pages[$this->page])) {
|
||||
$this->page = key($pages);
|
||||
}
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
switch ($this->page) {
|
||||
case 'arcanist':
|
||||
|
||||
if (!$request->isDialogFormPost()) {
|
||||
$dialog = new AphrontDialogView();
|
||||
$dialog->setUser($user);
|
||||
$dialog->setTitle('Really regenerate session?');
|
||||
$dialog->setSubmitURI('/settings/page/arcanist/');
|
||||
$dialog->addSubmitButton('Regenerate');
|
||||
$dialog->addCancelbutton('/settings/page/arcanist/');
|
||||
$dialog->appendChild(
|
||||
'<p>Really destroy the old certificate? Any established '.
|
||||
'sessions will be terminated.');
|
||||
|
||||
return id(new AphrontDialogResponse())
|
||||
->setDialog($dialog);
|
||||
}
|
||||
|
||||
$conn = $user->establishConnection('w');
|
||||
queryfx(
|
||||
$conn,
|
||||
'DELETE FROM %T WHERE userPHID = %s AND type LIKE %>',
|
||||
PhabricatorUser::SESSION_TABLE,
|
||||
$user->getPHID(),
|
||||
'conduit');
|
||||
// This implicitly regenerates the certificate.
|
||||
$user->setConduitCertificate(null);
|
||||
$user->save();
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI('/settings/page/arcanist/?regenerated=true');
|
||||
}
|
||||
}
|
||||
|
||||
switch ($this->page) {
|
||||
case 'arcanist':
|
||||
$content = $this->renderArcanistCertificateForm();
|
||||
break;
|
||||
default:
|
||||
$content = 'derp derp';
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
$sidenav = new AphrontSideNavView();
|
||||
foreach ($pages as $page => $name) {
|
||||
$sidenav->addNavItem(
|
||||
phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '/settings/page/'.$page.'/',
|
||||
'class' => ($page == $this->page)
|
||||
? 'aphront-side-nav-selected'
|
||||
: null,
|
||||
),
|
||||
phutil_escape_html($name)));
|
||||
}
|
||||
|
||||
$sidenav->appendChild($content);
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
$sidenav,
|
||||
array(
|
||||
'title' => 'Account Settings',
|
||||
));
|
||||
}
|
||||
|
||||
private function renderArcanistCertificateForm() {
|
||||
$request = $this->getRequest();
|
||||
$user = $request->getUser();
|
||||
|
||||
if ($request->getStr('regenerated')) {
|
||||
$notice = new AphrontErrorView();
|
||||
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
|
||||
$notice->setTitle('Certificate Regenerated');
|
||||
$notice->appendChild(
|
||||
'<p>Your old certificate has been destroyed and you have been issued '.
|
||||
'a new certificate. Sessions established under the old certificate '.
|
||||
'are no longer valid.</p>');
|
||||
$notice = $notice->render();
|
||||
} else {
|
||||
$notice = null;
|
||||
}
|
||||
|
||||
$host = PhabricatorEnv::getEnvConfig('phabricator.conduit-uri');
|
||||
|
||||
$cert_form = new AphrontFormView();
|
||||
$cert_form
|
||||
->setUser($user)
|
||||
->appendChild(
|
||||
'<p class="aphront-form-instructions">Copy and paste this certificate '.
|
||||
'into your <tt>~/.arcconfig</tt> in the "hosts" section to enable '.
|
||||
'Arcanist to authenticate against this host.</p>')
|
||||
->appendChild(
|
||||
id(new AphrontFormTextAreaControl())
|
||||
->setLabel('Certificate')
|
||||
->setHeight(AphrontFormTextAreaControl::HEIGHT_SHORT)
|
||||
->setValue($user->getConduitCertificate()));
|
||||
|
||||
$cert = new AphrontPanelView();
|
||||
$cert->setHeader('Arcanist Certificate');
|
||||
$cert->appendChild($cert_form);
|
||||
$cert->setWidth(AphrontPanelView::WIDTH_FORM);
|
||||
|
||||
$regen_form = new AphrontFormView();
|
||||
$regen_form
|
||||
->setUser($user)
|
||||
->setWorkflow(true)
|
||||
->setAction('/settings/page/arcanist/')
|
||||
->appendChild(
|
||||
'<p class="aphront-form-instructions">You can regenerate this '.
|
||||
'certificate, which will invalidate the old certificate and create '.
|
||||
'a new one.</p>')
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue('Regenerate Certificate'));
|
||||
|
||||
$regen = new AphrontPanelView();
|
||||
$regen->setHeader('Regenerate Certificate');
|
||||
$regen->appendChild($regen_form);
|
||||
$regen->setWidth(AphrontPanelView::WIDTH_FORM);
|
||||
|
||||
return $notice.$cert->render().$regen->render();
|
||||
}
|
||||
|
||||
}
|
27
src/applications/people/controller/settings/__init__.php
Normal file
27
src/applications/people/controller/settings/__init__.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/response/dialog');
|
||||
phutil_require_module('phabricator', 'aphront/response/redirect');
|
||||
phutil_require_module('phabricator', 'applications/people/controller/base');
|
||||
phutil_require_module('phabricator', 'applications/people/storage/user');
|
||||
phutil_require_module('phabricator', 'infrastructure/env');
|
||||
phutil_require_module('phabricator', 'storage/queryfx');
|
||||
phutil_require_module('phabricator', 'view/dialog');
|
||||
phutil_require_module('phabricator', 'view/form/base');
|
||||
phutil_require_module('phabricator', 'view/form/control/submit');
|
||||
phutil_require_module('phabricator', 'view/form/control/textarea');
|
||||
phutil_require_module('phabricator', 'view/form/error');
|
||||
phutil_require_module('phabricator', 'view/layout/panel');
|
||||
phutil_require_module('phabricator', 'view/layout/sidenav');
|
||||
|
||||
phutil_require_module('phutil', 'markup');
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('PhabricatorUserSettingsController.php');
|
|
@ -20,6 +20,8 @@ class PhabricatorUser extends PhabricatorUserDAO {
|
|||
|
||||
const PHID_TYPE = 'USER';
|
||||
|
||||
const SESSION_TABLE = 'phabricator_session';
|
||||
|
||||
protected $phid;
|
||||
protected $userName;
|
||||
protected $realName;
|
||||
|
@ -33,6 +35,8 @@ class PhabricatorUser extends PhabricatorUserDAO {
|
|||
protected $consoleVisible = 0;
|
||||
protected $consoleTab = '';
|
||||
|
||||
protected $conduitCertificate;
|
||||
|
||||
public function getProfileImagePHID() {
|
||||
return nonempty(
|
||||
$this->profileImagePHID,
|
||||
|
@ -56,6 +60,34 @@ class PhabricatorUser extends PhabricatorUserDAO {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function save() {
|
||||
if (!$this->conduitCertificate) {
|
||||
$this->conduitCertificate = $this->generateConduitCertificate();
|
||||
}
|
||||
return parent::save();
|
||||
}
|
||||
|
||||
private function generateConduitCertificate() {
|
||||
$entropy = $this->generateEntropy($bytes = 256);
|
||||
$entropy = base64_encode($entropy);
|
||||
$entropy = substr($entropy, 0, 255);
|
||||
return $entropy;
|
||||
}
|
||||
|
||||
private function generateEntropy($bytes) {
|
||||
$urandom = fopen('/dev/urandom', 'r');
|
||||
if (!$urandom) {
|
||||
throw new Exception("Failed to open /dev/urandom!");
|
||||
}
|
||||
|
||||
$entropy = fread($urandom, $bytes);
|
||||
if (strlen($entropy) != $bytes) {
|
||||
throw new Exception("Failed to read /dev/urandom!");
|
||||
}
|
||||
|
||||
return $entropy;
|
||||
}
|
||||
|
||||
public function comparePassword($password) {
|
||||
$password = $this->hashPassword($password);
|
||||
return ($password === $this->getPasswordHash());
|
||||
|
@ -105,26 +137,19 @@ class PhabricatorUser extends PhabricatorUserDAO {
|
|||
public function establishSession($session_type) {
|
||||
$conn_w = $this->establishConnection('w');
|
||||
|
||||
$urandom = fopen('/dev/urandom', 'r');
|
||||
if (!$urandom) {
|
||||
throw new Exception("Failed to open /dev/urandom!");
|
||||
}
|
||||
|
||||
$entropy = fread($urandom, 20);
|
||||
if (strlen($entropy) != 20) {
|
||||
throw new Exception("Failed to read /dev/urandom!");
|
||||
}
|
||||
$entropy = $this->generateEntropy($bytes = 20);
|
||||
|
||||
$session_key = sha1($entropy);
|
||||
queryfx(
|
||||
$conn_w,
|
||||
'INSERT INTO phabricator_session '.
|
||||
'INSERT INTO %T '.
|
||||
'(userPHID, type, sessionKey, sessionStart)'.
|
||||
' VALUES '.
|
||||
'(%s, %s, %s, UNIX_TIMESTAMP()) '.
|
||||
'ON DUPLICATE KEY UPDATE '.
|
||||
'sessionKey = VALUES(sessionKey), '.
|
||||
'sessionStart = VALUES(sessionStart)',
|
||||
self::SESSION_TABLE,
|
||||
$this->getPHID(),
|
||||
$session_type,
|
||||
$session_key);
|
||||
|
|
|
@ -113,6 +113,7 @@ class AphrontDialogView extends AphrontView {
|
|||
),
|
||||
'<input type="hidden" name="__form__" value="1" />'.
|
||||
'<input type="hidden" name="__csrf__" value="'.$csrf.'" />'.
|
||||
'<input type="hidden" name="__dialog__" value="1" />'.
|
||||
$hidden_inputs.
|
||||
'<div class="aphront-dialog-head">'.
|
||||
phutil_escape_html($this->title).
|
||||
|
|
|
@ -24,6 +24,7 @@ final class AphrontFormView extends AphrontView {
|
|||
private $data = array();
|
||||
private $encType;
|
||||
private $user;
|
||||
private $workflow;
|
||||
|
||||
public function setUser(PhabricatorUser $user) {
|
||||
$this->user = $user;
|
||||
|
@ -50,15 +51,21 @@ final class AphrontFormView extends AphrontView {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function setWorkflow($workflow) {
|
||||
$this->workflow = $workflow;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function render() {
|
||||
require_celerity_resource('aphront-form-view-css');
|
||||
return phutil_render_tag(
|
||||
return javelin_render_tag(
|
||||
'form',
|
||||
array(
|
||||
'action' => $this->action,
|
||||
'method' => $this->method,
|
||||
'class' => 'aphront-form-view',
|
||||
'enctype' => $this->encType,
|
||||
'sigil' => $this->workflow ? 'workflow' : null,
|
||||
),
|
||||
$this->renderDataInputs().
|
||||
$this->renderChildren());
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
|
||||
phutil_require_module('phabricator', 'infrastructure/celerity/api');
|
||||
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
|
||||
phutil_require_module('phabricator', 'view/base');
|
||||
|
||||
phutil_require_module('phutil', 'markup');
|
||||
|
|
|
@ -20,7 +20,7 @@ final class AphrontErrorView extends AphrontView {
|
|||
|
||||
const SEVERITY_ERROR = 'error';
|
||||
const SEVERITY_WARNING = 'warning';
|
||||
const SEVERITY_NOTE = 'note';
|
||||
const SEVERITY_NOTICE = 'notice';
|
||||
|
||||
const WIDTH_DEFAULT = 'default';
|
||||
const WIDTH_WIDE = 'wide';
|
||||
|
|
|
@ -28,3 +28,9 @@
|
|||
border: 1px solid #888800;
|
||||
background: #ffffdd;
|
||||
}
|
||||
|
||||
.aphront-error-severity-notice {
|
||||
border: 1px solid #000088;
|
||||
background: #e3e3ff;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue