From a774620042c10a6cbd5aafdbb8c0bdb48e3f9d97 Mon Sep 17 00:00:00 2001 From: Ricky Elrod Date: Thu, 27 Dec 2012 15:20:09 -0800 Subject: [PATCH] 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 --- resources/sql/patches/20121226.config.sql | 12 ++ src/__phutil_library_map__.php | 12 ++ .../PhabricatorApplicationConfig.php | 34 +++++ .../PhabricatorConfigController.php | 27 ++++ .../PhabricatorConfigEditController.php | 124 ++++++++++++++++++ .../PhabricatorConfigListController.php | 62 +++++++++ .../config/storage/PhabricatorConfigEntry.php | 32 +++++ .../storage/PhabricatorConfigEntryDAO.php | 9 ++ .../phid/PhabricatorPHIDConstants.php | 1 + .../handle/PhabricatorObjectHandleData.php | 31 +++++ .../patch/PhabricatorBuiltinPatchList.php | 8 ++ 11 files changed, 352 insertions(+) create mode 100644 resources/sql/patches/20121226.config.sql create mode 100644 src/applications/config/application/PhabricatorApplicationConfig.php create mode 100644 src/applications/config/controller/PhabricatorConfigController.php create mode 100644 src/applications/config/controller/PhabricatorConfigEditController.php create mode 100644 src/applications/config/controller/PhabricatorConfigListController.php create mode 100644 src/applications/config/storage/PhabricatorConfigEntry.php create mode 100644 src/applications/config/storage/PhabricatorConfigEntryDAO.php diff --git a/resources/sql/patches/20121226.config.sql b/resources/sql/patches/20121226.config.sql new file mode 100644 index 0000000000..fcd6870df9 --- /dev/null +++ b/resources/sql/patches/20121226.config.sql @@ -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; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0f92af9106..8d816451cf 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -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', diff --git a/src/applications/config/application/PhabricatorApplicationConfig.php b/src/applications/config/application/PhabricatorApplicationConfig.php new file mode 100644 index 0000000000..88e78dd3c7 --- /dev/null +++ b/src/applications/config/application/PhabricatorApplicationConfig.php @@ -0,0 +1,34 @@ + array( + '' => 'PhabricatorConfigListController', + 'edit/(?P[\w\.\-]+)/' => 'PhabricatorConfigEditController', + ), + ); + } + +} diff --git a/src/applications/config/controller/PhabricatorConfigController.php b/src/applications/config/controller/PhabricatorConfigController.php new file mode 100644 index 0000000000..a94cb7fe4c --- /dev/null +++ b/src/applications/config/controller/PhabricatorConfigController.php @@ -0,0 +1,27 @@ +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; + } + +} diff --git a/src/applications/config/controller/PhabricatorConfigEditController.php b/src/applications/config/controller/PhabricatorConfigEditController.php new file mode 100644 index 0000000000..3960c44408 --- /dev/null +++ b/src/applications/config/controller/PhabricatorConfigEditController.php @@ -0,0 +1,124 @@ +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, + )); + } + +} diff --git a/src/applications/config/controller/PhabricatorConfigListController.php b/src/applications/config/controller/PhabricatorConfigListController.php new file mode 100644 index 0000000000..d885436dd2 --- /dev/null +++ b/src/applications/config/controller/PhabricatorConfigListController.php @@ -0,0 +1,62 @@ +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; + } + +} diff --git a/src/applications/config/storage/PhabricatorConfigEntry.php b/src/applications/config/storage/PhabricatorConfigEntry.php new file mode 100644 index 0000000000..c94a34dd50 --- /dev/null +++ b/src/applications/config/storage/PhabricatorConfigEntry.php @@ -0,0 +1,32 @@ +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); + } + +} diff --git a/src/applications/config/storage/PhabricatorConfigEntryDAO.php b/src/applications/config/storage/PhabricatorConfigEntryDAO.php new file mode 100644 index 0000000000..f5359567b3 --- /dev/null +++ b/src/applications/config/storage/PhabricatorConfigEntryDAO.php @@ -0,0 +1,9 @@ +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(); diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index 458cbea77f..3c38dc6514 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -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'), + ), ); } -- 2.51.2