1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00

Start of a config web interface.

Summary:
This is somewhat clowny, particularly in how it handles JSON encode/decode, but
I've commented why I did things the way I did. The goal is to store minified JSON
but show pretty-printed JSON where possible, to the user editing it.

Test Plan:
* Went to /config/ and saw a list of keys from the `default` config.
* Clicked on one of them, submitted the default value successfully.
* Changed the value to invalid JSON and got a decent error.
* Changed the value to valid JSON and checked the DB to confirm it saved.
* Confirmed the DB values were minified.
* Confirmed the user-facing values were pretty-printed where they could be.
* Confirmed that PHIDs were getting assigned properly and that isDeleted
  properly defaulted to false/0.

Reviewers: epriestley

Reviewed By: epriestley

CC: aran, Korvin

Maniphest Tasks: T2246

Differential Revision: https://secure.phabricator.com/D4290
This commit is contained in:
Ricky Elrod 2012-12-27 15:20:09 -08:00 committed by epriestley
parent 1e2dfb5b6b
commit a774620042
11 changed files with 352 additions and 0 deletions

View file

@ -0,0 +1,12 @@
CREATE TABLE {$NAMESPACE}_config.config_entry (
`id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
`phid` VARCHAR(64) NOT NULL COLLATE utf8_bin,
`namespace` VARCHAR(64) BINARY NOT NULL COLLATE utf8_bin,
`configKey` VARCHAR(64) BINARY NOT NULL COLLATE utf8_bin,
`value` LONGTEXT NOT NULL,
`isDeleted` BOOL NOT NULL,
`dateCreated` INT UNSIGNED NOT NULL,
`dateModified` INT UNSIGNED NOT NULL,
UNIQUE KEY `key_phid` (`phid`),
UNIQUE KEY `key_name` (`namespace`, `configKey`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View file

@ -580,6 +580,7 @@ phutil_register_library_map(array(
'PhabricatorApplicationAuth' => 'applications/auth/application/PhabricatorApplicationAuth.php',
'PhabricatorApplicationCalendar' => 'applications/calendar/application/PhabricatorApplicationCalendar.php',
'PhabricatorApplicationConduit' => 'applications/conduit/application/PhabricatorApplicationConduit.php',
'PhabricatorApplicationConfig' => 'applications/config/application/PhabricatorApplicationConfig.php',
'PhabricatorApplicationCountdown' => 'applications/countdown/application/PhabricatorApplicationCountdown.php',
'PhabricatorApplicationDaemons' => 'applications/daemon/application/PhabricatorApplicationDaemons.php',
'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php',
@ -679,8 +680,13 @@ phutil_register_library_map(array(
'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php',
'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php',
'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php',
'PhabricatorConfigController' => 'applications/config/controller/PhabricatorConfigController.php',
'PhabricatorConfigDictionarySource' => 'infrastructure/env/PhabricatorConfigDictionarySource.php',
'PhabricatorConfigEditController' => 'applications/config/controller/PhabricatorConfigEditController.php',
'PhabricatorConfigEntry' => 'applications/config/storage/PhabricatorConfigEntry.php',
'PhabricatorConfigEntryDAO' => 'applications/config/storage/PhabricatorConfigEntryDAO.php',
'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php',
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
'PhabricatorConfigStackSource' => 'infrastructure/env/PhabricatorConfigStackSource.php',
@ -1873,6 +1879,7 @@ phutil_register_library_map(array(
'PhabricatorApplicationAuth' => 'PhabricatorApplication',
'PhabricatorApplicationCalendar' => 'PhabricatorApplication',
'PhabricatorApplicationConduit' => 'PhabricatorApplication',
'PhabricatorApplicationConfig' => 'PhabricatorApplication',
'PhabricatorApplicationCountdown' => 'PhabricatorApplication',
'PhabricatorApplicationDaemons' => 'PhabricatorApplication',
'PhabricatorApplicationDifferential' => 'PhabricatorApplication',
@ -1986,8 +1993,13 @@ phutil_register_library_map(array(
'PhabricatorConduitLogController' => 'PhabricatorConduitController',
'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO',
'PhabricatorConduitTokenController' => 'PhabricatorConduitController',
'PhabricatorConfigController' => 'PhabricatorController',
'PhabricatorConfigDictionarySource' => 'PhabricatorConfigSource',
'PhabricatorConfigEditController' => 'PhabricatorConfigController',
'PhabricatorConfigEntry' => 'PhabricatorConfigEntryDAO',
'PhabricatorConfigEntryDAO' => 'PhabricatorLiskDAO',
'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource',
'PhabricatorConfigListController' => 'PhabricatorConfigController',
'PhabricatorConfigProxySource' => 'PhabricatorConfigSource',
'PhabricatorConfigStackSource' => 'PhabricatorConfigSource',
'PhabricatorContentSourceView' => 'AphrontView',

View file

@ -0,0 +1,34 @@
<?php
final class PhabricatorApplicationConfig extends PhabricatorApplication {
public function getBaseURI() {
return '/config/';
}
public function getIconName() {
return 'config';
}
public function getTitleGlyph() {
return "\xE2\x98\xBA";
}
public function getApplicationGroup() {
return self::GROUP_ADMIN;
}
public function shouldAppearInLaunchView() {
return false;
}
public function getRoutes() {
return array(
'/config/' => array(
'' => 'PhabricatorConfigListController',
'edit/(?P<key>[\w\.\-]+)/' => 'PhabricatorConfigEditController',
),
);
}
}

View file

@ -0,0 +1,27 @@
<?php
abstract class PhabricatorConfigController extends PhabricatorController {
public function shouldRequireAdmin() {
return true;
}
public function buildSideNavView($filter = null, $for_app = false) {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI('filter/')));
return $nav;
}
public function buildApplicationMenu() {
return $this->buildSideNavView(null, true)->getMenu();
}
public function buildApplicationCrumbs() {
$crumbs = parent::buildApplicationCrumbs();
return $crumbs;
}
}

View file

@ -0,0 +1,124 @@
<?php
final class PhabricatorConfigEditController
extends PhabricatorConfigController {
private $key;
public function willProcessRequest(array $data) {
$this->key = idx($data, 'key');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$config = id(new PhabricatorConfigFileSource('default'))
->getAllKeys();
if (!$this->key || !array_key_exists($this->key, $config)) {
return new Aphront404Response();
}
// Check if the config key is already stored in the database.
// Grab the value if it is.
$value = null;
$config_entry = id(new PhabricatorConfigEntry())
->loadOneWhere(
'configKey = %s AND namespace=%s',
$this->key,
'default');
if ($config_entry) {
$value = $config_entry->getValue();
} else {
$config_entry = id(new PhabricatorConfigEntry())
->setConfigKey($this->key);
}
$e_value = null;
$errors = array();
if ($request->isFormPost()) {
$new_value = $request->getStr('value');
if (strlen($new_value)) {
$json = json_decode($new_value, true);
if ($json === null && strtolower($value) != 'null') {
$e_value = 'Invalid';
$errors[] = 'The given value must be valid JSON. This means, among '.
'other things, that you must wrap strings in double-quotes.';
$value = $new_value;
} else {
$value = $json;
}
} else {
// TODO: When we do Transactions, make this just set isDeleted = 1
$config_entry->delete();
}
$config_entry->setValue($value);
$config_entry->setNamespace('default');
if (!$errors) {
$config_entry->save();
return id(new AphrontRedirectResponse())
->setURI($config_entry->getURI());
}
}
$form = new AphrontFormView();
$form->setFlexible(true);
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('You broke everything!')
->setErrors($errors);
} else {
// Check not only that it's an array, but that it's an "unnatural" array
// meaning that the keys aren't 0 -> size_of_array.
if (is_array($value) &&
array_keys($value) != range(0, count($value) - 1)) {
$value = id(new PhutilJSON())->encodeFormatted($value);
} else {
$value = json_encode($value);
}
}
$form
->setUser($user)
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('JSON Value')
->setError($e_value)
->setValue($value)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
->setCustomClass('PhabricatorMonospaced')
->setName('value'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($config_entry->getURI())
->setValue(pht('Save Config Entry')));
$title = pht('Edit %s', $this->key);
$short = pht('Edit');
$crumbs = $this->buildApplicationCrumbs($this->buildSideNavView());
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($this->key)
->setHref('/config/edit/'.$this->key));
$crumbs->addCrumb(
id(new PhabricatorCrumbView())->setName($short));
return $this->buildApplicationPage(
array(
$crumbs,
id(new PhabricatorHeaderView())->setHeader($title),
$error_view,
$form,
),
array(
'title' => $title,
'device' => true,
));
}
}

View file

@ -0,0 +1,62 @@
<?php
final class PhabricatorConfigListController
extends PhabricatorConfigController {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$nav = $this->buildSideNavView();
$pager = new AphrontCursorPagerView();
$pager->readFromRequest($request);
$config = new PhabricatorConfigFileSource('default');
$list = $this->buildConfigList(array_keys($config->getAllKeys()));
$list->setPager($pager);
$list->setNoDataString(
'No data. Something probably went wrong in reading the default config.');
$header = id(new PhabricatorHeaderView())
->setHeader(pht('Configuration'));
$nav->appendChild(
array(
$header,
$list,
));
$crumbs = $this
->buildApplicationCrumbs($nav)
->addCrumb(
id(new PhabricatorCrumbView())
->setName(pht('Configuration'))
->setHref($this->getApplicationURI('filter/')));
$nav->setCrumbs($crumbs);
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('Configuration'),
'device' => true,
)
);
}
private function buildConfigList(array $keys) {
$list = new PhabricatorObjectItemListView();
foreach ($keys as $key) {
$item = id(new PhabricatorObjectItemView())
->setHeader($key)
->setHref('/config/edit/'.$key)
->setObject($key);
$list->addItem($item);
}
return $list;
}
}

View file

@ -0,0 +1,32 @@
<?php
final class PhabricatorConfigEntry extends PhabricatorConfigEntryDAO {
protected $id;
protected $phid;
protected $namespace;
protected $configKey;
protected $value;
// TODO: Remove this default when implementing Transactions.
protected $isDeleted = 0;
public function getURI() {
return '/config/edit/'.$this->configKey;
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'value' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_CONF);
}
}

View file

@ -0,0 +1,9 @@
<?php
abstract class PhabricatorConfigEntryDAO extends PhabricatorLiskDAO {
public function getApplicationName() {
return 'config';
}
}

View file

@ -30,6 +30,7 @@ final class PhabricatorPHIDConstants {
const PHID_TYPE_ANSW = 'ANSW';
const PHID_TYPE_MOCK = 'MOCK';
const PHID_TYPE_MCRO = 'MCRO';
const PHID_TYPE_CONF = 'CONF';
const PHID_TYPE_XACT = 'XACT';
const PHID_TYPE_XCMT = 'XCMT';

View file

@ -77,6 +77,15 @@ final class PhabricatorObjectHandleData {
$objects[$task->getPHID()] = $task;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_CONF:
$config_dao = new PhabricatorConfigEntry();
$entries = $config_dao->loadAllWhere(
'phid IN (%Ls)',
$phids);
foreach ($entries as $entry) {
$objects[$entry->getPHID()] = $entry;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_DREV:
$revision_dao = new DifferentialRevision();
$revisions = $revision_dao->loadAllWhere(
@ -366,6 +375,28 @@ final class PhabricatorObjectHandleData {
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_CONF:
$object = new PhabricatorConfigEntry();
$entries = $object->loadAllWhere('phid in (%Ls)', $phids);
$entries = mpull($entries, null, 'getPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
$handle->setType($type);
if (empty($entries[$phid])) {
$handle->setName('Unknown Config Entry');
} else {
$entry = $entry[$phid];
$handle->setName($entry->getKey());
$handle->setURI('/config/edit/'.$entry->getKey());
$handle->setFullName($entry->getKey());
$handle->setComplete(true);
}
$handles[$phid] = $handle;
}
break;
case PhabricatorPHIDConstants::PHID_TYPE_FILE:
$object = new PhabricatorFile();

View file

@ -1064,6 +1064,14 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql',
'name' => $this->getPatchPath('20121220.generalcache.sql'),
),
'db.config' => array(
'type' => 'db',
'name' => 'config',
),
'20121226.config.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('20121226.config.sql'),
),
);
}