From 42566379dc3c4fd01a73067215da4a7ca18f9c17 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 20 Feb 2016 11:51:33 -0800 Subject: [PATCH 01/31] Fix HTTP body decompression in PHP 5.6 Summary: Ref T10264. Under PHP 5.6, you are no longer allowed to use `compress.zlib://php://input` as an argument to either `fopen()` or `file_get_contents()`. Instead, open `php://input` as a file handle, then add `zlib.inflate` as a stream wrapper. This requires some level of magic to work properly. Test Plan: First, I constructed a synthetic gzipped payload by typing some words into a file and using `gzcompress()` to compress it. Then I used a `curl` command like this to make requests with it: ``` $ curl -X POST -H "Content-Length: 66" -H "Content-Type: text/plain" -H "Content-Encoding: gzip" --data-binary @payload.deflate -v http://127.0.0.1/ ``` I modified Phabricator to just dump the raw request body and exit, and reproduced the issue under PHP 5.6 (no body, error in log) by brining up a micro instance in EC2 and installing php56 on it. After this patch, it dumped the body properly instead, and PHP 5.5 also continued worked properly. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10264 Differential Revision: https://secure.phabricator.com/D15314 --- support/PhabricatorStartup.php | 37 +++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/support/PhabricatorStartup.php b/support/PhabricatorStartup.php index bc7daf4ea6..5174767e28 100644 --- a/support/PhabricatorStartup.php +++ b/support/PhabricatorStartup.php @@ -135,13 +135,40 @@ final class PhabricatorStartup { $encoding = null; } - if ($encoding === 'gzip') { - $source_stream = 'compress.zlib://php://input'; - } else { - $source_stream = 'php://input'; + $input_stream = fopen('php://input', 'rb'); + if (!$input_stream) { + self::didFatal( + 'Unable to open "php://input" to read HTTP request body.'); } - self::$rawInput = (string)file_get_contents($source_stream); + if ($encoding === 'gzip') { + $ok = stream_filter_append( + $input_stream, + 'zlib.inflate', + STREAM_FILTER_READ, + array( + 'window' => 30, + )); + + if (!$ok) { + self::didFatal( + 'Failed to append gzip inflate filter to HTTP request body input '. + 'stream.'); + } + } + + $input_data = ''; + while (!feof($input_stream)) { + $read_bytes = fread($input_stream, 16 * 1024); + if ($read_bytes === false) { + self::didFatal( + 'Failed to read input bytes from HTTP request body.'); + } + $input_data .= $read_bytes; + } + fclose($input_stream); + + self::$rawInput = $input_data; } From a4db6f387da251e29f69e3e711e40255488bedbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Santoro?= Date: Sun, 21 Feb 2016 01:51:03 -0800 Subject: [PATCH 02/31] =?UTF-8?q?Fix=20typo:=20discsussions=20=E2=86=92=20?= =?UTF-8?q?discussions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test Plan: Read again the sentence. Reviewers: joshuaspence, #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: epriestley Differential Revision: https://secure.phabricator.com/D15316 --- .../daemon/bot/handler/PhabricatorBotObjectNameHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php index 804e5b27bb..64f7665827 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php @@ -173,7 +173,7 @@ final class PhabricatorBotObjectNameHandler extends PhabricatorBotHandler { // Don't mention the same object more than once every 10 minutes // in public channels, so we avoid spamming the chat over and over - // again for discsussions of a specific revision, for example. + // again for discussions of a specific revision, for example. $target_name = $original_message->getTarget()->getName(); if (empty($this->recentlyMentioned[$target_name])) { From 1b6ddae6b2d8a28c45575dddd2e39b26ba055981 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 21 Feb 2016 02:48:10 -0800 Subject: [PATCH 03/31] Allow Almanac devices to be queried and sorted by name Summary: Ref T10205. Ref T10246. This is general modernization, but also supports fixing the interface datasource in T10205. - Update Query. - Update SearchEngine. - Use an ngrams index for searching names efficiently. Test Plan: - Ran migrations. - Searched Almanac devices by name. - Created a new device, searched for it by name. {F1121303} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10205, T10246 Differential Revision: https://secure.phabricator.com/D15319 --- .../20160221.almanac.1.devicen.sql | 7 ++ .../20160221.almanac.2.devicei.php | 11 +++ src/__phutil_library_map__.php | 3 + .../customfield/AlmanacCoreCustomField.php | 3 + .../almanac/editor/AlmanacDeviceEditor.php | 6 +- .../almanac/query/AlmanacDeviceQuery.php | 84 +++++++++++++------ .../query/AlmanacDeviceSearchEngine.php | 33 ++++---- .../almanac/storage/AlmanacDevice.php | 14 +++- .../storage/AlmanacDeviceNameNgrams.php | 18 ++++ 9 files changed, 133 insertions(+), 46 deletions(-) create mode 100644 resources/sql/autopatches/20160221.almanac.1.devicen.sql create mode 100644 resources/sql/autopatches/20160221.almanac.2.devicei.php create mode 100644 src/applications/almanac/storage/AlmanacDeviceNameNgrams.php diff --git a/resources/sql/autopatches/20160221.almanac.1.devicen.sql b/resources/sql/autopatches/20160221.almanac.1.devicen.sql new file mode 100644 index 0000000000..c098173f25 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.1.devicen.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_devicename_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.2.devicei.php b/resources/sql/autopatches/20160221.almanac.2.devicei.php new file mode 100644 index 0000000000..aea17d0ad6 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.2.devicei.php @@ -0,0 +1,11 @@ +getPHID(), + array( + 'force' => true, + )); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ae699acf07..2a175303fe 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -38,6 +38,7 @@ phutil_register_library_map(array( 'AlmanacDeviceEditController' => 'applications/almanac/controller/AlmanacDeviceEditController.php', 'AlmanacDeviceEditor' => 'applications/almanac/editor/AlmanacDeviceEditor.php', 'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php', + 'AlmanacDeviceNameNgrams' => 'applications/almanac/storage/AlmanacDeviceNameNgrams.php', 'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php', 'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php', 'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php', @@ -4011,11 +4012,13 @@ phutil_register_library_map(array( 'PhabricatorSSHPublicKeyInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', ), 'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', 'AlmanacDeviceEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacDeviceListController' => 'AlmanacDeviceController', + 'AlmanacDeviceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacDevicePHIDType' => 'PhabricatorPHIDType', 'AlmanacDeviceQuery' => 'AlmanacQuery', 'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/almanac/customfield/AlmanacCoreCustomField.php b/src/applications/almanac/customfield/AlmanacCoreCustomField.php index c1df52321f..91560f14e6 100644 --- a/src/applications/almanac/customfield/AlmanacCoreCustomField.php +++ b/src/applications/almanac/customfield/AlmanacCoreCustomField.php @@ -17,6 +17,9 @@ final class AlmanacCoreCustomField } public function createFields($object) { + if (!$object->getID()) { + return array(); + } $specs = $object->getAlmanacPropertyFieldSpecifications(); diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php index 3f7817bccc..25d49fb295 100644 --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -11,6 +11,10 @@ final class AlmanacDeviceEditor return pht('Almanac Device'); } + protected function supportsSearch() { + return true; + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); @@ -303,6 +307,4 @@ final class AlmanacDeviceEditor return $errors; } - - } diff --git a/src/applications/almanac/query/AlmanacDeviceQuery.php b/src/applications/almanac/query/AlmanacDeviceQuery.php index 287af6c647..29461bfbfd 100644 --- a/src/applications/almanac/query/AlmanacDeviceQuery.php +++ b/src/applications/almanac/query/AlmanacDeviceQuery.php @@ -34,35 +34,34 @@ final class AlmanacDeviceQuery return $this; } - protected function loadPage() { - $table = new AlmanacDevice(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new AlmanacDeviceNameNgrams(), + $ngrams); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + public function newResultObject() { + return new AlmanacDevice(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, - 'id IN (%Ld)', + $conn, + 'device.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, - 'phid IN (%Ls)', + $conn, + 'device.phid IN (%Ls)', $this->phids); } @@ -72,28 +71,59 @@ final class AlmanacDeviceQuery $hashes[] = PhabricatorHash::digestForIndex($name); } $where[] = qsprintf( - $conn_r, - 'nameIndex IN (%Ls)', + $conn, + 'device.nameIndex IN (%Ls)', $hashes); } if ($this->namePrefix !== null) { $where[] = qsprintf( - $conn_r, - 'name LIKE %>', + $conn, + 'device.name LIKE %>', $this->namePrefix); } if ($this->nameSuffix !== null) { $where[] = qsprintf( - $conn_r, - 'name LIKE %<', + $conn, + 'device.name LIKE %<', $this->nameSuffix); } - $where[] = $this->buildPagingClause($conn_r); + return $where; + } - return $this->formatWhereClause($where); + protected function getPrimaryTableAlias() { + return 'device'; + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'name' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'name', + 'type' => 'string', + 'unique' => true, + 'reverse' => true, + ), + ); + } + + protected function getPagingValueMap($cursor, array $keys) { + $device = $this->loadCursorObject($cursor); + return array( + 'id' => $device->getID(), + 'name' => $device->getName(), + ); + } + + public function getBuiltinOrders() { + return array( + 'name' => array( + 'vector' => array('name'), + 'name' => pht('Device Name'), + ), + ) + parent::getBuiltinOrders(); } public function getQueryApplicationClass() { diff --git a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php index 99e960425c..d9e13fba33 100644 --- a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php @@ -11,22 +11,29 @@ final class AlmanacDeviceSearchEngine return 'PhabricatorAlmanacApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - return $saved; + public function newQuery() { + return new AlmanacDeviceQuery(); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new AlmanacDeviceQuery()); + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for devices by name substring.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) {} - protected function getURI($path) { return '/almanac/device/'.$path; } @@ -52,12 +59,6 @@ final class AlmanacDeviceSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $devices, - PhabricatorSavedQuery $query) { - return array(); - } - protected function renderResultList( array $devices, PhabricatorSavedQuery $query, diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index d0994174b3..09503a31b6 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -9,7 +9,8 @@ final class AlmanacDevice PhabricatorProjectInterface, PhabricatorSSHPublicKeyInterface, AlmanacPropertyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorNgramsInterface { protected $name; protected $nameIndex; @@ -250,4 +251,15 @@ final class AlmanacDevice $this->delete(); } + +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + public function newNgrams() { + return array( + id(new AlmanacDeviceNameNgrams()) + ->setValue($this->getName()), + ); + } + } diff --git a/src/applications/almanac/storage/AlmanacDeviceNameNgrams.php b/src/applications/almanac/storage/AlmanacDeviceNameNgrams.php new file mode 100644 index 0000000000..a0ca1e2810 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacDeviceNameNgrams.php @@ -0,0 +1,18 @@ + Date: Sun, 21 Feb 2016 03:12:06 -0800 Subject: [PATCH 04/31] Allow Almanac interfaces to be browsed Summary: Fixes T10205. Ref T10246. Previously, the issue was that the result set was not ordered, so "More Results" would not have been able to work in a reasonable way if there were more than 100 matching interfaces. You would have seen 100 interfaces more or less at random, then clicked "more" and gotten 100 more random interfaces. Now, you would see 100 "a" interfaces, then click more to get the next 100 alphabetical interfaces (say, "b" and "c" interfaces). Test Plan: - Clicked browse when binding an interface. - Got a browse dialog. - Artificially set query limit to 1, paged through "local" interfaces in an ordered, consistent way. {F1121313} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10205, T10246 Differential Revision: https://secure.phabricator.com/D15320 --- .../almanac/query/AlmanacInterfaceQuery.php | 111 ++++++++++++++---- .../typeahead/AlmanacInterfaceDatasource.php | 12 +- ...orTypeaheadModularDatasourceController.php | 2 + 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/src/applications/almanac/query/AlmanacInterfaceQuery.php b/src/applications/almanac/query/AlmanacInterfaceQuery.php index e119203da5..353ce34a49 100644 --- a/src/applications/almanac/query/AlmanacInterfaceQuery.php +++ b/src/applications/almanac/query/AlmanacInterfaceQuery.php @@ -34,19 +34,12 @@ final class AlmanacInterfaceQuery return $this; } + public function newResultObject() { + return new AlmanacInterface(); + } + protected function loadPage() { - $table = new AlmanacInterface(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $interfaces) { @@ -83,34 +76,34 @@ final class AlmanacInterfaceQuery return $interfaces; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, - 'id IN (%Ld)', + $conn, + 'interface.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, - 'phid IN (%Ls)', + $conn, + 'interface.phid IN (%Ls)', $this->phids); } if ($this->networkPHIDs !== null) { $where[] = qsprintf( - $conn_r, - 'networkPHID IN (%Ls)', + $conn, + 'interface.networkPHID IN (%Ls)', $this->networkPHIDs); } if ($this->devicePHIDs !== null) { $where[] = qsprintf( - $conn_r, - 'devicePHID IN (%Ls)', + $conn, + 'interface.devicePHID IN (%Ls)', $this->devicePHIDs); } @@ -118,8 +111,10 @@ final class AlmanacInterfaceQuery $parts = array(); foreach ($this->addresses as $address) { $parts[] = qsprintf( - $conn_r, - '(networkPHID = %s AND address = %s AND port = %d)', + $conn, + '(interface.networkPHID = %s '. + 'AND interface.address = %s '. + 'AND interface.port = %d)', $address->getNetworkPHID(), $address->getAddress(), $address->getPort()); @@ -127,13 +122,77 @@ final class AlmanacInterfaceQuery $where[] = implode(' OR ', $parts); } - $where[] = $this->buildPagingClause($conn_r); + return $where; + } - return $this->formatWhereClause($where); + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); + + if ($this->shouldJoinDeviceTable()) { + $joins[] = qsprintf( + $conn, + 'JOIN %T device ON device.phid = interface.devicePHID', + id(new AlmanacDevice())->getTableName()); + } + + return $joins; + } + + protected function shouldGroupQueryResultRows() { + if ($this->shouldJoinDeviceTable()) { + return true; + } + + return parent::shouldGroupQueryResultRows(); + } + + private function shouldJoinDeviceTable() { + $vector = $this->getOrderVector(); + + if ($vector->containsKey('name')) { + return true; + } + + return false; + } + + protected function getPrimaryTableAlias() { + return 'interface'; } public function getQueryApplicationClass() { return 'PhabricatorAlmanacApplication'; } + public function getBuiltinOrders() { + return array( + 'name' => array( + 'vector' => array('name', 'id'), + 'name' => pht('Device Name'), + ), + ) + parent::getBuiltinOrders(); + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'name' => array( + 'table' => 'device', + 'column' => 'name', + 'type' => 'string', + 'reverse' => true, + ), + ); + } + + protected function getPagingValueMap($cursor, array $keys) { + $interface = $this->loadCursorObject($cursor); + + $map = array( + 'id' => $interface->getID(), + 'name' => $interface->getDevice()->getName(), + ); + + return $map; + } + } diff --git a/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php index a5f46ddfb0..15e50f5993 100644 --- a/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php +++ b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php @@ -3,12 +3,6 @@ final class AlmanacInterfaceDatasource extends PhabricatorTypeaheadDatasource { - public function isBrowsable() { - // TODO: We should make this browsable, but need to make the result set - // orderable by device name. - return false; - } - public function getBrowseTitle() { return pht('Browse Interfaces'); } @@ -31,10 +25,12 @@ final class AlmanacInterfaceDatasource ->execute(); if ($devices) { - $interfaces = id(new AlmanacInterfaceQuery()) + $interface_query = id(new AlmanacInterfaceQuery()) ->setViewer($viewer) ->withDevicePHIDs(mpull($devices, 'getPHID')) - ->execute(); + ->setOrder('name'); + + $interfaces = $this->executeQuery($interface_query); } else { $interfaces = array(); } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index c20999601e..b3687b85e8 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -107,6 +107,8 @@ final class PhabricatorTypeaheadModularDatasourceController if (($offset + (2 * $limit)) < $hard_limit) { $next_uri = id(new PhutilURI($request->getRequestURI())) ->setQueryParam('offset', $offset + $limit) + ->setQueryParam('q', $query) + ->setQueryParam('raw', $raw_query) ->setQueryParam('format', 'html'); $next_link = javelin_tag( From 31927476e31c7cf9c0c2ea622c3cebb03ceb489d Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Sun, 21 Feb 2016 12:16:04 -0800 Subject: [PATCH 05/31] Reorder audit actions to match Differential Summary: These trip me up every time because Differential has: > Comment, Accept, Request Changes, Resign, Commandeer, Add Reviewers, Add Subscribers while audits currently show: > Comment, Add Subscribers, Add Auditors, Accept, Raise Concern, Resign Now they're more or less in the same order which helps with muscle memory. Test Plan: Careful inspection. Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15323 --- .../diffusion/controller/DiffusionCommitController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index 9d6b845c91..6736b99430 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -790,8 +790,6 @@ final class DiffusionCommitController extends DiffusionController { $actions = array(); $actions[PhabricatorAuditActionConstants::COMMENT] = true; - $actions[PhabricatorAuditActionConstants::ADD_CCS] = true; - $actions[PhabricatorAuditActionConstants::ADD_AUDITORS] = true; // We allow you to accept your own commits. A use case here is that you // notice an issue with your own commit and "Raise Concern" as an indicator @@ -801,7 +799,6 @@ final class DiffusionCommitController extends DiffusionController { $actions[PhabricatorAuditActionConstants::ACCEPT] = true; $actions[PhabricatorAuditActionConstants::CONCERN] = true; - // To resign, a user must have authority on some request and not be the // commit's author. if (!$user_is_author) { @@ -837,6 +834,9 @@ final class DiffusionCommitController extends DiffusionController { $actions[PhabricatorAuditActionConstants::CLOSE] = true; } + $actions[PhabricatorAuditActionConstants::ADD_AUDITORS] = true; + $actions[PhabricatorAuditActionConstants::ADD_CCS] = true; + foreach ($actions as $constant => $ignored) { $actions[$constant] = PhabricatorAuditActionConstants::getActionName($constant); From 959bb16d0f6780c2691fd2ea7d2e724143ea8425 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 21 Feb 2016 10:52:19 -0800 Subject: [PATCH 06/31] Allow Almanac services to be searched by substring Summary: Ref T10246. Build an ngram index for Almanac services, and use it to support improved search. Test Plan: {F1121725} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10246 Differential Revision: https://secure.phabricator.com/D15321 --- .../20160221.almanac.3.servicen.sql | 7 +++++++ .../20160221.almanac.4.servicei.php | 11 ++++++++++ src/__phutil_library_map__.php | 3 +++ .../almanac/editor/AlmanacServiceEditor.php | 4 ++++ .../almanac/query/AlmanacServiceQuery.php | 20 ++++++++++++++++++- .../query/AlmanacServiceSearchEngine.php | 16 ++++++++++----- .../almanac/storage/AlmanacService.php | 14 ++++++++++++- .../storage/AlmanacServiceNameNgrams.php | 18 +++++++++++++++++ 8 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 resources/sql/autopatches/20160221.almanac.3.servicen.sql create mode 100644 resources/sql/autopatches/20160221.almanac.4.servicei.php create mode 100644 src/applications/almanac/storage/AlmanacServiceNameNgrams.php diff --git a/resources/sql/autopatches/20160221.almanac.3.servicen.sql b/resources/sql/autopatches/20160221.almanac.3.servicen.sql new file mode 100644 index 0000000000..2e48cd60c8 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.3.servicen.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_servicename_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.4.servicei.php b/resources/sql/autopatches/20160221.almanac.4.servicei.php new file mode 100644 index 0000000000..97211ca7b5 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.4.servicei.php @@ -0,0 +1,11 @@ +getPHID(), + array( + 'force' => true, + )); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2a175303fe..7488ddcff6 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -89,6 +89,7 @@ phutil_register_library_map(array( 'AlmanacServiceEditController' => 'applications/almanac/controller/AlmanacServiceEditController.php', 'AlmanacServiceEditor' => 'applications/almanac/editor/AlmanacServiceEditor.php', 'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php', + 'AlmanacServiceNameNgrams' => 'applications/almanac/storage/AlmanacServiceNameNgrams.php', 'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php', 'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php', 'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php', @@ -4082,12 +4083,14 @@ phutil_register_library_map(array( 'PhabricatorProjectInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', ), 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacServiceEditController' => 'AlmanacServiceController', 'AlmanacServiceEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacServiceListController' => 'AlmanacServiceController', + 'AlmanacServiceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacServicePHIDType' => 'PhabricatorPHIDType', 'AlmanacServiceQuery' => 'AlmanacQuery', 'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php index 868f31d026..178621fee0 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -11,6 +11,10 @@ final class AlmanacServiceEditor return pht('Almanac Service'); } + protected function supportsSearch() { + return true; + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php index 3701e850e9..f1e6630736 100644 --- a/src/applications/almanac/query/AlmanacServiceQuery.php +++ b/src/applications/almanac/query/AlmanacServiceQuery.php @@ -54,6 +54,12 @@ final class AlmanacServiceQuery return $this; } + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new AlmanacServiceNameNgrams(), + $ngrams); + } + public function needBindings($need_bindings) { $this->needBindings = $need_bindings; return $this; @@ -66,7 +72,7 @@ final class AlmanacServiceQuery protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); - if ($this->devicePHIDs !== null) { + if ($this->shouldJoinBindingTable()) { $joins[] = qsprintf( $conn, 'JOIN %T binding ON service.phid = binding.servicePHID', @@ -178,6 +184,18 @@ final class AlmanacServiceQuery return parent::didFilterPage($services); } + private function shouldJoinBindingTable() { + return ($this->devicePHIDs !== null); + } + + protected function shouldGroupQueryResultRows() { + if ($this->shouldJoinBindingTable()) { + return true; + } + + return parent::shouldGroupQueryResultRows(); + } + protected function getPrimaryTableAlias() { return 'service'; } diff --git a/src/applications/almanac/query/AlmanacServiceSearchEngine.php b/src/applications/almanac/query/AlmanacServiceSearchEngine.php index 51d97729fe..2f7d6cf58f 100644 --- a/src/applications/almanac/query/AlmanacServiceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacServiceSearchEngine.php @@ -16,21 +16,27 @@ final class AlmanacServiceSearchEngine } public function newResultObject() { - // NOTE: We need to attach a service type in order to generate custom - // field definitions. - return AlmanacService::initializeNewService() - ->attachServiceType(new AlmanacCustomServiceType()); + return AlmanacService::initializeNewService(); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } + return $query; } protected function buildCustomSearchFields() { - return array(); + return array( + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for services by name substring.')), + ); } protected function getURI($path) { diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index b351f0d3fc..c16f85c8ee 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -8,7 +8,8 @@ final class AlmanacService PhabricatorApplicationTransactionInterface, PhabricatorProjectInterface, AlmanacPropertyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorNgramsInterface { protected $name; protected $nameIndex; @@ -231,4 +232,15 @@ final class AlmanacService $this->delete(); } + +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + public function newNgrams() { + return array( + id(new AlmanacServiceNameNgrams()) + ->setValue($this->getName()), + ); + } + } diff --git a/src/applications/almanac/storage/AlmanacServiceNameNgrams.php b/src/applications/almanac/storage/AlmanacServiceNameNgrams.php new file mode 100644 index 0000000000..4ad6a9639d --- /dev/null +++ b/src/applications/almanac/storage/AlmanacServiceNameNgrams.php @@ -0,0 +1,18 @@ + Date: Sun, 21 Feb 2016 11:13:08 -0800 Subject: [PATCH 07/31] Allow Almanac namespaces to be searched by ngram index Summary: Ref T6741. Ref T10246. This is largely modernization, but will partially support namespace locking in Almanac. Test Plan: Searched for Almanac networks by name substring. {F1121740} Reviewers: chad Reviewed By: chad Maniphest Tasks: T6741, T10246 Differential Revision: https://secure.phabricator.com/D15322 --- .../20160221.almanac.5.networkn.sql | 7 +++ .../20160221.almanac.6.networki.php | 11 +++++ src/__phutil_library_map__.php | 3 ++ .../almanac/editor/AlmanacNetworkEditor.php | 4 ++ .../almanac/query/AlmanacNetworkQuery.php | 43 ++++++++++--------- .../query/AlmanacNetworkSearchEngine.php | 33 +++++++------- .../almanac/storage/AlmanacNetwork.php | 14 +++++- .../storage/AlmanacNetworkNameNgrams.php | 18 ++++++++ 8 files changed, 95 insertions(+), 38 deletions(-) create mode 100644 resources/sql/autopatches/20160221.almanac.5.networkn.sql create mode 100644 resources/sql/autopatches/20160221.almanac.6.networki.php create mode 100644 src/applications/almanac/storage/AlmanacNetworkNameNgrams.php diff --git a/resources/sql/autopatches/20160221.almanac.5.networkn.sql b/resources/sql/autopatches/20160221.almanac.5.networkn.sql new file mode 100644 index 0000000000..c47db69d5d --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.5.networkn.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_networkname_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.6.networki.php b/resources/sql/autopatches/20160221.almanac.6.networki.php new file mode 100644 index 0000000000..263defbb33 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.6.networki.php @@ -0,0 +1,11 @@ +getPHID(), + array( + 'force' => true, + )); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7488ddcff6..2dde62fdd0 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -66,6 +66,7 @@ phutil_register_library_map(array( 'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php', 'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php', 'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php', + 'AlmanacNetworkNameNgrams' => 'applications/almanac/storage/AlmanacNetworkNameNgrams.php', 'AlmanacNetworkPHIDType' => 'applications/almanac/phid/AlmanacNetworkPHIDType.php', 'AlmanacNetworkQuery' => 'applications/almanac/query/AlmanacNetworkQuery.php', 'AlmanacNetworkSearchEngine' => 'applications/almanac/query/AlmanacNetworkSearchEngine.php', @@ -4051,11 +4052,13 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', ), 'AlmanacNetworkController' => 'AlmanacController', 'AlmanacNetworkEditController' => 'AlmanacNetworkController', 'AlmanacNetworkEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacNetworkListController' => 'AlmanacNetworkController', + 'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType', 'AlmanacNetworkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/almanac/editor/AlmanacNetworkEditor.php b/src/applications/almanac/editor/AlmanacNetworkEditor.php index 4e49cce163..f32b58219e 100644 --- a/src/applications/almanac/editor/AlmanacNetworkEditor.php +++ b/src/applications/almanac/editor/AlmanacNetworkEditor.php @@ -11,6 +11,10 @@ final class AlmanacNetworkEditor return pht('Almanac Network'); } + protected function supportsSearch() { + return true; + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); diff --git a/src/applications/almanac/query/AlmanacNetworkQuery.php b/src/applications/almanac/query/AlmanacNetworkQuery.php index d33def72ea..a1391ff916 100644 --- a/src/applications/almanac/query/AlmanacNetworkQuery.php +++ b/src/applications/almanac/query/AlmanacNetworkQuery.php @@ -16,41 +16,42 @@ final class AlmanacNetworkQuery return $this; } - protected function loadPage() { - $table = new AlmanacNetwork(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + public function newResultObject() { + return new AlmanacNetwork(); } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new AlmanacNetworkNameNgrams(), + $ngrams); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, - 'id IN (%Ld)', + $conn, + 'network.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, - 'phid IN (%Ls)', + $conn, + 'network.phid IN (%Ls)', $this->phids); } - $where[] = $this->buildPagingClause($conn_r); + return $where; + } - return $this->formatWhereClause($where); + protected function getPrimaryTableAlias() { + return 'network'; } public function getQueryApplicationClass() { diff --git a/src/applications/almanac/query/AlmanacNetworkSearchEngine.php b/src/applications/almanac/query/AlmanacNetworkSearchEngine.php index 410e984a3f..a34e90ba64 100644 --- a/src/applications/almanac/query/AlmanacNetworkSearchEngine.php +++ b/src/applications/almanac/query/AlmanacNetworkSearchEngine.php @@ -11,22 +11,29 @@ final class AlmanacNetworkSearchEngine return 'PhabricatorAlmanacApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - return $saved; + public function newQuery() { + return new AlmanacNetworkQuery(); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new AlmanacNetworkQuery()); + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for devices by name substring.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) {} - protected function getURI($path) { return '/almanac/network/'.$path; } @@ -52,12 +59,6 @@ final class AlmanacNetworkSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $networks, - PhabricatorSavedQuery $query) { - return array(); - } - protected function renderResultList( array $networks, PhabricatorSavedQuery $query, diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index a623248fd9..064b612999 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -5,7 +5,8 @@ final class AlmanacNetwork implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorNgramsInterface { protected $name; protected $mailKey; @@ -114,4 +115,15 @@ final class AlmanacNetwork $this->delete(); } + +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + public function newNgrams() { + return array( + id(new AlmanacNetworkNameNgrams()) + ->setValue($this->getName()), + ); + } + } diff --git a/src/applications/almanac/storage/AlmanacNetworkNameNgrams.php b/src/applications/almanac/storage/AlmanacNetworkNameNgrams.php new file mode 100644 index 0000000000..639bc869a7 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacNetworkNameNgrams.php @@ -0,0 +1,18 @@ + Date: Sun, 21 Feb 2016 12:15:57 -0800 Subject: [PATCH 08/31] Rough-in Almanac namespaces Summary: Ref T6741. Ref T10246. Root problem: to provide Drydock in the cluster, we need to expose Almanac, and doing so would let users accidentally or intentionally create a bunch of `repo006.phacility.net` devices/services which could conflict with the real ones we manage. There's currently no way to say "you can't create anything named `*.blah.net`". This adds "namespaces", which let you do that (well, not yet, but they will after the next diff). After the next diff, if you try to create `repo003.phacility.net`, but the namespace `phacility.net` already exists and you don't have permission to edit it, you'll be asked to choose a different name. Also various modernizations and some new docs. Test Plan: - Created cool namespaces like `this.computer`. - Almanac namespaces don't actually enforce policies yet. Reviewers: chad Reviewed By: chad Maniphest Tasks: T6741, T10246 Differential Revision: https://secure.phabricator.com/D15324 --- .../20160221.almanac.7.namespacen.sql | 7 + .../20160221.almanac.8.namespace.sql | 14 ++ .../20160221.almanac.9.namespacex.sql | 19 ++ src/__phutil_library_map__.php | 37 ++++ .../PhabricatorAlmanacApplication.php | 17 +- .../AlmanacCreateNamespacesCapability.php | 16 ++ .../controller/AlmanacConsoleController.php | 44 +++- .../controller/AlmanacNamespaceController.php | 14 ++ .../AlmanacNamespaceEditController.php | 11 + .../AlmanacNamespaceListController.php | 26 +++ .../AlmanacNamespaceViewController.php | 87 ++++++++ .../AlmanacPropertyEditController.php | 4 +- .../almanac/editor/AlmanacDeviceEditor.php | 2 +- .../editor/AlmanacNamespaceEditEngine.php | 86 ++++++++ .../almanac/editor/AlmanacNamespaceEditor.php | 162 ++++++++++++++ .../almanac/editor/AlmanacServiceEditor.php | 2 +- .../almanac/phid/AlmanacNamespacePHIDType.php | 44 ++++ .../almanac/query/AlmanacNamespaceQuery.php | 103 +++++++++ .../query/AlmanacNamespaceSearchEngine.php | 90 ++++++++ .../AlmanacNamespaceTransactionQuery.php | 10 + .../query/AlmanacNetworkSearchEngine.php | 2 +- .../almanac/storage/AlmanacDevice.php | 2 +- .../almanac/storage/AlmanacNamespace.php | 197 ++++++++++++++++++ .../storage/AlmanacNamespaceNameNgrams.php | 18 ++ .../storage/AlmanacNamespaceTransaction.php | 43 ++++ .../almanac/storage/AlmanacService.php | 2 +- .../almanac/util/AlmanacNames.php | 37 ++-- .../util/__tests__/AlmanacNamesTestCase.php | 6 +- src/docs/user/userguide/almanac.diviner | 133 +++++++++++- 29 files changed, 1195 insertions(+), 40 deletions(-) create mode 100644 resources/sql/autopatches/20160221.almanac.7.namespacen.sql create mode 100644 resources/sql/autopatches/20160221.almanac.8.namespace.sql create mode 100644 resources/sql/autopatches/20160221.almanac.9.namespacex.sql create mode 100644 src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php create mode 100644 src/applications/almanac/controller/AlmanacNamespaceController.php create mode 100644 src/applications/almanac/controller/AlmanacNamespaceEditController.php create mode 100644 src/applications/almanac/controller/AlmanacNamespaceListController.php create mode 100644 src/applications/almanac/controller/AlmanacNamespaceViewController.php create mode 100644 src/applications/almanac/editor/AlmanacNamespaceEditEngine.php create mode 100644 src/applications/almanac/editor/AlmanacNamespaceEditor.php create mode 100644 src/applications/almanac/phid/AlmanacNamespacePHIDType.php create mode 100644 src/applications/almanac/query/AlmanacNamespaceQuery.php create mode 100644 src/applications/almanac/query/AlmanacNamespaceSearchEngine.php create mode 100644 src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php create mode 100644 src/applications/almanac/storage/AlmanacNamespace.php create mode 100644 src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php create mode 100644 src/applications/almanac/storage/AlmanacNamespaceTransaction.php diff --git a/resources/sql/autopatches/20160221.almanac.7.namespacen.sql b/resources/sql/autopatches/20160221.almanac.7.namespacen.sql new file mode 100644 index 0000000000..bdacc3552c --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.7.namespacen.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_namespacename_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.8.namespace.sql b/resources/sql/autopatches/20160221.almanac.8.namespace.sql new file mode 100644 index 0000000000..90f53d3daa --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.8.namespace.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_namespace ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT}, + nameIndex BINARY(12) NOT NULL, + mailKey BINARY(20) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_nameindex` (nameIndex), + KEY `key_name` (name) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160221.almanac.9.namespacex.sql b/resources/sql/autopatches/20160221.almanac.9.namespacex.sql new file mode 100644 index 0000000000..4f695456f9 --- /dev/null +++ b/resources/sql/autopatches/20160221.almanac.9.namespacex.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_almanac.almanac_namespacetransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2dde62fdd0..92943d801d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -28,6 +28,7 @@ phutil_register_library_map(array( 'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php', 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', + 'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', 'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php', 'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php', @@ -61,6 +62,19 @@ phutil_register_library_map(array( 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', 'AlmanacNamesTestCase' => 'applications/almanac/util/__tests__/AlmanacNamesTestCase.php', + 'AlmanacNamespace' => 'applications/almanac/storage/AlmanacNamespace.php', + 'AlmanacNamespaceController' => 'applications/almanac/controller/AlmanacNamespaceController.php', + 'AlmanacNamespaceEditController' => 'applications/almanac/controller/AlmanacNamespaceEditController.php', + 'AlmanacNamespaceEditEngine' => 'applications/almanac/editor/AlmanacNamespaceEditEngine.php', + 'AlmanacNamespaceEditor' => 'applications/almanac/editor/AlmanacNamespaceEditor.php', + 'AlmanacNamespaceListController' => 'applications/almanac/controller/AlmanacNamespaceListController.php', + 'AlmanacNamespaceNameNgrams' => 'applications/almanac/storage/AlmanacNamespaceNameNgrams.php', + 'AlmanacNamespacePHIDType' => 'applications/almanac/phid/AlmanacNamespacePHIDType.php', + 'AlmanacNamespaceQuery' => 'applications/almanac/query/AlmanacNamespaceQuery.php', + 'AlmanacNamespaceSearchEngine' => 'applications/almanac/query/AlmanacNamespaceSearchEngine.php', + 'AlmanacNamespaceTransaction' => 'applications/almanac/storage/AlmanacNamespaceTransaction.php', + 'AlmanacNamespaceTransactionQuery' => 'applications/almanac/query/AlmanacNamespaceTransactionQuery.php', + 'AlmanacNamespaceViewController' => 'applications/almanac/controller/AlmanacNamespaceViewController.php', 'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php', 'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php', 'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php', @@ -4000,6 +4014,7 @@ phutil_register_library_map(array( ), 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', + 'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCustomField' => 'PhabricatorCustomField', @@ -4047,6 +4062,28 @@ phutil_register_library_map(array( 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacNames' => 'Phobject', 'AlmanacNamesTestCase' => 'PhabricatorTestCase', + 'AlmanacNamespace' => array( + 'AlmanacDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorCustomFieldInterface', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorProjectInterface', + 'AlmanacPropertyInterface', + 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', + ), + 'AlmanacNamespaceController' => 'AlmanacController', + 'AlmanacNamespaceEditController' => 'AlmanacController', + 'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine', + 'AlmanacNamespaceEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacNamespaceListController' => 'AlmanacNamespaceController', + 'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams', + 'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType', + 'AlmanacNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'AlmanacNamespaceTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'AlmanacNamespaceViewController' => 'AlmanacNamespaceController', 'AlmanacNetwork' => array( 'AlmanacDAO', 'PhabricatorApplicationTransactionInterface', diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php index a444554a0e..ebcefe4a51 100644 --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -29,7 +29,7 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { public function getHelpDocumentationArticles(PhabricatorUser $viewer) { return array( array( - 'name' => pht('Alamanac User Guide'), + 'name' => pht('Almanac User Guide'), 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), ), ); @@ -44,12 +44,12 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { '/almanac/' => array( '' => 'AlmanacConsoleController', 'service/' => array( - '(?:query/(?P[^/]+)/)?' => 'AlmanacServiceListController', + $this->getQueryRoutePattern() => 'AlmanacServiceListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacServiceEditController', 'view/(?P[^/]+)/' => 'AlmanacServiceViewController', ), 'device/' => array( - '(?:query/(?P[^/]+)/)?' => 'AlmanacDeviceListController', + $this->getQueryRoutePattern() => 'AlmanacDeviceListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacDeviceEditController', 'view/(?P[^/]+)/' => 'AlmanacDeviceViewController', ), @@ -61,7 +61,7 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { '(?P\d+)/' => 'AlmanacBindingViewController', ), 'network/' => array( - '(?:query/(?P[^/]+)/)?' => 'AlmanacNetworkListController', + $this->getQueryRoutePattern() => 'AlmanacNetworkListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacNetworkEditController', '(?P\d+)/' => 'AlmanacNetworkViewController', ), @@ -69,6 +69,12 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { 'edit/' => 'AlmanacPropertyEditController', 'delete/' => 'AlmanacPropertyDeleteController', ), + 'namespace/' => array( + $this->getQueryRoutePattern() => 'AlmanacNamespaceListController', + $this->getEditRoutePattern('edit/') + => 'AlmanacNamespaceEditController', + '(?P\d+)/' => 'AlmanacNamespaceViewController', + ), ), ); } @@ -84,6 +90,9 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { AlmanacCreateNetworksCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), + AlmanacCreateNamespacesCapability::CAPABILITY => array( + 'default' => PhabricatorPolicies::POLICY_ADMIN, + ), AlmanacCreateClusterServicesCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), diff --git a/src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php b/src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php new file mode 100644 index 0000000000..bfff9aaeaf --- /dev/null +++ b/src/applications/almanac/capability/AlmanacCreateNamespacesCapability.php @@ -0,0 +1,16 @@ +setUser($viewer); - $menu->addItem( - id(new PHUIObjectItemView()) - ->setHeader(pht('Services')) - ->setHref($this->getApplicationURI('service/')) - ->setIcon('fa-plug') - ->addAttribute(pht('Manage Almanac services.'))); - $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Devices')) ->setHref($this->getApplicationURI('device/')) ->setIcon('fa-server') - ->addAttribute(pht('Manage Almanac devices.'))); + ->addAttribute( + pht( + 'Create an inventory of physical and virtual hosts and '. + 'devices.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Services')) + ->setHref($this->getApplicationURI('service/')) + ->setIcon('fa-plug') + ->addAttribute( + pht( + 'Create and update services, and map them to interfaces on '. + 'devices.'))); $menu->addItem( id(new PHUIObjectItemView()) ->setHeader(pht('Networks')) ->setHref($this->getApplicationURI('network/')) ->setIcon('fa-globe') - ->addAttribute(pht('Manage Almanac networks.'))); + ->addAttribute( + pht( + 'Manage public and private networks.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Namespaces')) + ->setHref($this->getApplicationURI('namespace/')) + ->setIcon('fa-asterisk') + ->addAttribute( + pht('Control who can create new named services and devices.'))); + + $docs_uri = PhabricatorEnv::getDoclink( + 'Almanac User Guide'); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Documentation')) + ->setHref($docs_uri) + ->setIcon('fa-book') + ->addAttribute(pht('Browse documentation for Almanac.'))); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Console')); diff --git a/src/applications/almanac/controller/AlmanacNamespaceController.php b/src/applications/almanac/controller/AlmanacNamespaceController.php new file mode 100644 index 0000000000..abdb8a01e6 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceController.php @@ -0,0 +1,14 @@ +getApplicationURI('namespace/'); + $crumbs->addTextCrumb(pht('Namespaces'), $list_uri); + + return $crumbs; + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceEditController.php b/src/applications/almanac/controller/AlmanacNamespaceEditController.php new file mode 100644 index 0000000000..1f3cf84e52 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceEditController.php @@ -0,0 +1,11 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceListController.php b/src/applications/almanac/controller/AlmanacNamespaceListController.php new file mode 100644 index 0000000000..72c9593457 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceListController.php @@ -0,0 +1,26 @@ +setController($this) + ->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + id(new AlmanacNamespaceEditEngine()) + ->setViewer($this->getViewer()) + ->addActionToCrumbs($crumbs); + + return $crumbs; + } + +} diff --git a/src/applications/almanac/controller/AlmanacNamespaceViewController.php b/src/applications/almanac/controller/AlmanacNamespaceViewController.php new file mode 100644 index 0000000000..4f140683a2 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacNamespaceViewController.php @@ -0,0 +1,87 @@ +getViewer(); + + $id = $request->getURIData('id'); + $namespace = id(new AlmanacNamespaceQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$namespace) { + return new Aphront404Response(); + } + + $title = pht('Namespace %s', $namespace->getName()); + + $property_list = $this->buildPropertyList($namespace); + $action_list = $this->buildActionList($namespace); + $property_list->setActionList($action_list); + + $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setHeader($namespace->getName()) + ->setPolicyObject($namespace); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($property_list); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb($namespace->getName()); + + $timeline = $this->buildTransactionTimeline( + $namespace, + new AlmanacNamespaceTransactionQuery()); + $timeline->setShouldTerminate(true); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $box, + $timeline, + )); + } + + private function buildPropertyList(AlmanacNamespace $namespace) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + return $properties; + } + + private function buildActionList(AlmanacNamespace $namespace) { + $viewer = $this->getViewer(); + $id = $namespace->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $namespace, + PhabricatorPolicyCapability::CAN_EDIT); + + $actions = id(new PhabricatorActionListView()) + ->setUser($viewer); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Namespace')) + ->setHref($this->getApplicationURI("namespace/edit/{$id}/")) + ->setWorkflow(!$can_edit) + ->setDisabled(!$can_edit)); + + return $actions; + } + +} diff --git a/src/applications/almanac/controller/AlmanacPropertyEditController.php b/src/applications/almanac/controller/AlmanacPropertyEditController.php index 9a881bc046..aa52dc07d6 100644 --- a/src/applications/almanac/controller/AlmanacPropertyEditController.php +++ b/src/applications/almanac/controller/AlmanacPropertyEditController.php @@ -55,7 +55,7 @@ final class AlmanacPropertyEditController } else { $caught = null; try { - AlmanacNames::validateServiceOrDeviceName($name); + AlmanacNames::validateName($name); } catch (Exception $ex) { $caught = $ex; } @@ -92,7 +92,7 @@ final class AlmanacPropertyEditController // Make sure property key is appropriate. // TODO: It would be cleaner to put this safety check in the Editor. - AlmanacNames::validateServiceOrDeviceName($property_key); + AlmanacNames::validateName($property_key); // If we're adding a new property, put a placeholder on the object so // that we can build a CustomField for it. diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php index 25d49fb295..055322d8b0 100644 --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -136,7 +136,7 @@ final class AlmanacDeviceEditor $name = $xaction->getNewValue(); try { - AlmanacNames::validateServiceOrDeviceName($name); + AlmanacNames::validateName($name); } catch (Exception $ex) { $message = $ex->getMessage(); } diff --git a/src/applications/almanac/editor/AlmanacNamespaceEditEngine.php b/src/applications/almanac/editor/AlmanacNamespaceEditEngine.php new file mode 100644 index 0000000000..0aaba76d3c --- /dev/null +++ b/src/applications/almanac/editor/AlmanacNamespaceEditEngine.php @@ -0,0 +1,86 @@ +getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Namespace'); + } + + protected function getObjectCreateShortText() { + return pht('Create Namespace'); + } + + protected function getEditorURI() { + return '/almanac/namespace/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/almanac/namespace/'; + } + + protected function getObjectViewURI($object) { + $id = $object->getID(); + return "/almanac/namespace/{$id}/"; + } + + protected function getCreateNewObjectPolicy() { + return $this->getApplication()->getPolicy( + AlmanacCreateNamespacesCapability::CAPABILITY); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the namespace.')) + ->setTransactionType(AlmanacNamespaceTransaction::TYPE_NAME) + ->setIsRequired(true) + ->setValue($object->getName()), + ); + } + +} diff --git a/src/applications/almanac/editor/AlmanacNamespaceEditor.php b/src/applications/almanac/editor/AlmanacNamespaceEditor.php new file mode 100644 index 0000000000..b11a081630 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacNamespaceEditor.php @@ -0,0 +1,162 @@ +getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + return $object->getName(); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + $object->setName($xaction->getNewValue()); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacNamespaceTransaction::TYPE_NAME: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case AlmanacNamespaceTransaction::TYPE_NAME: + $missing = $this->validateIsEmptyTextField( + $object->getName(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('Namespace name is required.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } else { + foreach ($xactions as $xaction) { + $name = $xaction->getNewValue(); + + $message = null; + try { + AlmanacNames::validateName($name); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $message, + $xaction); + $errors[] = $error; + continue; + } + + $other = id(new AlmanacNamespaceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'The namespace name "%s" is already in use by another '. + 'namespace. Each namespace must have a unique name.', + $name), + $xaction); + $errors[] = $error; + continue; + } + } + } + + break; + } + + return $errors; + } + + protected function didCatchDuplicateKeyException( + PhabricatorLiskDAO $object, + array $xactions, + Exception $ex) { + + $errors = array(); + + $errors[] = new PhabricatorApplicationTransactionValidationError( + null, + pht('Invalid'), + pht( + 'Another namespace with this name already exists. Each namespace '. + 'must have a unique name.'), + null); + + throw new PhabricatorApplicationTransactionValidationException($errors); + } + +} diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php index 178621fee0..4c230f8b85 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -128,7 +128,7 @@ final class AlmanacServiceEditor $name = $xaction->getNewValue(); try { - AlmanacNames::validateServiceOrDeviceName($name); + AlmanacNames::validateName($name); } catch (Exception $ex) { $message = $ex->getMessage(); } diff --git a/src/applications/almanac/phid/AlmanacNamespacePHIDType.php b/src/applications/almanac/phid/AlmanacNamespacePHIDType.php new file mode 100644 index 0000000000..aa0b8fc4ef --- /dev/null +++ b/src/applications/almanac/phid/AlmanacNamespacePHIDType.php @@ -0,0 +1,44 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $namespace = $objects[$phid]; + + $id = $namespace->getID(); + $name = $namespace->getName(); + + $handle->setObjectName(pht('Namespace %d', $id)); + $handle->setName($name); + $handle->setURI($namespace->getURI()); + } + } + +} diff --git a/src/applications/almanac/query/AlmanacNamespaceQuery.php b/src/applications/almanac/query/AlmanacNamespaceQuery.php new file mode 100644 index 0000000000..48570c0f80 --- /dev/null +++ b/src/applications/almanac/query/AlmanacNamespaceQuery.php @@ -0,0 +1,103 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withNames(array $names) { + $this->names = $names; + return $this; + } + + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new AlmanacNamespaceNameNgrams(), + $ngrams); + } + + public function newResultObject() { + return new AlmanacNamespace(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'namespace.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'namespace.phid IN (%Ls)', + $this->phids); + } + + if ($this->names !== null) { + $where[] = qsprintf( + $conn, + 'namespace.name IN (%Ls)', + $this->names); + } + + return $where; + } + + protected function getPrimaryTableAlias() { + return 'namespace'; + } + + public function getOrderableColumns() { + return parent::getOrderableColumns() + array( + 'name' => array( + 'table' => $this->getPrimaryTableAlias(), + 'column' => 'name', + 'type' => 'string', + 'unique' => true, + 'reverse' => true, + ), + ); + } + + protected function getPagingValueMap($cursor, array $keys) { + $namespace = $this->loadCursorObject($cursor); + return array( + 'id' => $namespace->getID(), + 'name' => $namespace->getName(), + ); + } + + public function getBuiltinOrders() { + return array( + 'name' => array( + 'vector' => array('name'), + 'name' => pht('Namespace Name'), + ), + ) + parent::getBuiltinOrders(); + } + + public function getQueryApplicationClass() { + return 'PhabricatorAlmanacApplication'; + } + +} diff --git a/src/applications/almanac/query/AlmanacNamespaceSearchEngine.php b/src/applications/almanac/query/AlmanacNamespaceSearchEngine.php new file mode 100644 index 0000000000..14a96d22a0 --- /dev/null +++ b/src/applications/almanac/query/AlmanacNamespaceSearchEngine.php @@ -0,0 +1,90 @@ +setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for namespaces by name substring.')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } + + return $query; + } + + protected function getURI($path) { + return '/almanac/namespace/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Namespaces'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $namespaces, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($namespaces, 'AlmanacNamespace'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($namespaces as $namespace) { + $id = $namespace->getID(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Namespace %d', $id)) + ->setHeader($namespace->getName()) + ->setHref($this->getApplicationURI("namespace/{$id}/")) + ->setObject($namespace); + + $list->addItem($item); + } + + $result = new PhabricatorApplicationSearchResultView(); + $result->setObjectList($list); + $result->setNoDataString(pht('No Almanac namespaces found.')); + + return $result; + } +} diff --git a/src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php b/src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php new file mode 100644 index 0000000000..64ce639f45 --- /dev/null +++ b/src/applications/almanac/query/AlmanacNamespaceTransactionQuery.php @@ -0,0 +1,10 @@ +setLabel(pht('Name Contains')) ->setKey('match') - ->setDescription(pht('Search for devices by name substring.')), + ->setDescription(pht('Search for networks by name substring.')), ); } diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index 09503a31b6..425fc32818 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -56,7 +56,7 @@ final class AlmanacDevice } public function save() { - AlmanacNames::validateServiceOrDeviceName($this->getName()); + AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); diff --git a/src/applications/almanac/storage/AlmanacNamespace.php b/src/applications/almanac/storage/AlmanacNamespace.php new file mode 100644 index 0000000000..ff27d6c52b --- /dev/null +++ b/src/applications/almanac/storage/AlmanacNamespace.php @@ -0,0 +1,197 @@ +setViewPolicy(PhabricatorPolicies::POLICY_USER) + ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) + ->attachAlmanacProperties(array()); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text128', + 'nameIndex' => 'bytes12', + 'mailKey' => 'bytes20', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_nameindex' => array( + 'columns' => array('nameIndex'), + 'unique' => true, + ), + 'key_name' => array( + 'columns' => array('name'), + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + AlmanacNamespacePHIDType::TYPECONST); + } + + public function save() { + AlmanacNames::validateName($this->getName()); + + $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); + + if (!$this->mailKey) { + $this->mailKey = Filesystem::readRandomCharacters(20); + } + + return parent::save(); + } + + public function getURI() { + return '/almanac/namespace/view/'.$this->getName().'/'; + } + + +/* -( AlmanacPropertyInterface )------------------------------------------- */ + + + public function attachAlmanacProperties(array $properties) { + assert_instances_of($properties, 'AlmanacProperty'); + $this->almanacProperties = mpull($properties, null, 'getFieldName'); + return $this; + } + + public function getAlmanacProperties() { + return $this->assertAttached($this->almanacProperties); + } + + public function hasAlmanacProperty($key) { + $this->assertAttached($this->almanacProperties); + return isset($this->almanacProperties[$key]); + } + + public function getAlmanacProperty($key) { + return $this->assertAttachedKey($this->almanacProperties, $key); + } + + public function getAlmanacPropertyValue($key, $default = null) { + if ($this->hasAlmanacProperty($key)) { + return $this->getAlmanacProperty($key)->getFieldValue(); + } else { + return $default; + } + } + + public function getAlmanacPropertyFieldSpecifications() { + return array(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + + +/* -( PhabricatorCustomFieldInterface )------------------------------------ */ + + + public function getCustomFieldSpecificationForRole($role) { + return array(); + } + + public function getCustomFieldBaseClass() { + return 'AlmanacCustomField'; + } + + public function getCustomFields() { + return $this->assertAttached($this->customFields); + } + + public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { + $this->customFields = $fields; + return $this; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new AlmanacNamespaceEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new AlmanacNamespaceTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + return $timeline; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + + +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + public function newNgrams() { + return array( + id(new AlmanacNamespaceNameNgrams()) + ->setValue($this->getName()), + ); + } + +} diff --git a/src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php b/src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php new file mode 100644 index 0000000000..c7907b931e --- /dev/null +++ b/src/applications/almanac/storage/AlmanacNamespaceNameNgrams.php @@ -0,0 +1,18 @@ +getAuthorPHID(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case PhabricatorTransactions::TYPE_CREATE: + return pht( + '%s created this namespace.', + $this->renderHandleLink($author_phid)); + break; + case self::TYPE_NAME: + return pht( + '%s renamed this namespace from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); + } + + return parent::getTitle(); + } + +} diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index c16f85c8ee..765ae58278 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -62,7 +62,7 @@ final class AlmanacService } public function save() { - AlmanacNames::validateServiceOrDeviceName($this->getName()); + AlmanacNames::validateName($this->getName()); $this->nameIndex = PhabricatorHash::digestForIndex($this->getName()); diff --git a/src/applications/almanac/util/AlmanacNames.php b/src/applications/almanac/util/AlmanacNames.php index d65779264d..68a387cb9b 100644 --- a/src/applications/almanac/util/AlmanacNames.php +++ b/src/applications/almanac/util/AlmanacNames.php @@ -2,54 +2,61 @@ final class AlmanacNames extends Phobject { - public static function validateServiceOrDeviceName($name) { + public static function validateName($name) { if (strlen($name) < 3) { throw new Exception( pht( - 'Almanac service and device names must be at least 3 '. - 'characters long.')); + 'Almanac service, device, property and namespace names must be '. + 'at least 3 characters long.')); + } + + if (strlen($name) > 100) { + throw new Exception( + pht( + 'Almanac service, device, property and namespace names may not '. + 'be more than 100 characters long.')); } if (!preg_match('/^[a-z0-9.-]+\z/', $name)) { throw new Exception( pht( - 'Almanac service and device names may only contain lowercase '. - 'letters, numbers, hyphens, and periods.')); + 'Almanac service, device, property and namespace names may only '. + 'contain lowercase letters, numbers, hyphens, and periods.')); } if (preg_match('/(^|\\.)\d+(\z|\\.)/', $name)) { throw new Exception( pht( - 'Almanac service and device names may not have any segments '. - 'containing only digits.')); + 'Almanac service, device, property and namespace names may not '. + 'have any segments containing only digits.')); } if (preg_match('/\.\./', $name)) { throw new Exception( pht( - 'Almanac service and device names may not contain multiple '. - 'consecutive periods.')); + 'Almanac service, device, property and namespace names may not '. + 'contain multiple consecutive periods.')); } if (preg_match('/\\.-|-\\./', $name)) { throw new Exception( pht( - 'Amanac service and device names may not contain hyphens adjacent '. - 'to periods.')); + 'Almanac service, device, property and namespace names may not '. + 'contain hyphens adjacent to periods.')); } if (preg_match('/--/', $name)) { throw new Exception( pht( - 'Almanac service and device names may not contain multiple '. - 'consecutive hyphens.')); + 'Almanac service, device, property and namespace names may not '. + 'contain multiple consecutive hyphens.')); } if (!preg_match('/^[a-z0-9].*[a-z0-9]\z/', $name)) { throw new Exception( pht( - 'Almanac service and device names must begin and end with a letter '. - 'or number.')); + 'Almanac service, device, property and namespace names must begin '. + 'and end with a letter or number.')); } } diff --git a/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php b/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php index 7ca1a37f9f..78dea769e4 100644 --- a/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php +++ b/src/applications/almanac/util/__tests__/AlmanacNamesTestCase.php @@ -33,12 +33,16 @@ final class AlmanacNamesTestCase extends PhabricatorTestCase { 'db.phacility.instance' => true, 'web002.useast.example.com' => true, 'master.example-corp.com' => true, + + // Maximum length is 100. + str_repeat('a', 100) => true, + str_repeat('a', 101) => false, ); foreach ($map as $input => $expect) { $caught = null; try { - AlmanacNames::validateServiceOrDeviceName($input); + AlmanacNames::validateName($input); } catch (Exception $ex) { $caught = $ex; } diff --git a/src/docs/user/userguide/almanac.diviner b/src/docs/user/userguide/almanac.diviner index c6f5ef2971..2e950a586a 100644 --- a/src/docs/user/userguide/almanac.diviner +++ b/src/docs/user/userguide/almanac.diviner @@ -1,13 +1,138 @@ @title Almanac User Guide @group userguide -Using Almanac to manage services. +Using Almanac to manage devices and services. -= Overview = +Overview +======== IMPORTANT: Almanac is a prototype application. See @{article:User Guide: Prototype Applications}. +Almanac is a device and service inventory application. It allows you to create +lists of //devices// and //services// that humans and other applications can +use to keep track of what is running where. + +At a very high level, Almanac can be thought of as a bit like a DNS server. +Callers ask it for information about services, and it responds with details +about which devices host those services. However, it can respond to a broader +range of queries and provide more detailed responses than DNS alone can. + +Today, the primary use cases for Almanac involve configuring Phabricator +itself: Almanac is used to configure Phabricator to operate in a cluster setup, +and to expose hardware to Drydock so it can run build and integration tasks. + +Beyond internal uses, Almanac is a general-purpose service and device inventory +application and can be used to configure and manage other types of service and +hardware inventories, but these use cases are currently considered experimental +and you should be exercise caution in pursuing them. + + +Example: Drydock Build Pool +================================ + +Here's a quick example of how you might configure Almanac to solve a real-world +problem. This section describes configuration at a high level to give you an +introduction to Almanac concepts and a better idea of how the pieces fit +together. + +In this scenario, we want to use Drydock to run some sort of build process. To +do this, Drydock needs hardware to run on. We're going to use Almanac to tell +Drydock about the hardware it should use. + +In this scenario, Almanac will work a bit like a DNS server. When we're done, +Drydock will be able to query Almanac for information about a service (like +`build.mycompany.com`) and get back information about which hosts are part of +that service and where it should connect to. + +Before getting started, we need to create a **network**. For simplicity, let's +suppose everything will be connected through the public internet. If you +haven't already, you'd create a "Public Internet" network first. + +Once we have a network, we create the actual physical or virtual hosts by +launching instances in EC2, or racking and powering on some servers, or already +having some hardware on hand we want to use. We set the hosts up normally and +connect them to the internet or network. + +After the hosts exist, we add them to Almanac as **devices**, like +`build001.mycompany.com`, `build002.mycompany.com`, and so on. In Almanac, +devices are usually physical or virtual hosts, although you could also use it +to inventory other types of devices and hardware. + +For each **device**, we add an **interface**. This is just an address and port +on a particular network. Since we're going to connect to these hosts over +SSH, we'll add interfaces on the standard SSH port 22. An example configuration +might look a little bit like this: + +| Device | Network | Address | Port | +|--------|---------|---------|------| +| `build001.mycompany.com` | Public Internet | 58.8.9.10 | 22 +| `build002.mycompany.com` | Public Internet | 58.8.9.11 | 22 +| ... | Public Internet | ... | 22 + +Now, we create the **service**. This is what we'll tell Drydock about, and +it can query for information about this service to find connected devices. +Here, we'll call it `build.mycompany.com`. + +After creating the service, add **bindings** to the interfaces we configured +above. This will tell Drydock where it should actually connect to. + +Once this is complete, we're done in Almanac and can continue configuration in +Drydock, which is outside the scope of this example. Once everything is fully +configured, this is how Almanac will be used by Drydock: + + - Drydock will query information about `build.mycompany.com` from Almanac. + - Drydock will get back a list of bound interfaces, among other data. + - The interfaces provide information about addresses and ports that Drydock + can use to connect to the actual devices. + +You can now add and remove devices to the pool by binding them and unbinding +them from the service. + + +Concepts +======== + +The major concepts in Almanac are **devices*, **interfaces**, **services**, +**bindings**, **networks**, and **namespaces**. + +**Devices**: Almanac devices represent physical or virtual devices. +Usually, they are hosts (like `web001.mycompany.net`), although you could +use devices to keep inventory of any other kind of device or physical asset +(like phones, laptops, or office chairs). + +Each device has a name, and may have properties and interfaces. + +**Interfaces**: Interfaces are listening address/port combinations on devices. +For example, if you have a webserver host device named `web001.mycompany.net`, +you might add an interface on port `80`. + +Interfaces tell users and applications where they should connect to to access +services and devices. + +**Services**: These are named services like `build.mycompany.net` that work +a bit like DNS. Humans or other applications can look up a service to find +configuration information and learn which devices are hosting the service. + +Each service has a name, and may have properties and bindings. + +**Bindings**: Bindings are connections between services and interfaces. They +tell callers which devices host a named service. + +**Networks**: Networks allow Almanac to distingiush between addresses on +different networks, like VPNs vs the public internet. + +If you have hosts in different VPNs or on private networks, you might have +multiple devices which share the same IP address (like `10.0.0.3`). Networks +allow Almanac to distinguish between devices with the same address on different +sections of the network. + +**Namespaces**: Namespaces let you control who is permitted to create devices +and services with particular names. For example, the namespace `mycompany.com` +controls who can create services with names like `a.mycompany.com` and +`b.mycompany.com`. + + Locking and Unlocking Services ============================== @@ -17,8 +142,8 @@ services prevents an attacker from modifying the Phabricator cluster definition. For more details on this scenario, see @{article:User Guide: Phabricator Clusters}. -Beyond hardening cluster definitions, you might also want to lock a service to -prevent accidental edits. +Beyond hardening cluster definitions, you might also want to lock a critical +service to prevent accidental edits. To lock a service, run: From 411331469af4c2954b28b39932615fc8f1aaac0b Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 21 Feb 2016 14:34:03 -0800 Subject: [PATCH 09/31] Apply namespace locking rules in Almanac Summary: Ref T10246. Ref T6741. When you have a namespace like "phacility.net", require users creating services and devices within it to have edit permission on the namespace. This primarily allows us to lock down future device names in the cluster, so instances can't break themselves once they get access to Almanac. Test Plan: - Configured a `phacility.net` namespace, locked myself out of it. - Could not create new `stuff.phacility.net` services/devices. - Could still edit existing devices I had permission for. - Configured a `free.phacility.net` namespace with more liberal policies. - Could create `me.free.phacility.net`. - Still could not create `other.phacility.net`. Reviewers: chad Reviewed By: chad Maniphest Tasks: T6741, T10246 Differential Revision: https://secure.phabricator.com/D15325 --- src/__phutil_library_map__.php | 2 +- .../almanac/editor/AlmanacDeviceEditor.php | 48 +++++++++++++------ .../almanac/editor/AlmanacNamespaceEditor.php | 22 ++++++++- .../almanac/editor/AlmanacServiceEditor.php | 48 +++++++++++++------ .../almanac/query/AlmanacNamespaceQuery.php | 2 +- .../almanac/storage/AlmanacNamespace.php | 45 +++++++++++++++++ src/docs/user/userguide/almanac.diviner | 46 ++++++++++++++++++ 7 files changed, 182 insertions(+), 31 deletions(-) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 92943d801d..95d66112ab 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4079,7 +4079,7 @@ phutil_register_library_map(array( 'AlmanacNamespaceListController' => 'AlmanacNamespaceController', 'AlmanacNamespaceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacNamespacePHIDType' => 'PhabricatorPHIDType', - 'AlmanacNamespaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacNamespaceQuery' => 'AlmanacQuery', 'AlmanacNamespaceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacNamespaceTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacNamespaceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php index 055322d8b0..e76cc338b5 100644 --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -148,22 +148,42 @@ final class AlmanacDeviceEditor $message, $xaction); $errors[] = $error; + continue; } - } - } - if ($xactions) { - $duplicate = id(new AlmanacDeviceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withNames(array(last($xactions)->getNewValue())) - ->executeOne(); - if ($duplicate && ($duplicate->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Not Unique'), - pht('Almanac devices must have unique names.'), - last($xactions)); - $errors[] = $error; + $other = id(new AlmanacDeviceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Not Unique'), + pht('Almanac devices must have unique names.'), + $xaction); + $errors[] = $error; + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Restricted'), + pht( + 'You do not have permission to create Almanac devices '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + $errors[] = $error; + continue; + } } } diff --git a/src/applications/almanac/editor/AlmanacNamespaceEditor.php b/src/applications/almanac/editor/AlmanacNamespaceEditor.php index b11a081630..5243d3c935 100644 --- a/src/applications/almanac/editor/AlmanacNamespaceEditor.php +++ b/src/applications/almanac/editor/AlmanacNamespaceEditor.php @@ -123,7 +123,7 @@ final class AlmanacNamespaceEditor if ($other && ($other->getID() != $object->getID())) { $error = new PhabricatorApplicationTransactionValidationError( $type, - pht('Invalid'), + pht('Not Unique'), pht( 'The namespace name "%s" is already in use by another '. 'namespace. Each namespace must have a unique name.', @@ -132,6 +132,26 @@ final class AlmanacNamespaceEditor $errors[] = $error; continue; } + + if ($name === $object->getName()) { + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Restricted'), + pht( + 'You do not have permission to create Almanac namespaces '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + $errors[] = $error; + continue; + } } } diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php index 4c230f8b85..b8013b5261 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -140,22 +140,42 @@ final class AlmanacServiceEditor $message, $xaction); $errors[] = $error; + continue; } - } - } - if ($xactions) { - $duplicate = id(new AlmanacServiceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withNames(array(last($xactions)->getNewValue())) - ->executeOne(); - if ($duplicate && ($duplicate->getID() != $object->getID())) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Not Unique'), - pht('Almanac services must have unique names.'), - last($xactions)); - $errors[] = $error; + $other = id(new AlmanacServiceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames(array($name)) + ->executeOne(); + if ($other && ($other->getID() != $object->getID())) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Not Unique'), + pht('Almanac services must have unique names.'), + last($xactions)); + $errors[] = $error; + continue; + } + + if ($name === $object->getName()) { + continue; + } + + $namespace = AlmanacNamespace::loadRestrictedNamespace( + $this->getActor(), + $name); + if ($namespace) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Restricted'), + pht( + 'You do not have permission to create Almanac services '. + 'within the "%s" namespace.', + $namespace->getName()), + $xaction); + $errors[] = $error; + continue; + } } } diff --git a/src/applications/almanac/query/AlmanacNamespaceQuery.php b/src/applications/almanac/query/AlmanacNamespaceQuery.php index 48570c0f80..81332cf03b 100644 --- a/src/applications/almanac/query/AlmanacNamespaceQuery.php +++ b/src/applications/almanac/query/AlmanacNamespaceQuery.php @@ -1,7 +1,7 @@ getName().'/'; } + public function getNameLength() { + return strlen($this->getName()); + } + + /** + * Load the namespace which prevents use of an Almanac name, if one exists. + */ + public static function loadRestrictedNamespace( + PhabricatorUser $viewer, + $name) { + + // For a name like "x.y.z", produce a list of controlling namespaces like + // ("z", "y.x", "x.y.z"). + $names = array(); + $parts = explode('.', $name); + for ($ii = 0; $ii < count($parts); $ii++) { + $names[] = implode('.', array_slice($parts, -($ii + 1))); + } + + // Load all the possible controlling namespaces. + $namespaces = id(new AlmanacNamespaceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withNames($names) + ->execute(); + if (!$namespaces) { + return null; + } + + // Find the "nearest" (longest) namespace that exists. If both + // "sub.domain.com" and "domain.com" exist, we only care about the policy + // on the former. + $namespaces = msort($namespaces, 'getNameLength'); + $namespace = last($namespaces); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $namespace, + PhabricatorPolicyCapability::CAN_EDIT); + if ($can_edit) { + return null; + } + + return $namespace; + } + /* -( AlmanacPropertyInterface )------------------------------------------- */ diff --git a/src/docs/user/userguide/almanac.diviner b/src/docs/user/userguide/almanac.diviner index 2e950a586a..d84176c57d 100644 --- a/src/docs/user/userguide/almanac.diviner +++ b/src/docs/user/userguide/almanac.diviner @@ -133,6 +133,52 @@ controls who can create services with names like `a.mycompany.com` and `b.mycompany.com`. +Namespaces +========== + +Almanac namespaces allow you to control who can create services and devices +with certain names. + +If you keep a list of cattle as devices with names like +`cow001.herd.myranch.com`, `cow002.herd.myranch.moo`, you might have some +applications which query for all devices in `*.herd.myranch.moo`, and thus +want to limit who can create devices there in order to prevent mistakes. + +If a namespace like `herd.myranch.moo` exists, users must have permission to +edit the namespace in order to create new services, devices, or namespaces +within it. For example, a user can not create `cow003.herd.myranch.moo` if +they do not have edit permission on the `herd.myranch.moo` namespace. + +When you try to create a `cow003.herd.myranch.moo` service (or rename an +existing service to have that name), Almanac looks for these namespaces, then +checks the policy of the first one it finds: + +| Namespace | +|----|----- +| `cow003.herd.ranch.moo` | //"Nearest" namespace, considered first.// +| `herd.ranch.moo` | | +| `ranch.moo` | | +| `moo` | //"Farthest" namespace, considered last.// + +Note that namespaces treat names as lists of domain parts, not as strict +substrings, so the namespace `herd.myranch.moo` does not prevent +someone from creating `goatherd.myranch.moo` or `goat001.goatherd.myranch.moo`. +The name `goatherd.myranch.moo` is not part of the `herd.myranch.moo` namespace +because the initial subdomain differs. + +If a name belongs to multiple namespaces, the policy of the nearest namespace +is controlling. For example, if `myranch.moo` has a very restrictive edit +policy but `shed.myranch.moo` has a more open one, users can create devices and +services like `rake.shed.myranch.moo` as long as they can pass the policy check +for `shed.myranch.moo`, even if they do not have permission under the policy +for `myranch.moo`. + +Users can edit services and devices within a namespace if they have edit +permission on the service or device itself, as long as they don't try to rename +the service or device to move it into a namespace they don't have permission +to access. + + Locking and Unlocking Services ============================== From ab86523ac4902eb4c0fbb55fdd133fd2611109b1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 22 Feb 2016 06:35:30 -0800 Subject: [PATCH 10/31] Allow Almanac properties to be deleted, use EditEngine instead of CustomField Summary: Fixes T10410. Immediate impact of this is that you can now actually delete properties from Almanac services, devices and bindings. The meat of the change is switching from CustomField to EditEngine for most of the actual editing logic. CustomField creates a lot of problems with using EditEngine for everything else (D15326), and weird, hard-to-resolve bugs like this one (not being able to delete stuff). Using EditEngine to do this stuff instead seems like it works out much better -- I did this in ProfilePanel first and am happy with how it looks. This also makes the internal storage for properties JSON instead of raw text. Test Plan: - Created, edited and deleted properties on services, devices and bindings. - Edited and reset builtin properties on repository services. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10410 Differential Revision: https://secure.phabricator.com/D15327 --- .../20160222.almanac.1.properties.php | 28 +++ src/__phutil_library_map__.php | 37 ++-- .../PhabricatorAlmanacApplication.php | 12 +- .../almanac/controller/AlmanacController.php | 59 +++--- .../AlmanacPropertyDeleteController.php | 49 +---- .../AlmanacPropertyEditController.php | 178 +++++++----------- .../controller/AlmanacServiceController.php | 10 + .../customfield/AlmanacCoreCustomField.php | 80 -------- .../customfield/AlmanacCustomField.php | 4 - .../almanac/editor/AlmanacBindingEditor.php | 6 +- .../AlmanacBindingPropertyEditEngine.php | 16 ++ .../almanac/editor/AlmanacDeviceEditor.php | 10 +- .../AlmanacDevicePropertyEditEngine.php | 16 ++ .../almanac/editor/AlmanacEditor.php | 156 +++++++++++++++ .../editor/AlmanacPropertyEditEngine.php | 79 ++++++++ .../almanac/editor/AlmanacServiceEditor.php | 12 +- .../AlmanacServicePropertyEditEngine.php | 16 ++ .../property/AlmanacPropertyInterface.php | 1 + .../almanac/query/AlmanacQuery.php | 19 +- .../AlmanacClusterDatabaseServiceType.php | 4 - .../AlmanacClusterRepositoryServiceType.php | 11 +- .../almanac/storage/AlmanacBinding.php | 31 +-- .../storage/AlmanacBindingTransaction.php | 2 +- .../almanac/storage/AlmanacDevice.php | 27 +-- .../storage/AlmanacDeviceTransaction.php | 2 +- .../almanac/storage/AlmanacNamespace.php | 27 +-- .../almanac/storage/AlmanacProperty.php | 66 +++---- .../almanac/storage/AlmanacService.php | 27 +-- .../storage/AlmanacServiceTransaction.php | 10 +- .../almanac/storage/AlmanacTransaction.php | 38 ++++ .../editengine/PhabricatorEditEngine.php | 62 ++++-- .../PhabricatorApplicationTransaction.php | 8 +- 32 files changed, 605 insertions(+), 498 deletions(-) create mode 100644 resources/sql/autopatches/20160222.almanac.1.properties.php delete mode 100644 src/applications/almanac/customfield/AlmanacCoreCustomField.php delete mode 100644 src/applications/almanac/customfield/AlmanacCustomField.php create mode 100644 src/applications/almanac/editor/AlmanacBindingPropertyEditEngine.php create mode 100644 src/applications/almanac/editor/AlmanacDevicePropertyEditEngine.php create mode 100644 src/applications/almanac/editor/AlmanacEditor.php create mode 100644 src/applications/almanac/editor/AlmanacPropertyEditEngine.php create mode 100644 src/applications/almanac/editor/AlmanacServicePropertyEditEngine.php create mode 100644 src/applications/almanac/storage/AlmanacTransaction.php diff --git a/resources/sql/autopatches/20160222.almanac.1.properties.php b/resources/sql/autopatches/20160222.almanac.1.properties.php new file mode 100644 index 0000000000..d12b3e5c61 --- /dev/null +++ b/resources/sql/autopatches/20160222.almanac.1.properties.php @@ -0,0 +1,28 @@ +establishConnection('w'); + +// We're going to JSON-encode the value in each row: previously rows stored +// plain strings, but now they store JSON, so we need to update them. + +foreach (new LiskMigrationIterator($table) as $property) { + $key = $property->getFieldName(); + + $current_row = queryfx_one( + $conn_w, + 'SELECT fieldValue FROM %T WHERE id = %d', + $table->getTableName(), + $property->getID()); + + if (!$current_row) { + continue; + } + + queryfx( + $conn_w, + 'UPDATE %T SET fieldValue = %s WHERE id = %d', + $table->getTableName(), + phutil_json_encode($current_row['fieldValue']), + $property->getID()); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 95d66112ab..e4196425e1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -14,6 +14,7 @@ phutil_register_library_map(array( 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', + 'AlmanacBindingPropertyEditEngine' => 'applications/almanac/editor/AlmanacBindingPropertyEditEngine.php', 'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php', 'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php', 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', @@ -25,13 +26,11 @@ phutil_register_library_map(array( 'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', - 'AlmanacCoreCustomField' => 'applications/almanac/customfield/AlmanacCoreCustomField.php', 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', 'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', 'AlmanacCreateServicesCapability' => 'applications/almanac/capability/AlmanacCreateServicesCapability.php', - 'AlmanacCustomField' => 'applications/almanac/customfield/AlmanacCustomField.php', 'AlmanacCustomServiceType' => 'applications/almanac/servicetype/AlmanacCustomServiceType.php', 'AlmanacDAO' => 'applications/almanac/storage/AlmanacDAO.php', 'AlmanacDevice' => 'applications/almanac/storage/AlmanacDevice.php', @@ -41,12 +40,14 @@ phutil_register_library_map(array( 'AlmanacDeviceListController' => 'applications/almanac/controller/AlmanacDeviceListController.php', 'AlmanacDeviceNameNgrams' => 'applications/almanac/storage/AlmanacDeviceNameNgrams.php', 'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php', + 'AlmanacDevicePropertyEditEngine' => 'applications/almanac/editor/AlmanacDevicePropertyEditEngine.php', 'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php', 'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php', 'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php', 'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php', 'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php', 'AlmanacDrydockPoolServiceType' => 'applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php', + 'AlmanacEditor' => 'applications/almanac/editor/AlmanacEditor.php', 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', @@ -92,6 +93,7 @@ phutil_register_library_map(array( 'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php', 'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php', 'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php', + 'AlmanacPropertyEditEngine' => 'applications/almanac/editor/AlmanacPropertyEditEngine.php', 'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php', 'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php', 'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php', @@ -106,6 +108,7 @@ phutil_register_library_map(array( 'AlmanacServiceListController' => 'applications/almanac/controller/AlmanacServiceListController.php', 'AlmanacServiceNameNgrams' => 'applications/almanac/storage/AlmanacServiceNameNgrams.php', 'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php', + 'AlmanacServicePropertyEditEngine' => 'applications/almanac/editor/AlmanacServicePropertyEditEngine.php', 'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php', 'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php', 'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php', @@ -113,6 +116,7 @@ phutil_register_library_map(array( 'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php', 'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php', 'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php', + 'AlmanacTransaction' => 'applications/almanac/storage/AlmanacTransaction.php', 'AphlictDropdownDataQuery' => 'applications/aphlict/query/AphlictDropdownDataQuery.php', 'Aphront304Response' => 'aphront/response/Aphront304Response.php', 'Aphront400Response' => 'aphront/response/Aphront400Response.php', @@ -3989,17 +3993,17 @@ phutil_register_library_map(array( 'AlmanacBinding' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', - 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', ), 'AlmanacBindingEditController' => 'AlmanacServiceController', - 'AlmanacBindingEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacBindingEditor' => 'AlmanacEditor', 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', + 'AlmanacBindingPropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacBindingQuery' => 'AlmanacQuery', 'AlmanacBindingTableView' => 'AphrontView', - 'AlmanacBindingTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacBindingTransaction' => 'AlmanacTransaction', 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacBindingViewController' => 'AlmanacServiceController', 'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType', @@ -4008,22 +4012,16 @@ phutil_register_library_map(array( 'AlmanacConduitAPIMethod' => 'ConduitAPIMethod', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', - 'AlmanacCoreCustomField' => array( - 'AlmanacCustomField', - 'PhabricatorStandardCustomFieldInterface', - ), 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateServicesCapability' => 'PhabricatorPolicyCapability', - 'AlmanacCustomField' => 'PhabricatorCustomField', 'AlmanacCustomServiceType' => 'AlmanacServiceType', 'AlmanacDAO' => 'PhabricatorLiskDAO', 'AlmanacDevice' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', - 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'PhabricatorSSHPublicKeyInterface', @@ -4033,16 +4031,18 @@ phutil_register_library_map(array( ), 'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', - 'AlmanacDeviceEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacDeviceEditor' => 'AlmanacEditor', 'AlmanacDeviceListController' => 'AlmanacDeviceController', 'AlmanacDeviceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacDevicePHIDType' => 'PhabricatorPHIDType', + 'AlmanacDevicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacDeviceQuery' => 'AlmanacQuery', 'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'AlmanacDeviceTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacDeviceTransaction' => 'AlmanacTransaction', 'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacDeviceViewController' => 'AlmanacDeviceController', 'AlmanacDrydockPoolServiceType' => 'AlmanacServiceType', + 'AlmanacEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacInterface' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', @@ -4065,7 +4065,6 @@ phutil_register_library_map(array( 'AlmanacNamespace' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', - 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'AlmanacPropertyInterface', @@ -4104,12 +4103,13 @@ phutil_register_library_map(array( 'AlmanacNetworkViewController' => 'AlmanacNetworkController', 'AlmanacPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'AlmanacProperty' => array( - 'PhabricatorCustomFieldStorage', + 'AlmanacDAO', 'PhabricatorPolicyInterface', ), 'AlmanacPropertyController' => 'AlmanacController', 'AlmanacPropertyDeleteController' => 'AlmanacDeviceController', 'AlmanacPropertyEditController' => 'AlmanacDeviceController', + 'AlmanacPropertyEditEngine' => 'PhabricatorEditEngine', 'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQueryDevicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', @@ -4118,7 +4118,6 @@ phutil_register_library_map(array( 'AlmanacService' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', - 'PhabricatorCustomFieldInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorProjectInterface', 'AlmanacPropertyInterface', @@ -4128,17 +4127,19 @@ phutil_register_library_map(array( 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacServiceEditController' => 'AlmanacServiceController', - 'AlmanacServiceEditor' => 'PhabricatorApplicationTransactionEditor', + 'AlmanacServiceEditor' => 'AlmanacEditor', 'AlmanacServiceListController' => 'AlmanacServiceController', 'AlmanacServiceNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacServicePHIDType' => 'PhabricatorPHIDType', + 'AlmanacServicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacServiceQuery' => 'AlmanacQuery', 'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'AlmanacServiceTransaction' => 'PhabricatorApplicationTransaction', + 'AlmanacServiceTransaction' => 'AlmanacTransaction', 'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacServiceType' => 'Phobject', 'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase', 'AlmanacServiceViewController' => 'AlmanacServiceController', + 'AlmanacTransaction' => 'PhabricatorApplicationTransaction', 'AphlictDropdownDataQuery' => 'Phobject', 'Aphront304Response' => 'AphrontResponse', 'Aphront400Response' => 'AphrontResponse', diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php index ebcefe4a51..2e413b9fee 100644 --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -43,12 +43,12 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { return array( '/almanac/' => array( '' => 'AlmanacConsoleController', - 'service/' => array( + '(?Pservice)/' => array( $this->getQueryRoutePattern() => 'AlmanacServiceListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacServiceEditController', 'view/(?P[^/]+)/' => 'AlmanacServiceViewController', ), - 'device/' => array( + '(?Pdevice)/' => array( $this->getQueryRoutePattern() => 'AlmanacDeviceListController', 'edit/(?:(?P\d+)/)?' => 'AlmanacDeviceEditController', 'view/(?P[^/]+)/' => 'AlmanacDeviceViewController', @@ -65,16 +65,16 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { 'edit/(?:(?P\d+)/)?' => 'AlmanacNetworkEditController', '(?P\d+)/' => 'AlmanacNetworkViewController', ), - 'property/' => array( - 'edit/' => 'AlmanacPropertyEditController', - 'delete/' => 'AlmanacPropertyDeleteController', - ), 'namespace/' => array( $this->getQueryRoutePattern() => 'AlmanacNamespaceListController', $this->getEditRoutePattern('edit/') => 'AlmanacNamespaceEditController', '(?P\d+)/' => 'AlmanacNamespaceViewController', ), + 'property/' => array( + 'delete/' => 'AlmanacPropertyDeleteController', + 'update/' => 'AlmanacPropertyEditController', + ), ), ); } diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index e1a7c2a69e..9673a0ef4a 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -10,27 +10,14 @@ abstract class AlmanacController $properties = $object->getAlmanacProperties(); $this->requireResource('almanac-css'); + Javelin::initBehavior('phabricator-tooltips', array()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object, PhabricatorPolicyCapability::CAN_EDIT); - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_DEFAULT); - - // Before reading values from the object, read defaults. - $defaults = mpull( - $field_list->getFields(), - 'getValueForStorage', - 'getFieldKey'); - - $field_list - ->setViewer($viewer) - ->readFieldsFromStorage($object); - - Javelin::initBehavior('phabricator-tooltips', array()); + $properties = $object->getAlmanacProperties(); $icon_builtin = id(new PHUIIconView()) ->setIcon('fa-circle') @@ -51,45 +38,46 @@ abstract class AlmanacController )); $builtins = $object->getAlmanacPropertyFieldSpecifications(); + $defaults = mpull($builtins, null, 'getValueForTransaction'); // Sort fields so builtin fields appear first, then fields are ordered // alphabetically. - $fields = $field_list->getFields(); - $fields = msort($fields, 'getFieldKey'); + $properties = msort($properties, 'getFieldName'); $head = array(); $tail = array(); - foreach ($fields as $field) { - $key = $field->getFieldKey(); + foreach ($properties as $property) { + $key = $property->getFieldName(); if (isset($builtins[$key])) { - $head[$key] = $field; + $head[$key] = $property; } else { - $tail[$key] = $field; + $tail[$key] = $property; } } - $fields = $head + $tail; + $properties = $head + $tail; + + $delete_base = $this->getApplicationURI('property/delete/'); + $edit_base = $this->getApplicationURI('property/update/'); $rows = array(); - foreach ($fields as $key => $field) { - $value = $field->getValueForStorage(); + foreach ($properties as $key => $property) { + $value = $property->getFieldValue(); $is_builtin = isset($builtins[$key]); - $delete_uri = $this->getApplicationURI('property/delete/'); - $delete_uri = id(new PhutilURI($delete_uri)) + $delete_uri = id(new PhutilURI($delete_base)) ->setQueryParams( array( - 'objectPHID' => $object->getPHID(), 'key' => $key, + 'objectPHID' => $object->getPHID(), )); - $edit_uri = $this->getApplicationURI('property/edit/'); - $edit_uri = id(new PhutilURI($edit_uri)) + $edit_uri = id(new PhutilURI($edit_base)) ->setQueryParams( array( - 'objectPHID' => $object->getPHID(), 'key' => $key, + 'objectPHID' => $object->getPHID(), )); $delete = javelin_tag( @@ -153,7 +141,8 @@ abstract class AlmanacController )); $phid = $object->getPHID(); - $add_uri = $this->getApplicationURI("property/edit/?objectPHID={$phid}"); + $add_uri = id(new PhutilURI($edit_base)) + ->setQueryParam('objectPHID', $object->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -196,4 +185,12 @@ abstract class AlmanacController $box->setInfoView($error_view); } + protected function getPropertyDeleteURI($object) { + return null; + } + + protected function getPropertyUpdateURI($object) { + return null; + } + } diff --git a/src/applications/almanac/controller/AlmanacPropertyDeleteController.php b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php index 0b328affcd..2bdaa5f35c 100644 --- a/src/applications/almanac/controller/AlmanacPropertyDeleteController.php +++ b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php @@ -34,53 +34,24 @@ final class AlmanacPropertyDeleteController $is_builtin = isset($builtins[$key]); if ($is_builtin) { - // This is a builtin property, so we're going to reset it to the - // default value. - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_DEFAULT); - - // Note that we're NOT loading field values from the object: we just want - // to get the field's default value so we can reset it. - - $fields = $field_list->getFields(); - $field = $fields[$key]; - - $is_delete = false; - $new_value = $field->getValueForStorage(); - - // Now, load the field to get the old value. - - $field_list - ->setViewer($viewer) - ->readFieldsFromStorage($object); - - $old_value = $field->getValueForStorage(); - $title = pht('Reset Property'); - $body = pht('Reset this property to its default value?'); - $submit_text = pht('Reset'); + $body = pht( + 'Reset property "%s" to its default value?', + $key); + $submit_text = pht('Reset Property'); } else { - // This is a custom property, so we're going to delete it outright. - $is_delete = true; - $old_value = $object->getAlmanacPropertyValue($key); - $new_value = null; - $title = pht('Delete Property'); - $body = pht('Delete this property? TODO: DOES NOT WORK YET'); - $submit_text = pht('Delete'); + $body = pht( + 'Delete property "%s"?', + $key); + $submit_text = pht('Delete Property'); } $validation_exception = null; if ($request->isFormPost()) { $xaction = $object->getApplicationTransactionTemplate() - ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) - ->setMetadataValue('customfield:key', $key) - ->setOldValue($old_value) - ->setNewValue($new_value); - - // TODO: We aren't really deleting properties that we claim to delete - // yet, but that needs to be specialized a little bit. + ->setTransactionType(AlmanacTransaction::TYPE_PROPERTY_REMOVE) + ->setMetadataValue('almanac.property', $key); $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) diff --git a/src/applications/almanac/controller/AlmanacPropertyEditController.php b/src/applications/almanac/controller/AlmanacPropertyEditController.php index aa52dc07d6..46d609af86 100644 --- a/src/applications/almanac/controller/AlmanacPropertyEditController.php +++ b/src/applications/almanac/controller/AlmanacPropertyEditController.php @@ -24,133 +24,81 @@ final class AlmanacPropertyEditController } $cancel_uri = $object->getURI(); + $property_key = $request->getStr('key'); - $key = $request->getStr('key'); - if ($key) { - $property_key = $key; - - $is_new = false; - $title = pht('Edit Property'); - $save_button = pht('Save Changes'); + if (!strlen($property_key)) { + return $this->buildPropertyKeyResponse($cancel_uri, null); } else { - $property_key = null; - - $is_new = true; - $title = pht('Add Property'); - $save_button = pht('Add Property'); - } - - if ($is_new) { - $errors = array(); - $property = null; - - $v_name = null; - $e_name = true; - - if ($request->isFormPost()) { - $name = $request->getStr('name'); - if (!strlen($name)) { - $e_name = pht('Required'); - $errors[] = pht('You must provide a property name.'); - } else { - $caught = null; - try { - AlmanacNames::validateName($name); - } catch (Exception $ex) { - $caught = $ex; - } - if ($caught) { - $e_name = pht('Invalid'); - $errors[] = $caught->getMessage(); - } - } - - if (!$errors) { - $property_key = $name; - } - } - - if ($property_key === null) { - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormTextControl()) - ->setName('name') - ->setLabel(pht('Name')) - ->setValue($v_name) - ->setError($e_name)); - - return $this->newDialog() - ->setTitle($title) - ->setErrors($errors) - ->addHiddenInput('objectPHID', $request->getStr('objectPHID')) - ->appendForm($form) - ->addSubmitButton(pht('Continue')) - ->addCancelButton($cancel_uri); - } - } - - // Make sure property key is appropriate. - // TODO: It would be cleaner to put this safety check in the Editor. - AlmanacNames::validateName($property_key); - - // If we're adding a new property, put a placeholder on the object so - // that we can build a CustomField for it. - if (!$object->hasAlmanacProperty($property_key)) { - $temporary_property = id(new AlmanacProperty()) - ->setObjectPHID($object->getPHID()) - ->setFieldName($property_key); - - $object->attachAlmanacProperties(array($temporary_property)); - } - - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_DEFAULT); - - // Select only the field being edited. - $fields = $field_list->getFields(); - $fields = array_select_keys($fields, array($property_key)); - $field_list = new PhabricatorCustomFieldList($fields); - - $field_list - ->setViewer($viewer) - ->readFieldsFromStorage($object); - - $validation_exception = null; - if ($request->isFormPost() && $request->getStr('isValueEdit')) { - $xactions = $field_list->buildFieldTransactionsFromRequest( - $object->getApplicationTransactionTemplate(), - $request); - - $editor = $object->getApplicationTransactionEditor() - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); - + $error = null; try { - $editor->applyTransactions($object, $xactions); - return id(new AphrontRedirectResponse())->setURI($cancel_uri); - } catch (PhabricatorApplicationTransactionValidationException $ex) { - $validation_exception = $ex; + AlmanacNames::validateName($property_key); + } catch (Exception $ex) { + $error = $ex->getMessage(); } + + // NOTE: If you enter an existing name, we're just treating it as an + // edit operation. This might be a little confusing. + + if ($error !== null) { + if ($request->isFormPost()) { + // The user is creating a new property and picked a bad name. Give + // them an opportunity to fix it. + return $this->buildPropertyKeyResponse($cancel_uri, $error); + } else { + // The user is editing an invalid property. + return $this->newDialog() + ->setTitle(pht('Invalid Property')) + ->appendParagraph( + pht( + 'The property name "%s" is invalid. This property can not '. + 'be edited.', + $property_key)) + ->appendParagraph($error) + ->addCancelButton($cancel_uri); + } + } + } + + return $object->newAlmanacPropertyEditEngine() + ->addContextParameter('objectPHID') + ->addContextParameter('key') + ->setTargetObject($object) + ->setPropertyKey($property_key) + ->setController($this) + ->buildResponse(); + } + + private function buildPropertyKeyResponse($cancel_uri, $error) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + $v_key = $request->getStr('key'); + + if ($error !== null) { + $e_key = pht('Invalid'); + } else { + $e_key = true; } $form = id(new AphrontFormView()) ->setUser($viewer) - ->addHiddenInput('objectPHID', $request->getStr('objectPHID')) - ->addHiddenInput('key', $request->getStr('key')) - ->addHiddenInput('name', $property_key) - ->addHiddenInput('isValueEdit', true); + ->appendChild( + id(new AphrontFormTextControl()) + ->setName('key') + ->setLabel(pht('Name')) + ->setValue($v_key) + ->setError($e_key)); - $field_list->appendFieldsToForm($form); + $errors = array(); + if ($error !== null) { + $errors[] = $error; + } return $this->newDialog() - ->setTitle($title) - ->setValidationException($validation_exception) + ->setTitle(pht('Add Property')) + ->addHiddenInput('objectPHID', $request->getStr('objectPHID')) + ->setErrors($errors) ->appendForm($form) - ->addSubmitButton($save_button) + ->addSubmitButton(pht('Continue')) ->addCancelButton($cancel_uri); } diff --git a/src/applications/almanac/controller/AlmanacServiceController.php b/src/applications/almanac/controller/AlmanacServiceController.php index daf05f0692..12bfbf94ea 100644 --- a/src/applications/almanac/controller/AlmanacServiceController.php +++ b/src/applications/almanac/controller/AlmanacServiceController.php @@ -11,4 +11,14 @@ abstract class AlmanacServiceController extends AlmanacController { return $crumbs; } + protected function getPropertyDeleteURI($object) { + $id = $object->getID(); + return "/almanac/service/delete/{$id}/"; + } + + protected function getPropertyUpdateURI($object) { + $id = $object->getID(); + return "/almanac/service/property/{$id}/"; + } + } diff --git a/src/applications/almanac/customfield/AlmanacCoreCustomField.php b/src/applications/almanac/customfield/AlmanacCoreCustomField.php deleted file mode 100644 index 91560f14e6..0000000000 --- a/src/applications/almanac/customfield/AlmanacCoreCustomField.php +++ /dev/null @@ -1,80 +0,0 @@ -getProxy()->getRawStandardFieldKey(); - } - - public function getFieldName() { - return $this->getFieldKey(); - } - - public function createFields($object) { - if (!$object->getID()) { - return array(); - } - - $specs = $object->getAlmanacPropertyFieldSpecifications(); - - $default_specs = array(); - foreach ($object->getAlmanacProperties() as $property) { - $default_specs[$property->getFieldName()] = array( - 'name' => $property->getFieldName(), - 'type' => 'text', - ); - } - - return PhabricatorStandardCustomField::buildStandardFields( - $this, - $specs + $default_specs); - } - - public function shouldUseStorage() { - return false; - } - - public function readValueFromObject(PhabricatorCustomFieldInterface $object) { - $key = $this->getFieldKey(); - - if ($object->hasAlmanacProperty($key)) { - $this->setValueFromStorage($object->getAlmanacPropertyValue($key)); - } - } - - public function applyApplicationTransactionInternalEffects( - PhabricatorApplicationTransaction $xaction) { - return; - } - - public function applyApplicationTransactionExternalEffects( - PhabricatorApplicationTransaction $xaction) { - - $object = $this->getObject(); - $phid = $object->getPHID(); - $key = $this->getFieldKey(); - - $property = id(new AlmanacPropertyQuery()) - ->setViewer($this->getViewer()) - ->withObjectPHIDs(array($phid)) - ->withNames(array($key)) - ->executeOne(); - if (!$property) { - $property = id(new AlmanacProperty()) - ->setObjectPHID($phid) - ->setFieldIndex(PhabricatorHash::digestForIndex($key)) - ->setFieldName($key); - } - - $property - ->setFieldValue($xaction->getNewValue()) - ->save(); - } - -} diff --git a/src/applications/almanac/customfield/AlmanacCustomField.php b/src/applications/almanac/customfield/AlmanacCustomField.php deleted file mode 100644 index 6aefc35d2d..0000000000 --- a/src/applications/almanac/customfield/AlmanacCustomField.php +++ /dev/null @@ -1,4 +0,0 @@ -getURI(); + } + +} diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php index e76cc338b5..90928b1d7a 100644 --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -1,20 +1,12 @@ getURI(); + } + +} diff --git a/src/applications/almanac/editor/AlmanacEditor.php b/src/applications/almanac/editor/AlmanacEditor.php new file mode 100644 index 0000000000..a628606dbc --- /dev/null +++ b/src/applications/almanac/editor/AlmanacEditor.php @@ -0,0 +1,156 @@ +getTransactionType()) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + $property_key = $xaction->getMetadataValue('almanac.property'); + $exists = $object->hasAlmanacProperty($property_key); + $value = $object->getAlmanacPropertyValue($property_key); + return array( + 'existed' => $exists, + 'value' => $value, + ); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + $property_key = $xaction->getMetadataValue('almanac.property'); + if ($object->hasAlmanacProperty($property_key)) { + $property = $object->getAlmanacProperty($property_key); + } else { + $property = id(new AlmanacProperty()) + ->setObjectPHID($object->getPHID()) + ->setFieldName($property_key); + } + $property + ->setFieldValue($xaction->getNewValue()) + ->save(); + return; + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + $property_key = $xaction->getMetadataValue('almanac.property'); + if ($object->hasAlmanacProperty($property_key)) { + $property = $object->getAlmanacProperty($property_key); + $property->delete(); + } + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case AlmanacTransaction::TYPE_PROPERTY_UPDATE: + foreach ($xactions as $xaction) { + $property_key = $xaction->getMetadataValue('almanac.property'); + + $message = null; + try { + AlmanacNames::validateName($property_key); + } catch (Exception $ex) { + $message = $ex->getMessage(); + } + + if ($message !== null) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $message, + $xaction); + $errors[] = $error; + continue; + } + + $new_value = $xaction->getNewValue(); + try { + phutil_json_encode($new_value); + } catch (Exception $ex) { + $message = pht( + 'Almanac property values must be representable in JSON. %s', + $ex->getMessage()); + } + + if ($message !== null) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $message, + $xaction); + $errors[] = $error; + continue; + } + } + break; + + case AlmanacTransaction::TYPE_PROPERTY_REMOVE: + // NOTE: No name validation on removals since it's OK to delete + // an invalid property that somehow came into existence. + break; + } + + return $errors; + } + +} diff --git a/src/applications/almanac/editor/AlmanacPropertyEditEngine.php b/src/applications/almanac/editor/AlmanacPropertyEditEngine.php new file mode 100644 index 0000000000..c22a3ab869 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacPropertyEditEngine.php @@ -0,0 +1,79 @@ +propertyKey = $property_key; + return $this; + } + + public function getPropertyKey() { + return $this->propertyKey; + } + + public function isEngineConfigurable() { + return false; + } + + public function isEngineExtensible() { + return false; + } + + public function getEngineName() { + return pht('Almanac Properties'); + } + + public function getSummaryHeader() { + return pht('Edit Almanac Property Configurations'); + } + + public function getSummaryText() { + return pht('This engine is used to edit Almanac properties.'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorAlmanacApplication'; + } + + protected function newEditableObject() { + throw new PhutilMethodNotImplementedException(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create Property'); + } + + protected function getObjectCreateButtonText($object) { + return pht('Create Property'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Property: %s', $object->getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Property'); + } + + protected function getObjectCreateShortText() { + return pht('Create Property'); + } + + protected function buildCustomEditFields($object) { + $property_key = $this->getPropertyKey(); + $xaction_type = AlmanacTransaction::TYPE_PROPERTY_UPDATE; + + return array( + id(new PhabricatorTextEditField()) + ->setKey('value') + ->setMetadataValue('almanac.property', $property_key) + ->setLabel($property_key) + ->setTransactionType($xaction_type) + ->setValue($object->getAlmanacPropertyValue($property_key)), + ); + } + +} diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php index b8013b5261..8b8d2d2d67 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -1,20 +1,12 @@ getURI(); + } + +} diff --git a/src/applications/almanac/property/AlmanacPropertyInterface.php b/src/applications/almanac/property/AlmanacPropertyInterface.php index 9a2bf11455..b50b4089a8 100644 --- a/src/applications/almanac/property/AlmanacPropertyInterface.php +++ b/src/applications/almanac/property/AlmanacPropertyInterface.php @@ -8,5 +8,6 @@ interface AlmanacPropertyInterface { public function getAlmanacProperty($key); public function getAlmanacPropertyValue($key, $default = null); public function getAlmanacPropertyFieldSpecifications(); + public function newAlmanacPropertyEditEngine(); } diff --git a/src/applications/almanac/query/AlmanacQuery.php b/src/applications/almanac/query/AlmanacQuery.php index 54274dfe72..af28e60e30 100644 --- a/src/applications/almanac/query/AlmanacQuery.php +++ b/src/applications/almanac/query/AlmanacQuery.php @@ -5,9 +5,8 @@ abstract class AlmanacQuery protected function didFilterPage(array $objects) { if (head($objects) instanceof AlmanacPropertyInterface) { - // NOTE: We load properties unconditionally because CustomField assumes - // it can always generate a list of fields on an object. It may make - // sense to re-examine that assumption eventually. + // NOTE: We load properties for obsolete historical reasons. It may make + // sense to re-examine that assumption shortly. $property_query = id(new AlmanacPropertyQuery()) ->setViewer($this->getViewer()) @@ -25,9 +24,23 @@ abstract class AlmanacQuery $properties = mgroup($properties, 'getObjectPHID'); foreach ($objects as $object) { $object_properties = idx($properties, $object->getPHID(), array()); + $object_properties = mpull($object_properties, null, 'getFieldName'); + + // Create synthetic properties for defaults on the object itself. + $specs = $object->getAlmanacPropertyFieldSpecifications(); + foreach ($specs as $key => $spec) { + if (empty($object_properties[$key])) { + $object_properties[$key] = id(new AlmanacProperty()) + ->setObjectPHID($object->getPHID()) + ->setFieldName($key) + ->setFieldValue($spec->getValueForTransaction()); + } + } + foreach ($object_properties as $property) { $property->attachObject($object); } + $object->attachAlmanacProperties($object_properties); } } diff --git a/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php index 90e23dc8e0..bbf7e9878c 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php @@ -16,8 +16,4 @@ final class AlmanacClusterDatabaseServiceType 'Defines a database service for use in a Phabricator cluster.'); } - public function getFieldSpecifications() { - return array(); - } - } diff --git a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php index e4dc3c8afc..d33ee7167b 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php @@ -18,16 +18,7 @@ final class AlmanacClusterRepositoryServiceType public function getFieldSpecifications() { return array( - 'closed' => array( - 'type' => 'bool', - 'name' => pht('Closed'), - 'default' => false, - 'strings' => array( - 'edit.checkbox' => pht( - 'Prevent new repositories from being allocated on this '. - 'service.'), - ), - ), + 'closed' => id(new PhabricatorTextEditField()), ); } diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 8f56c86c1a..0d9cfa6fa3 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -4,7 +4,6 @@ final class AlmanacBinding extends AlmanacDAO implements PhabricatorPolicyInterface, - PhabricatorCustomFieldInterface, PhabricatorApplicationTransactionInterface, AlmanacPropertyInterface, PhabricatorDestructibleInterface { @@ -17,7 +16,6 @@ final class AlmanacBinding private $service = self::ATTACHABLE; private $device = self::ATTACHABLE; private $interface = self::ATTACHABLE; - private $customFields = self::ATTACHABLE; private $almanacProperties = self::ATTACHABLE; public static function initializeNewBinding(AlmanacService $service) { @@ -58,6 +56,10 @@ final class AlmanacBinding return parent::save(); } + public function getName() { + return pht('Binding %s', $this->getID()); + } + public function getURI() { return '/almanac/binding/'.$this->getID().'/'; } @@ -124,6 +126,10 @@ final class AlmanacBinding return array(); } + public function newAlmanacPropertyEditEngine() { + return new AlmanacBindingPropertyEditEngine(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -162,27 +168,6 @@ final class AlmanacBinding } -/* -( PhabricatorCustomFieldInterface )------------------------------------ */ - - - public function getCustomFieldSpecificationForRole($role) { - return array(); - } - - public function getCustomFieldBaseClass() { - return 'AlmanacCustomField'; - } - - public function getCustomFields() { - return $this->assertAttached($this->customFields); - } - - public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { - $this->customFields = $fields; - return $this; - } - - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacBindingTransaction.php b/src/applications/almanac/storage/AlmanacBindingTransaction.php index 4ea4909bb6..bc9f071928 100644 --- a/src/applications/almanac/storage/AlmanacBindingTransaction.php +++ b/src/applications/almanac/storage/AlmanacBindingTransaction.php @@ -1,7 +1,7 @@ assertAttached($this->customFields); - } - - public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { - $this->customFields = $fields; - return $this; - } - - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacDeviceTransaction.php b/src/applications/almanac/storage/AlmanacDeviceTransaction.php index b80b74af18..5f42ec0210 100644 --- a/src/applications/almanac/storage/AlmanacDeviceTransaction.php +++ b/src/applications/almanac/storage/AlmanacDeviceTransaction.php @@ -1,7 +1,7 @@ assertAttached($this->customFields); - } - - public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { - $this->customFields = $fields; - return $this; - } - - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacProperty.php b/src/applications/almanac/storage/AlmanacProperty.php index 9692143d10..e695da59ec 100644 --- a/src/applications/almanac/storage/AlmanacProperty.php +++ b/src/applications/almanac/storage/AlmanacProperty.php @@ -1,25 +1,33 @@ 'text128', - ); - - return $config; + return array( + self::CONFIG_TIMESTAMPS => false, + self::CONFIG_SERIALIZATION => array( + 'fieldValue' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'fieldIndex' => 'bytes12', + 'fieldName' => 'text128', + ), + self::CONFIG_KEY_SCHEMA => array( + 'objectPHID' => array( + 'columns' => array('objectPHID', 'fieldIndex'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); } public function getObject() { @@ -31,37 +39,11 @@ final class AlmanacProperty return $this; } - public static function buildTransactions( - AlmanacPropertyInterface $object, - array $properties) { + public function save() { + $hash = PhabricatorHash::digestForIndex($this->getFieldName()); + $this->setFieldIndex($hash); - $template = $object->getApplicationTransactionTemplate(); - - $attached_properties = $object->getAlmanacProperties(); - foreach ($properties as $key => $value) { - if (empty($attached_properties[$key])) { - $attached_properties[] = id(new AlmanacProperty()) - ->setObjectPHID($object->getPHID()) - ->setFieldName($key); - } - } - $object->attachAlmanacProperties($attached_properties); - - $field_list = PhabricatorCustomField::getObjectFields( - $object, - PhabricatorCustomField::ROLE_DEFAULT); - $fields = $field_list->getFields(); - - $xactions = array(); - foreach ($properties as $name => $property) { - $xactions[] = id(clone $template) - ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) - ->setMetadataValue('customfield:key', $name) - ->setOldValue($object->getAlmanacPropertyValue($name)) - ->setNewValue($property); - } - - return $xactions; + return parent::save(); } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 765ae58278..39c8c56e60 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -4,7 +4,6 @@ final class AlmanacService extends AlmanacDAO implements PhabricatorPolicyInterface, - PhabricatorCustomFieldInterface, PhabricatorApplicationTransactionInterface, PhabricatorProjectInterface, AlmanacPropertyInterface, @@ -19,7 +18,6 @@ final class AlmanacService protected $serviceClass; protected $isLocked; - private $customFields = self::ATTACHABLE; private $almanacProperties = self::ATTACHABLE; private $bindings = self::ATTACHABLE; private $serviceType = self::ATTACHABLE; @@ -130,6 +128,10 @@ final class AlmanacService return $this->getServiceType()->getFieldSpecifications(); } + public function newAlmanacPropertyEditEngine() { + return new AlmanacServicePropertyEditEngine(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -171,27 +173,6 @@ final class AlmanacService } -/* -( PhabricatorCustomFieldInterface )------------------------------------ */ - - - public function getCustomFieldSpecificationForRole($role) { - return array(); - } - - public function getCustomFieldBaseClass() { - return 'AlmanacCustomField'; - } - - public function getCustomFields() { - return $this->assertAttached($this->customFields); - } - - public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { - $this->customFields = $fields; - return $this; - } - - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacServiceTransaction.php b/src/applications/almanac/storage/AlmanacServiceTransaction.php index 2b48ea5393..a668f860fa 100644 --- a/src/applications/almanac/storage/AlmanacServiceTransaction.php +++ b/src/applications/almanac/storage/AlmanacServiceTransaction.php @@ -1,23 +1,15 @@ getAuthorPHID(); diff --git a/src/applications/almanac/storage/AlmanacTransaction.php b/src/applications/almanac/storage/AlmanacTransaction.php new file mode 100644 index 0000000000..18bd776335 --- /dev/null +++ b/src/applications/almanac/storage/AlmanacTransaction.php @@ -0,0 +1,38 @@ +getAuthorPHID(); + + switch ($this->getTransactionType()) { + case self::TYPE_PROPERTY_UPDATE: + $property_key = $this->getMetadataValue('almanac.property'); + return pht( + '%s updated the property "%s".', + $this->renderHandleLink($author_phid), + $property_key); + case self::TYPE_PROPERTY_REMOVE: + $property_key = $this->getMetadataValue('almanac.property'); + return pht( + '%s deleted the property "%s".', + $this->renderHandleLink($author_phid), + $property_key); + } + + return parent::getTitle(); + } + +} diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index f8dc0ee175..ce7ec1c437 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -23,6 +23,7 @@ abstract class PhabricatorEditEngine private $isCreate; private $editEngineConfiguration; private $contextParameters = array(); + private $targetObject; final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -61,6 +62,22 @@ abstract class PhabricatorEditEngine return true; } + public function isEngineExtensible() { + return true; + } + + /** + * Force the engine to edit a particular object. + */ + public function setTargetObject($target_object) { + $this->targetObject = $target_object; + return $this; + } + + public function getTargetObject() { + return $this->targetObject; + } + /* -( Managing Fields )---------------------------------------------------- */ @@ -94,7 +111,12 @@ abstract class PhabricatorEditEngine $fields = mpull($fields, null, 'getKey'); - $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions(); + if ($this->isEngineExtensible()) { + $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions(); + } else { + $extensions = array(); + } + foreach ($extensions as $extension) { $extension->setViewer($viewer); @@ -720,23 +742,28 @@ abstract class PhabricatorEditEngine break; } - $id = $request->getURIData('id'); + $object = $this->getTargetObject(); + if (!$object) { + $id = $request->getURIData('id'); - if ($id) { - $this->setIsCreate(false); - $object = $this->newObjectFromID($id, $capabilities); - if (!$object) { - return new Aphront404Response(); + if ($id) { + $this->setIsCreate(false); + $object = $this->newObjectFromID($id, $capabilities); + if (!$object) { + return new Aphront404Response(); + } + } else { + // Make sure the viewer has permission to create new objects of + // this type if we're going to create a new object. + if ($require_create) { + $this->requireCreateCapability(); + } + + $this->setIsCreate(true); + $object = $this->newEditableObject(); } } else { - // Make sure the viewer has permission to create new objects of - // this type if we're going to create a new object. - if ($require_create) { - $this->requireCreateCapability(); - } - - $this->setIsCreate(true); - $object = $this->newEditableObject(); + $id = $object->getID(); } $this->validateObject($object); @@ -831,7 +858,7 @@ abstract class PhabricatorEditEngine $template = $object->getApplicationTransactionTemplate(); $validation_exception = null; - if ($request->isFormPost()) { + if ($request->isFormPost() && $request->getBool('editEngine')) { $submit_fields = $fields; foreach ($submit_fields as $key => $field) { @@ -1044,7 +1071,8 @@ abstract class PhabricatorEditEngine $request = $controller->getRequest(); $form = id(new AphrontFormView()) - ->setUser($viewer); + ->setUser($viewer) + ->addHiddenInput('editEngine', 'true'); foreach ($this->contextParameters as $param) { $form->addHiddenInput($param, $request->getStr($param)); diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 7d50f8495c..8084ea302f 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -456,8 +456,14 @@ abstract class PhabricatorApplicationTransaction return null; } + $object = $this->getObject(); + + if (!($object instanceof PhabricatorCustomFieldInterface)) { + return null; + } + $field = PhabricatorCustomField::getObjectField( - $this->getObject(), + $object, PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $key); if (!$field) { From 24104be67d1ad804ccc80b7ffc3a5ddba4ba7a3a Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 22 Feb 2016 05:11:02 -0800 Subject: [PATCH 11/31] Use EditEngine for Almanac Services, Devices, and Networks Summary: Ref T10411. This cleans up / modernizes things and lets me get an `almanac.network.edit` API in the future. This is mostly straightforward, except that Services have an extra "choose type" screen in front of them. Test Plan: - Created and edited Almanac networks, services, and devices. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10411 Differential Revision: https://secure.phabricator.com/D15326 --- src/__phutil_library_map__.php | 4 +- .../AlmanacNamespaceEditController.php | 2 +- .../AlmanacNetworkEditController.php | 139 +----------------- .../editor/AlmanacNetworkEditEngine.php | 86 +++++++++++ .../storage/AlmanacNetworkTransaction.php | 21 ++- 5 files changed, 103 insertions(+), 149 deletions(-) create mode 100644 src/applications/almanac/editor/AlmanacNetworkEditEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e4196425e1..29c34e45cc 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -79,6 +79,7 @@ phutil_register_library_map(array( 'AlmanacNetwork' => 'applications/almanac/storage/AlmanacNetwork.php', 'AlmanacNetworkController' => 'applications/almanac/controller/AlmanacNetworkController.php', 'AlmanacNetworkEditController' => 'applications/almanac/controller/AlmanacNetworkEditController.php', + 'AlmanacNetworkEditEngine' => 'applications/almanac/editor/AlmanacNetworkEditEngine.php', 'AlmanacNetworkEditor' => 'applications/almanac/editor/AlmanacNetworkEditor.php', 'AlmanacNetworkListController' => 'applications/almanac/controller/AlmanacNetworkListController.php', 'AlmanacNetworkNameNgrams' => 'applications/almanac/storage/AlmanacNetworkNameNgrams.php', @@ -4072,7 +4073,7 @@ phutil_register_library_map(array( 'PhabricatorNgramsInterface', ), 'AlmanacNamespaceController' => 'AlmanacController', - 'AlmanacNamespaceEditController' => 'AlmanacController', + 'AlmanacNamespaceEditController' => 'AlmanacNamespaceController', 'AlmanacNamespaceEditEngine' => 'PhabricatorEditEngine', 'AlmanacNamespaceEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacNamespaceListController' => 'AlmanacNamespaceController', @@ -4092,6 +4093,7 @@ phutil_register_library_map(array( ), 'AlmanacNetworkController' => 'AlmanacController', 'AlmanacNetworkEditController' => 'AlmanacNetworkController', + 'AlmanacNetworkEditEngine' => 'PhabricatorEditEngine', 'AlmanacNetworkEditor' => 'PhabricatorApplicationTransactionEditor', 'AlmanacNetworkListController' => 'AlmanacNetworkController', 'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams', diff --git a/src/applications/almanac/controller/AlmanacNamespaceEditController.php b/src/applications/almanac/controller/AlmanacNamespaceEditController.php index 1f3cf84e52..0bb6ec2620 100644 --- a/src/applications/almanac/controller/AlmanacNamespaceEditController.php +++ b/src/applications/almanac/controller/AlmanacNamespaceEditController.php @@ -1,6 +1,6 @@ getViewer(); - - $list_uri = $this->getApplicationURI('network/'); - - $id = $request->getURIData('id'); - if ($id) { - $network = id(new AlmanacNetworkQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$network) { - return new Aphront404Response(); - } - - $is_new = false; - $network_uri = $this->getApplicationURI('network/'.$network->getID().'/'); - $cancel_uri = $network_uri; - $title = pht('Edit Network'); - $save_button = pht('Save Changes'); - } else { - $this->requireApplicationCapability( - AlmanacCreateNetworksCapability::CAPABILITY); - - $network = AlmanacNetwork::initializeNewNetwork(); - $is_new = true; - - $cancel_uri = $list_uri; - $title = pht('Create Network'); - $save_button = pht('Create Network'); - } - - $v_name = $network->getName(); - $e_name = true; - $validation_exception = null; - - if ($request->isFormPost()) { - $v_name = $request->getStr('name'); - $v_view = $request->getStr('viewPolicy'); - $v_edit = $request->getStr('editPolicy'); - - $type_name = AlmanacNetworkTransaction::TYPE_NAME; - $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; - $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; - - $xactions = array(); - - $xactions[] = id(new AlmanacNetworkTransaction()) - ->setTransactionType($type_name) - ->setNewValue($v_name); - - $xactions[] = id(new AlmanacNetworkTransaction()) - ->setTransactionType($type_view) - ->setNewValue($v_view); - - $xactions[] = id(new AlmanacNetworkTransaction()) - ->setTransactionType($type_edit) - ->setNewValue($v_edit); - - $editor = id(new AlmanacNetworkEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true); - - try { - $editor->applyTransactions($network, $xactions); - - $id = $network->getID(); - $network_uri = $this->getApplicationURI("network/{$id}/"); - return id(new AphrontRedirectResponse())->setURI($network_uri); - } catch (PhabricatorApplicationTransactionValidationException $ex) { - $validation_exception = $ex; - $e_name = $ex->getShortMessage($type_name); - - $network->setViewPolicy($v_view); - $network->setEditPolicy($v_edit); - } - } - - $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($viewer) - ->setObject($network) - ->execute(); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Name')) - ->setName('name') - ->setValue($v_name) - ->setError($e_name)) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setName('viewPolicy') - ->setPolicyObject($network) - ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) - ->setPolicies($policies)) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setName('editPolicy') - ->setPolicyObject($network) - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) - ->setPolicies($policies)) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton($cancel_uri) - ->setValue($save_button)); - - $box = id(new PHUIObjectBoxView()) - ->setValidationException($validation_exception) - ->setHeaderText($title) - ->setForm($form); - - $crumbs = $this->buildApplicationCrumbs(); - if ($is_new) { - $crumbs->addTextCrumb(pht('Create Network')); - } else { - $crumbs->addTextCrumb($network->getName(), $network_uri); - $crumbs->addTextCrumb(pht('Edit')); - } - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild( - array( - $box, - )); + return id(new AlmanacNetworkEditEngine()) + ->setController($this) + ->buildResponse(); } } diff --git a/src/applications/almanac/editor/AlmanacNetworkEditEngine.php b/src/applications/almanac/editor/AlmanacNetworkEditEngine.php new file mode 100644 index 0000000000..a99079ff11 --- /dev/null +++ b/src/applications/almanac/editor/AlmanacNetworkEditEngine.php @@ -0,0 +1,86 @@ +getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Network'); + } + + protected function getObjectCreateShortText() { + return pht('Create Network'); + } + + protected function getEditorURI() { + return '/almanac/network/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/almanac/network/'; + } + + protected function getObjectViewURI($object) { + $id = $object->getID(); + return "/almanac/network/{$id}/"; + } + + protected function getCreateNewObjectPolicy() { + return $this->getApplication()->getPolicy( + AlmanacCreateNetworksCapability::CAPABILITY); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the network.')) + ->setTransactionType(AlmanacNetworkTransaction::TYPE_NAME) + ->setIsRequired(true) + ->setValue($object->getName()), + ); + } + +} diff --git a/src/applications/almanac/storage/AlmanacNetworkTransaction.php b/src/applications/almanac/storage/AlmanacNetworkTransaction.php index 7d2f81c984..f8c2b43b0b 100644 --- a/src/applications/almanac/storage/AlmanacNetworkTransaction.php +++ b/src/applications/almanac/storage/AlmanacNetworkTransaction.php @@ -24,19 +24,16 @@ final class AlmanacNetworkTransaction $new = $this->getNewValue(); switch ($this->getTransactionType()) { + case PhabricatorTransactions::TYPE_CREATE: + return pht( + '%s created this network.', + $this->renderHandleLink($author_phid)); case self::TYPE_NAME: - if ($old === null) { - return pht( - '%s created this network.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s renamed this network from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - break; + return pht( + '%s renamed this network from "%s" to "%s".', + $this->renderHandleLink($author_phid), + $old, + $new); } return parent::getTitle(); From 023cfbb23af36b7f15be86a1f5950b62d68b246d Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 22 Feb 2016 11:48:44 -0800 Subject: [PATCH 12/31] Restrict PropertyListView width to just DocumentProView Summary: Fixes T10409. Long term need to build a proper "PageEngine" of sorts for layouts not needing special magic. For now this just affects a few applications. Test Plan: View Diffusion, Phriction, Phame, Legalpad, Diviner. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10409 Differential Revision: https://secure.phabricator.com/D15328 --- resources/celerity/map.php | 8 ++++---- .../diviner/controller/DivinerAtomController.php | 2 ++ .../controller/post/PhamePostViewController.php | 1 + .../controller/PhrictionDocumentController.php | 1 + webroot/rsrc/css/application/phame/phame.css | 3 +-- webroot/rsrc/css/phui/phui-document-pro.css | 13 ++++++++++--- 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b632ba7d1e..7e039dde0c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -81,7 +81,7 @@ return array( 'rsrc/css/application/owners/owners-path-editor.css' => '2f00933b', 'rsrc/css/application/paste/paste.css' => 'a5157c48', 'rsrc/css/application/people/people-profile.css' => '2473d929', - 'rsrc/css/application/phame/phame.css' => '4ca6fd6c', + 'rsrc/css/application/phame/phame.css' => '737792d6', 'rsrc/css/application/pholio/pholio-edit.css' => '3ad9d1ee', 'rsrc/css/application/pholio/pholio-inline-comments.css' => '8e545e49', 'rsrc/css/application/pholio/pholio.css' => 'ca89d380', @@ -127,7 +127,7 @@ return array( 'rsrc/css/phui/phui-button.css' => 'edf464e9', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-crumbs-view.css' => '79d536e5', - 'rsrc/css/phui/phui-document-pro.css' => 'a8872307', + 'rsrc/css/phui/phui-document-pro.css' => '92d5b648', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => '9c71d2bf', 'rsrc/css/phui/phui-feed-story.css' => '04aec08f', @@ -790,7 +790,7 @@ return array( 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', 'phabricator-zindex-css' => '5b6fcf3f', - 'phame-css' => '4ca6fd6c', + 'phame-css' => '737792d6', 'pholio-css' => 'ca89d380', 'pholio-edit-css' => '3ad9d1ee', 'pholio-inline-comments-css' => '8e545e49', @@ -812,7 +812,7 @@ return array( 'phui-crumbs-view-css' => '79d536e5', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => '9c71d2bf', - 'phui-document-view-pro-css' => 'a8872307', + 'phui-document-view-pro-css' => '92d5b648', 'phui-feed-story-css' => '04aec08f', 'phui-font-icon-base-css' => 'ecbbb4c2', 'phui-fontkit-css' => '9cda225e', diff --git a/src/applications/diviner/controller/DivinerAtomController.php b/src/applications/diviner/controller/DivinerAtomController.php index 46506ff317..1785ae6741 100644 --- a/src/applications/diviner/controller/DivinerAtomController.php +++ b/src/applications/diviner/controller/DivinerAtomController.php @@ -238,6 +238,8 @@ final class DivinerAtomController extends DivinerController { $document->setToc($side); } + $prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list); + return $this->buildApplicationPage( array( $crumbs, diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index a4231cc1ac..5579e82656 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -136,6 +136,7 @@ final class PhamePostViewController $document->setFoot($next_view); $crumbs = $this->buildApplicationCrumbs(); + $properties = phutil_tag_div('phui-document-view-pro-box', $properties); $page = $this->newPage() ->setTitle($post->getTitle()) diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index 7dae0fad03..360c0fbf3f 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -218,6 +218,7 @@ final class PhrictionDocumentController $prop_list = new PHUIPropertyGroupView(); $prop_list->addPropertyList($properties); } + $prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list); $page_content = id(new PHUIDocumentViewPro()) ->setHeader($header) diff --git a/webroot/rsrc/css/application/phame/phame.css b/webroot/rsrc/css/application/phame/phame.css index 299474fee1..d4fdeafff0 100644 --- a/webroot/rsrc/css/application/phame/phame.css +++ b/webroot/rsrc/css/application/phame/phame.css @@ -30,9 +30,8 @@ position: absolute; } -.phame-blog-description + .phui-property-list-section { +.phame-blog-description + .phui-document-view-pro-box { border-top: 1px solid rgba({$alphablue}, 0.20); - padding-top: 16px; } .phame-home-view .phui-document-view.phui-document-view-pro { diff --git a/webroot/rsrc/css/phui/phui-document-pro.css b/webroot/rsrc/css/phui/phui-document-pro.css index 138eec0955..c77ffa77e2 100644 --- a/webroot/rsrc/css/phui/phui-document-pro.css +++ b/webroot/rsrc/css/phui/phui-document-pro.css @@ -20,15 +20,22 @@ margin: 0 auto; } -.phui-property-list-section { - max-width: 800px; +.device .phui-document-view-pro-box { + margin: 0 8px; +} + +.phui-document-view-pro-box .phui-property-list-section { margin: 16px auto; } -.device .phui-property-list-section { +.device .phui-document-view-pro-box .phui-property-list-section { margin: 0 8px 16px; } +.device .phui-document-view-pro-box .phui-property-list-container { + padding: 24px 0 0 0; +} + .device-phone .phui-document-view.phui-document-view-pro { padding: 0 8px; margin: 0 auto; From f7d5904e4b53a17e44f79c760f10a8d87af00093 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 22 Feb 2016 12:53:40 -0800 Subject: [PATCH 13/31] Expose modern `*.search` Conduit endpoints in Almanac Summary: Fixes T10411. Ref T10246. There are probably still some rough edges with this, but replace the old-school endpoints with modern ones so we don't unprototype with deprecated stuff. Test Plan: - Made a bunch of calls to the new endpoints with various constraints/attachments. - Created and edited services, devices, interfaces, bindings, and properties on everything. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10246, T10411 Differential Revision: https://secure.phabricator.com/D15329 --- src/__phutil_library_map__.php | 20 ++++-- .../conduit/AlmanacConduitAPIMethod.php | 2 +- .../AlmanacDeviceSearchConduitAPIMethod.php | 18 +++++ .../AlmanacServiceSearchConduitAPIMethod.php | 18 +++++ .../AlmanacBindingViewController.php | 1 + .../AlmanacDeviceViewController.php | 1 + .../controller/AlmanacPropertyController.php | 55 +++++++++++++- .../AlmanacPropertyDeleteController.php | 20 ++---- .../AlmanacPropertyEditController.php | 20 ++---- .../AlmanacServiceViewController.php | 1 + .../AlmanacBindingsSearchEngineAttachment.php | 30 ++++++++ ...lmanacPropertiesSearchEngineAttachment.php | 26 +++++++ .../AlmanacSearchEngineAttachment.php | 64 +++++++++++++++++ .../almanac/query/AlmanacBindingQuery.php | 38 +++++----- .../query/AlmanacDeviceSearchEngine.php | 8 +++ .../almanac/query/AlmanacInterfaceQuery.php | 4 +- .../almanac/query/AlmanacNetworkQuery.php | 2 +- .../almanac/query/AlmanacPropertyQuery.php | 72 +++++++++---------- .../almanac/query/AlmanacQuery.php | 26 ++++--- .../almanac/query/AlmanacServiceQuery.php | 7 +- .../query/AlmanacServiceSearchEngine.php | 17 +++++ .../almanac/storage/AlmanacDevice.php | 29 +++++++- .../almanac/storage/AlmanacService.php | 31 +++++++- .../field/PhabricatorSearchTextField.php | 4 ++ 24 files changed, 403 insertions(+), 111 deletions(-) create mode 100644 src/applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php create mode 100644 src/applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php create mode 100644 src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php create mode 100644 src/applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php create mode 100644 src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 29c34e45cc..4de280e4f9 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -20,6 +20,7 @@ phutil_register_library_map(array( 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', 'AlmanacBindingTransactionQuery' => 'applications/almanac/query/AlmanacBindingTransactionQuery.php', 'AlmanacBindingViewController' => 'applications/almanac/controller/AlmanacBindingViewController.php', + 'AlmanacBindingsSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php', 'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php', 'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php', 'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php', @@ -42,6 +43,7 @@ phutil_register_library_map(array( 'AlmanacDevicePHIDType' => 'applications/almanac/phid/AlmanacDevicePHIDType.php', 'AlmanacDevicePropertyEditEngine' => 'applications/almanac/editor/AlmanacDevicePropertyEditEngine.php', 'AlmanacDeviceQuery' => 'applications/almanac/query/AlmanacDeviceQuery.php', + 'AlmanacDeviceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php', 'AlmanacDeviceSearchEngine' => 'applications/almanac/query/AlmanacDeviceSearchEngine.php', 'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php', 'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php', @@ -90,6 +92,7 @@ phutil_register_library_map(array( 'AlmanacNetworkTransactionQuery' => 'applications/almanac/query/AlmanacNetworkTransactionQuery.php', 'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php', 'AlmanacPropertiesDestructionEngineExtension' => 'applications/almanac/engineextension/AlmanacPropertiesDestructionEngineExtension.php', + 'AlmanacPropertiesSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php', 'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php', 'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php', 'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php', @@ -101,6 +104,7 @@ phutil_register_library_map(array( 'AlmanacQueryDevicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php', 'AlmanacQueryServicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php', 'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php', + 'AlmanacSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacSearchEngineAttachment.php', 'AlmanacService' => 'applications/almanac/storage/AlmanacService.php', 'AlmanacServiceController' => 'applications/almanac/controller/AlmanacServiceController.php', 'AlmanacServiceDatasource' => 'applications/almanac/typeahead/AlmanacServiceDatasource.php', @@ -111,6 +115,7 @@ phutil_register_library_map(array( 'AlmanacServicePHIDType' => 'applications/almanac/phid/AlmanacServicePHIDType.php', 'AlmanacServicePropertyEditEngine' => 'applications/almanac/editor/AlmanacServicePropertyEditEngine.php', 'AlmanacServiceQuery' => 'applications/almanac/query/AlmanacServiceQuery.php', + 'AlmanacServiceSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacServiceSearchConduitAPIMethod.php', 'AlmanacServiceSearchEngine' => 'applications/almanac/query/AlmanacServiceSearchEngine.php', 'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php', 'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php', @@ -4007,6 +4012,7 @@ phutil_register_library_map(array( 'AlmanacBindingTransaction' => 'AlmanacTransaction', 'AlmanacBindingTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacBindingViewController' => 'AlmanacServiceController', + 'AlmanacBindingsSearchEngineAttachment' => 'AlmanacSearchEngineAttachment', 'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterServiceType' => 'AlmanacServiceType', @@ -4029,6 +4035,7 @@ phutil_register_library_map(array( 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', + 'PhabricatorConduitResultInterface', ), 'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', @@ -4038,6 +4045,7 @@ phutil_register_library_map(array( 'AlmanacDevicePHIDType' => 'PhabricatorPHIDType', 'AlmanacDevicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacDeviceQuery' => 'AlmanacQuery', + 'AlmanacDeviceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacDeviceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacDeviceTransaction' => 'AlmanacTransaction', 'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', @@ -4052,7 +4060,7 @@ phutil_register_library_map(array( 'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacInterfaceEditController' => 'AlmanacDeviceController', 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', - 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacInterfaceQuery' => 'AlmanacQuery', 'AlmanacInterfaceTableView' => 'AphrontView', 'AlmanacKeys' => 'Phobject', 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', @@ -4098,25 +4106,27 @@ phutil_register_library_map(array( 'AlmanacNetworkListController' => 'AlmanacNetworkController', 'AlmanacNetworkNameNgrams' => 'PhabricatorSearchNgrams', 'AlmanacNetworkPHIDType' => 'PhabricatorPHIDType', - 'AlmanacNetworkQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'AlmanacNetworkQuery' => 'AlmanacQuery', 'AlmanacNetworkSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacNetworkTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacNetworkTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacNetworkViewController' => 'AlmanacNetworkController', 'AlmanacPropertiesDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', + 'AlmanacPropertiesSearchEngineAttachment' => 'AlmanacSearchEngineAttachment', 'AlmanacProperty' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', ), 'AlmanacPropertyController' => 'AlmanacController', - 'AlmanacPropertyDeleteController' => 'AlmanacDeviceController', - 'AlmanacPropertyEditController' => 'AlmanacDeviceController', + 'AlmanacPropertyDeleteController' => 'AlmanacPropertyController', + 'AlmanacPropertyEditController' => 'AlmanacPropertyController', 'AlmanacPropertyEditEngine' => 'PhabricatorEditEngine', 'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQueryDevicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', 'AlmanacQueryServicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', 'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'AlmanacSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'AlmanacService' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', @@ -4125,6 +4135,7 @@ phutil_register_library_map(array( 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', + 'PhabricatorConduitResultInterface', ), 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', @@ -4135,6 +4146,7 @@ phutil_register_library_map(array( 'AlmanacServicePHIDType' => 'PhabricatorPHIDType', 'AlmanacServicePropertyEditEngine' => 'AlmanacPropertyEditEngine', 'AlmanacServiceQuery' => 'AlmanacQuery', + 'AlmanacServiceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'AlmanacServiceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'AlmanacServiceTransaction' => 'AlmanacTransaction', 'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', diff --git a/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php index 974d888917..b7eec60cb4 100644 --- a/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php +++ b/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php @@ -8,7 +8,7 @@ abstract class AlmanacConduitAPIMethod extends ConduitAPIMethod { } public function getMethodStatus() { - return self::METHOD_STATUS_UNSTABLE; + return self::METHOD_STATUS_DEPRECATED; } public function getMethodStatusDescription() { diff --git a/src/applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php new file mode 100644 index 0000000000..af674428a5 --- /dev/null +++ b/src/applications/almanac/conduit/AlmanacDeviceSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +setViewer($viewer) ->withIDs(array($id)) + ->needProperties(true) ->executeOne(); if (!$binding) { return new Aphront404Response(); diff --git a/src/applications/almanac/controller/AlmanacDeviceViewController.php b/src/applications/almanac/controller/AlmanacDeviceViewController.php index c7756c5b25..89ec7198e3 100644 --- a/src/applications/almanac/controller/AlmanacDeviceViewController.php +++ b/src/applications/almanac/controller/AlmanacDeviceViewController.php @@ -15,6 +15,7 @@ final class AlmanacDeviceViewController $device = id(new AlmanacDeviceQuery()) ->setViewer($viewer) ->withNames(array($name)) + ->needProperties(true) ->executeOne(); if (!$device) { return new Aphront404Response(); diff --git a/src/applications/almanac/controller/AlmanacPropertyController.php b/src/applications/almanac/controller/AlmanacPropertyController.php index 73d47431fd..deb2709ddd 100644 --- a/src/applications/almanac/controller/AlmanacPropertyController.php +++ b/src/applications/almanac/controller/AlmanacPropertyController.php @@ -1,3 +1,56 @@ propertyObject; + } + + protected function loadPropertyObject() { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + $object_phid = $request->getStr('objectPHID'); + + + switch (phid_get_type($object_phid)) { + case AlmanacBindingPHIDType::TYPECONST: + $query = new AlmanacBindingQuery(); + break; + case AlmanacDevicePHIDType::TYPECONST: + $query = new AlmanacDeviceQuery(); + break; + case AlmanacServicePHIDType::TYPECONST: + $query = new AlmanacServiceQuery(); + break; + default: + return new Aphront404Response(); + } + + $object = $query + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->needProperties(true) + ->executeOne(); + + if (!$object) { + return new Aphront404Response(); + } + + if (!($object instanceof AlmanacPropertyInterface)) { + return new Aphront404Response(); + } + + $this->propertyObject = $object; + + return null; + } + + +} diff --git a/src/applications/almanac/controller/AlmanacPropertyDeleteController.php b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php index 2bdaa5f35c..93ec4ced64 100644 --- a/src/applications/almanac/controller/AlmanacPropertyDeleteController.php +++ b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php @@ -1,27 +1,17 @@ getViewer(); - $object = id(new PhabricatorObjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($request->getStr('objectPHID'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); + $response = $this->loadPropertyObject(); + if ($response) { + return $response; } - if (!($object instanceof AlmanacPropertyInterface)) { - return new Aphront404Response(); - } + $object = $this->getPropertyObject(); $key = $request->getStr('key'); if (!strlen($key)) { diff --git a/src/applications/almanac/controller/AlmanacPropertyEditController.php b/src/applications/almanac/controller/AlmanacPropertyEditController.php index 46d609af86..ef587e5b05 100644 --- a/src/applications/almanac/controller/AlmanacPropertyEditController.php +++ b/src/applications/almanac/controller/AlmanacPropertyEditController.php @@ -1,27 +1,17 @@ getViewer(); - $object = id(new PhabricatorObjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($request->getStr('objectPHID'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); + $response = $this->loadPropertyObject(); + if ($response) { + return $response; } - if (!($object instanceof AlmanacPropertyInterface)) { - return new Aphront404Response(); - } + $object = $this->getPropertyObject(); $cancel_uri = $object->getURI(); $property_key = $request->getStr('key'); diff --git a/src/applications/almanac/controller/AlmanacServiceViewController.php b/src/applications/almanac/controller/AlmanacServiceViewController.php index 113722bf06..debc1bc063 100644 --- a/src/applications/almanac/controller/AlmanacServiceViewController.php +++ b/src/applications/almanac/controller/AlmanacServiceViewController.php @@ -15,6 +15,7 @@ final class AlmanacServiceViewController $service = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withNames(array($name)) + ->needProperties(true) ->executeOne(); if (!$service) { return new Aphront404Response(); diff --git a/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php new file mode 100644 index 0000000000..a3afaf3251 --- /dev/null +++ b/src/applications/almanac/engineextension/AlmanacBindingsSearchEngineAttachment.php @@ -0,0 +1,30 @@ +needProperties(true); + $query->needBindings(true); + } + + public function getAttachmentForObject($object, $data, $spec) { + $bindings = array(); + foreach ($object->getBindings() as $binding) { + $bindings[] = $this->getAlmanacBindingDictionary($binding); + } + + return array( + 'bindings' => $bindings, + ); + } + +} diff --git a/src/applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php new file mode 100644 index 0000000000..35c7deae74 --- /dev/null +++ b/src/applications/almanac/engineextension/AlmanacPropertiesSearchEngineAttachment.php @@ -0,0 +1,26 @@ +needProperties(true); + } + + public function getAttachmentForObject($object, $data, $spec) { + $properties = $this->getAlmanacPropertyList($object); + + return array( + 'properties' => $properties, + ); + } + +} diff --git a/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php new file mode 100644 index 0000000000..0875294bff --- /dev/null +++ b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php @@ -0,0 +1,64 @@ +getAlmanacPropertyFieldSpecifications(); + + $properties = array(); + foreach ($object->getAlmanacProperties() as $key => $property) { + $is_builtin = isset($builtins[$key]); + + $properties[] = array( + 'key' => $key, + 'value' => $property->getFieldValue(), + 'builtin' => $is_builtin, + ); + } + + return $properties; + } + + protected function getAlmanacBindingDictionary(AlmanacBinding $binding) { + $interface = $binding->getInterface(); + + return array( + 'id' => (int)$binding->getID(), + 'phid' => $binding->getPHID(), + 'properties' => $this->getAlmanacPropertyList($binding), + 'interface' => $this->getAlmanacInterfaceDictionary($interface), + ); + } + + protected function getAlmanacInterfaceDictionary( + AlmanacInterface $interface) { + + return array( + 'id' => (int)$interface->getID(), + 'phid' => $interface->getPHID(), + 'address' => $interface->getAddress(), + 'port' => (int)$interface->getPort(), + 'device' => $this->getAlmanacDeviceDictionary($interface->getDevice()), + 'network' => $this->getAlmanacNetworkDictionary($interface->getNetwork()), + ); + } + + protected function getAlmanacDeviceDictionary(AlmanacDevice $device) { + return array( + 'id' => (int)$device->getID(), + 'phid' => $device->getPHID(), + 'name' => $device->getName(), + 'properties' => $this->getAlmanacPropertyList($device), + ); + } + + protected function getAlmanacNetworkDictionary(AlmanacNetwork $network) { + return array( + 'id' => (int)$network->getID(), + 'phid' => $network->getPHID(), + 'name' => $network->getName(), + ); + } + +} diff --git a/src/applications/almanac/query/AlmanacBindingQuery.php b/src/applications/almanac/query/AlmanacBindingQuery.php index 0b6d7cda3a..5d51d2ba35 100644 --- a/src/applications/almanac/query/AlmanacBindingQuery.php +++ b/src/applications/almanac/query/AlmanacBindingQuery.php @@ -34,19 +34,12 @@ final class AlmanacBindingQuery return $this; } + public function newResultObject() { + return new AlmanacBinding(); + } + protected function loadPage() { - $table = new AlmanacBinding(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $bindings) { @@ -58,6 +51,7 @@ final class AlmanacBindingQuery ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($service_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $services = mpull($services, null, 'getPHID'); @@ -65,6 +59,7 @@ final class AlmanacBindingQuery ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($device_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $devices = mpull($devices, null, 'getPHID'); @@ -72,6 +67,7 @@ final class AlmanacBindingQuery ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($interface_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $interfaces = mpull($interfaces, null, 'getPHID'); @@ -93,47 +89,45 @@ final class AlmanacBindingQuery return $bindings; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->servicePHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'servicePHID IN (%Ls)', $this->servicePHIDs); } if ($this->devicePHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'devicePHID IN (%Ls)', $this->devicePHIDs); } if ($this->interfacePHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'interfacePHID IN (%Ls)', $this->interfacePHIDs); } - $where[] = $this->buildPagingClause($conn_r); - - return $this->formatWhereClause($where); + return $where; } } diff --git a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php index d9e13fba33..a85c38e188 100644 --- a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php @@ -21,6 +21,10 @@ final class AlmanacDeviceSearchEngine ->setLabel(pht('Name Contains')) ->setKey('match') ->setDescription(pht('Search for devices by name substring.')), + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Exact Names')) + ->setKey('names') + ->setDescription(pht('Search for devices with specific names.')), ); } @@ -31,6 +35,10 @@ final class AlmanacDeviceSearchEngine $query->withNameNgrams($map['match']); } + if ($map['names']) { + $query->withNames($map['names']); + } + return $query; } diff --git a/src/applications/almanac/query/AlmanacInterfaceQuery.php b/src/applications/almanac/query/AlmanacInterfaceQuery.php index 353ce34a49..bb6fc2f9d9 100644 --- a/src/applications/almanac/query/AlmanacInterfaceQuery.php +++ b/src/applications/almanac/query/AlmanacInterfaceQuery.php @@ -1,7 +1,7 @@ setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($network_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $networks = mpull($networks, null, 'getPHID'); @@ -57,6 +58,7 @@ final class AlmanacInterfaceQuery ->setParentQuery($this) ->setViewer($this->getViewer()) ->withPHIDs($device_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $devices = mpull($devices, null, 'getPHID'); diff --git a/src/applications/almanac/query/AlmanacNetworkQuery.php b/src/applications/almanac/query/AlmanacNetworkQuery.php index a1391ff916..a09da0093b 100644 --- a/src/applications/almanac/query/AlmanacNetworkQuery.php +++ b/src/applications/almanac/query/AlmanacNetworkQuery.php @@ -1,7 +1,7 @@ ids = $ids; @@ -18,72 +18,72 @@ final class AlmanacPropertyQuery return $this; } + public function withObjects(array $objects) { + $this->objects = mpull($objects, null, 'getPHID'); + $this->objectPHIDs = array_keys($this->objects); + return $this; + } + public function withNames(array $names) { $this->names = $names; return $this; } - public function setDisablePolicyFilteringAndAttachment($disable) { - $this->disablePolicyFilteringAndAttachment = $disable; - return $this; - } - - protected function shouldDisablePolicyFiltering() { - return $this->disablePolicyFilteringAndAttachment; + public function newResultObject() { + return new AlmanacProperty(); } protected function loadPage() { - $table = new AlmanacProperty(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $properties) { - if (!$this->disablePolicyFilteringAndAttachment) { - $object_phids = mpull($properties, 'getObjectPHID'); + $object_phids = mpull($properties, 'getObjectPHID'); + $object_phids = array_fuse($object_phids); + + if ($this->objects !== null) { + $object_phids = array_diff_key($object_phids, $this->objects); + } + + if ($object_phids) { $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); + } else { + $objects = array(); + } - foreach ($properties as $key => $property) { - $object = idx($objects, $property->getObjectPHID()); - if (!$object) { - unset($properties[$key]); - continue; - } - $property->attachObject($object); + $objects += $this->objects; + + foreach ($properties as $key => $property) { + $object = idx($objects, $property->getObjectPHID()); + if (!$object) { + unset($properties[$key]); + continue; } + $property->attachObject($object); } return $properties; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } @@ -94,14 +94,12 @@ final class AlmanacPropertyQuery $hashes[] = PhabricatorHash::digestForIndex($name); } $where[] = qsprintf( - $conn_r, + $conn, 'fieldIndex IN (%Ls)', $hashes); } - $where[] = $this->buildPagingClause($conn_r); - - return $this->formatWhereClause($where); + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/almanac/query/AlmanacQuery.php b/src/applications/almanac/query/AlmanacQuery.php index af28e60e30..b046171d32 100644 --- a/src/applications/almanac/query/AlmanacQuery.php +++ b/src/applications/almanac/query/AlmanacQuery.php @@ -3,21 +3,25 @@ abstract class AlmanacQuery extends PhabricatorCursorPagedPolicyAwareQuery { - protected function didFilterPage(array $objects) { - if (head($objects) instanceof AlmanacPropertyInterface) { - // NOTE: We load properties for obsolete historical reasons. It may make - // sense to re-examine that assumption shortly. + private $needProperties; + public function needProperties($need_properties) { + $this->needProperties = $need_properties; + return $this; + } + + protected function getNeedProperties() { + return $this->needProperties; + } + + protected function didFilterPage(array $objects) { + $has_properties = (head($objects) instanceof AlmanacPropertyInterface); + + if ($has_properties && $this->needProperties) { $property_query = id(new AlmanacPropertyQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) - ->withObjectPHIDs(mpull($objects, 'getPHID')); - - // NOTE: We disable policy filtering and object attachment to avoid - // a cyclic dependency where objects need their properties and properties - // need their objects. We'll attach the objects below, and have already - // implicitly checked the necessary policies. - $property_query->setDisablePolicyFilteringAndAttachment(true); + ->withObjects($objects); $properties = $property_query->execute(); diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php index f1e6630736..2cfb7edb8f 100644 --- a/src/applications/almanac/query/AlmanacServiceQuery.php +++ b/src/applications/almanac/query/AlmanacServiceQuery.php @@ -65,8 +65,12 @@ final class AlmanacServiceQuery return $this; } + public function newResultObject() { + return new AlmanacService(); + } + protected function loadPage() { - return $this->loadStandardPage(new AlmanacService()); + return $this->loadStandardPage($this->newResultObject()); } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { @@ -172,6 +176,7 @@ final class AlmanacServiceQuery $bindings = id(new AlmanacBindingQuery()) ->setViewer($this->getViewer()) ->withServicePHIDs($service_phids) + ->needProperties($this->getNeedProperties()) ->execute(); $bindings = mgroup($bindings, 'getServicePHID'); diff --git a/src/applications/almanac/query/AlmanacServiceSearchEngine.php b/src/applications/almanac/query/AlmanacServiceSearchEngine.php index 2f7d6cf58f..4011d4fbc5 100644 --- a/src/applications/almanac/query/AlmanacServiceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacServiceSearchEngine.php @@ -26,6 +26,14 @@ final class AlmanacServiceSearchEngine $query->withNameNgrams($map['match']); } + if ($map['names']) { + $query->withNames($map['names']); + } + + if ($map['devicePHIDs']) { + $query->withDevicePHIDs($map['devicePHIDs']); + } + return $query; } @@ -36,6 +44,15 @@ final class AlmanacServiceSearchEngine ->setLabel(pht('Name Contains')) ->setKey('match') ->setDescription(pht('Search for services by name substring.')), + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Exact Names')) + ->setKey('names') + ->setDescription(pht('Search for services with specific names.')), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Devices')) + ->setKey('devicePHIDs') + ->setDescription( + pht('Search for services bound to particular devices.')), ); } diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index 54ba92b4be..7ef17bbc2b 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -9,7 +9,8 @@ final class AlmanacDevice PhabricatorSSHPublicKeyInterface, AlmanacPropertyInterface, PhabricatorDestructibleInterface, - PhabricatorNgramsInterface { + PhabricatorNgramsInterface, + PhabricatorConduitResultInterface { protected $name; protected $nameIndex; @@ -243,4 +244,30 @@ final class AlmanacDevice ); } + +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('name') + ->setType('string') + ->setDescription(pht('The name of the device.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'name' => $this->getName(), + ); + } + + public function getConduitSearchAttachments() { + return array( + id(new AlmanacPropertiesSearchEngineAttachment()) + ->setAttachmentKey('properties'), + ); + } + } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 39c8c56e60..48eee1700b 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -8,7 +8,8 @@ final class AlmanacService PhabricatorProjectInterface, AlmanacPropertyInterface, PhabricatorDestructibleInterface, - PhabricatorNgramsInterface { + PhabricatorNgramsInterface, + PhabricatorConduitResultInterface { protected $name; protected $nameIndex; @@ -224,4 +225,32 @@ final class AlmanacService ); } + +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('name') + ->setType('string') + ->setDescription(pht('The name of the service.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'name' => $this->getName(), + ); + } + + public function getConduitSearchAttachments() { + return array( + id(new AlmanacPropertiesSearchEngineAttachment()) + ->setAttachmentKey('properties'), + id(new AlmanacBindingsSearchEngineAttachment()) + ->setAttachmentKey('bindings'), + ); + } + } diff --git a/src/applications/search/field/PhabricatorSearchTextField.php b/src/applications/search/field/PhabricatorSearchTextField.php index e3cf1f845c..915f22a6e9 100644 --- a/src/applications/search/field/PhabricatorSearchTextField.php +++ b/src/applications/search/field/PhabricatorSearchTextField.php @@ -15,4 +15,8 @@ final class PhabricatorSearchTextField return new AphrontFormTextControl(); } + protected function newConduitParameterType() { + return new ConduitStringParameterType(); + } + } From 800d800766a1b84c5d17a84792ae9bea0a3a8a12 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 23 Feb 2016 08:36:49 -0800 Subject: [PATCH 14/31] Remove dark Phabricator special CSS Summary: Looks like the border was almost always overridden and the background was only slightly darker. Safe to remove these. Fixes T10424 Test Plan: Set Workboard to "red" see red workboard. Toggle CSS on and off on homepage, note little to no visual difference. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T10424 Differential Revision: https://secure.phabricator.com/D15331 --- resources/celerity/map.php | 4 ++-- webroot/rsrc/css/application/base/phui-theme.css | 14 -------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 7e039dde0c..7b96b6b720 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -35,7 +35,7 @@ return array( 'rsrc/css/application/base/main-menu-view.css' => 'd00a795a', 'rsrc/css/application/base/notification-menu.css' => 'f31c0bde', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601', - 'rsrc/css/application/base/phui-theme.css' => 'ab7b848c', + 'rsrc/css/application/base/phui-theme.css' => '027ba77e', 'rsrc/css/application/base/standard-page-view.css' => 'e709f6d0', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', @@ -839,7 +839,7 @@ return array( 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '9d5d4400', - 'phui-theme-css' => 'ab7b848c', + 'phui-theme-css' => '027ba77e', 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => '0763177e', 'phui-workboard-color-css' => 'ac6fe6a7', diff --git a/webroot/rsrc/css/application/base/phui-theme.css b/webroot/rsrc/css/application/base/phui-theme.css index 815f0e0027..e0bfbf36a6 100644 --- a/webroot/rsrc/css/application/base/phui-theme.css +++ b/webroot/rsrc/css/application/base/phui-theme.css @@ -61,17 +61,3 @@ background: #122916; } - -/*--- Dark "Classic Phabricator" ---------------------------------------------*/ - -.phui-theme-dark { - background-color: {$page.background.dark}; -} - -.phui-theme-dark .phui-box-border { - border-color: {$lightblueborder}; -} - -.phui-theme-dark .phui-box-grey { - background-color: {$page.background.dark}; -} From 2cdc40eb004a125418ec4e127ef9209ab446a3cd Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 23 Feb 2016 08:52:41 -0800 Subject: [PATCH 15/31] Fix description variable on Blog manage page Summary: Properly set description variable. Test Plan: Visit blog manage page. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15332 --- .../phame/controller/blog/PhameBlogManageController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/applications/phame/controller/blog/PhameBlogManageController.php b/src/applications/phame/controller/blog/PhameBlogManageController.php index 14caafaab4..30de8686c8 100644 --- a/src/applications/phame/controller/blog/PhameBlogManageController.php +++ b/src/applications/phame/controller/blog/PhameBlogManageController.php @@ -117,7 +117,8 @@ final class PhameBlogManageController extends PhameBlogController { $properties->invokeWillRenderEvent(); - if (strlen($blog->getDescription())) { + $description = $blog->getDescription(); + if (strlen($description)) { $description = new PHUIRemarkupView($viewer, $description); $properties->addSectionHeader( pht('Description'), From 0799c91822246f83a3a038155d968d976d327368 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 23 Feb 2016 09:58:14 -0800 Subject: [PATCH 16/31] In Maniphest tasks, only move old owner to CC if owner changed Summary: Fixes T10426. When the owner of a task changes, we try to add the old owner to CC so they're kept in the loop. Currently, we do this unconditionally. This can add the owner as a subscriber when someone didn't change anything, which is confusing. Instead, only do this if the owner actually changed. Test Plan: - With "A" as owner, edited task and saved. - Before patch, A was added as subscriber. - After patch, A not added. - With "A" as owner, changed owner to "B" and saved. - Both before and after patch, "A" is added as a subscriber. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10426 Differential Revision: https://secure.phabricator.com/D15333 --- .../maniphest/editor/ManiphestTransactionEditor.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 97c567c30d..b88c5a5923 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1033,16 +1033,22 @@ final class ManiphestTransactionEditor )); break; case ManiphestTransaction::TYPE_OWNER: + // If this is a no-op update, don't expand it. + $old_value = $object->getOwnerPHID(); + $new_value = $xaction->getNewValue(); + if ($old_value === $new_value) { + continue; + } + // When a task is reassigned, move the old owner to the subscriber // list so they're still in the loop. - $owner_phid = $object->getOwnerPHID(); - if ($owner_phid) { + if ($old_value) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setIgnoreOnNoEffect(true) ->setNewValue( array( - '+' => array($owner_phid => $owner_phid), + '+' => array($old_value => $old_value), )); } break; From a112bc5cbaf0018408138e82396c2870da8d5098 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 23 Feb 2016 10:30:07 -0800 Subject: [PATCH 17/31] Sort Spaces dropdown by name, not "alphabetical ID" Summary: Fixes T10414. I think this sorted by name at one time (the `asort()`) but then I probably added "Space SX" in front of it. Or I just got this wrong from the beginning. Instead, sort by space name. Test Plan: {F1126034} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10414 Differential Revision: https://secure.phabricator.com/D15334 --- .../spaces/query/PhabricatorSpacesNamespaceQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php b/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php index 45d7bce1a6..ee11dcdd06 100644 --- a/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php +++ b/src/applications/spaces/query/PhabricatorSpacesNamespaceQuery.php @@ -184,6 +184,7 @@ final class PhabricatorSpacesNamespaceQuery $space_phid) { $viewer_spaces = self::getViewerSpaces($viewer); + $viewer_spaces = msort($viewer_spaces, 'getNamespaceName'); $map = array(); foreach ($viewer_spaces as $space) { @@ -200,7 +201,6 @@ final class PhabricatorSpacesNamespaceQuery $space->getMonogram(), $space->getNamespaceName()); } - asort($map); return $map; } From 76d4e85bfc1b13151e049ce3770fe570cc14f520 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 23 Feb 2016 10:44:30 -0800 Subject: [PATCH 18/31] Only hide archived project tags on workboard cards Summary: Fixes T10413. I accidentally hid these //everywhere//, but only intended to hide them on workboards. Test Plan: - Viewed a workboard, saw un-archived projects only. - Viewed a task detail page, saw archived and un-archived projects. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10413 Differential Revision: https://secure.phabricator.com/D15335 --- src/applications/phid/view/PHUIHandleTagListView.php | 7 ------- src/applications/project/view/ProjectBoardTaskCard.php | 10 ++++++++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/applications/phid/view/PHUIHandleTagListView.php b/src/applications/phid/view/PHUIHandleTagListView.php index baad938051..0bbfc4d249 100644 --- a/src/applications/phid/view/PHUIHandleTagListView.php +++ b/src/applications/phid/view/PHUIHandleTagListView.php @@ -52,13 +52,6 @@ final class PHUIHandleTagListView extends AphrontTagView { protected function getTagContent() { $handles = $this->handles; - // Remove any archived projects from the list. - foreach ($handles as $key => $handle) { - if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) { - unset($handles[$key]); - } - } - // If the list is empty, we may render a "No Projects" tag. if (!$handles) { if (strlen($this->noDataString)) { diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 087e9c1789..7be0af2c2f 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -116,6 +116,16 @@ final class ProjectBoardTaskCard extends Phobject { } $project_handles = $this->getProjectHandles(); + + // Remove any archived projects from the list. + if ($project_handles) { + foreach ($project_handles as $key => $handle) { + if ($handle->getStatus() == PhabricatorObjectHandle::STATUS_CLOSED) { + unset($project_handles[$key]); + } + } + } + if ($project_handles) { $tag_list = id(new PHUIHandleTagListView()) ->setSlim(true) From ee6070a98492f3faf5f6f5d71780b704fb79512e Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 23 Feb 2016 13:44:12 -0800 Subject: [PATCH 19/31] Add a couple of missing needProperties() Almanac calls Summary: Fixes T10432. I missed these in making properties non-default. Test Plan: Diffusion now works again in a cluster configuration. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10432 Differential Revision: https://secure.phabricator.com/D15337 --- .../diffusion/controller/DiffusionRepositoryCreateController.php | 1 + src/applications/repository/storage/PhabricatorRepository.php | 1 + 2 files changed, 2 insertions(+) diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php index 1333bf67f9..bfd01ff30c 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php @@ -51,6 +51,7 @@ final class DiffusionRepositoryCreateController array( 'AlmanacClusterRepositoryServiceType', )) + ->needProperties(true) ->execute(); if ($services) { // Filter out services which do not permit new allocations. diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index c2ed8023f3..e6510e7ad9 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2038,6 +2038,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($service_phid)) ->needBindings(true) + ->needProperties(true) ->executeOne(); if (!$service) { throw new Exception( From 9baae00fbd02e50a72595d7a53ff010a1bf76745 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 23 Feb 2016 14:41:36 -0800 Subject: [PATCH 20/31] Fix a couple of column editing issues Summary: Ref T10349. - Don't show subproject columns on "Manage Board". - Fix "Edit Column" for milestone columns (allows you to set points, but not rename). Test Plan: - Viewed "Manage Board" on a project with subprojects. - Edited a milestone column and set a point limit. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10349 Differential Revision: https://secure.phabricator.com/D15338 --- .../controller/PhabricatorProjectBoardManageController.php | 5 +++++ .../controller/PhabricatorProjectColumnEditController.php | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardManageController.php b/src/applications/project/controller/PhabricatorProjectBoardManageController.php index db4dfe31be..75bff07106 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardManageController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardManageController.php @@ -152,6 +152,11 @@ final class PhabricatorProjectBoardManageController foreach ($columns as $column) { $column_id = $column->getID(); + $proxy = $column->getProxy(); + if ($proxy && !$proxy->isMilestone()) { + continue; + } + $detail_uri = "/project/board/{$board_id}/column/{$column_id}/"; $item = id(new PHUIObjectItemView()) diff --git a/src/applications/project/controller/PhabricatorProjectColumnEditController.php b/src/applications/project/controller/PhabricatorProjectColumnEditController.php index 9f880b9515..fb0c7b0910 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnEditController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnEditController.php @@ -81,14 +81,15 @@ final class PhabricatorProjectColumnEditController $xactions = array(); + $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; + $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT; + if (!$column->getProxy()) { - $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; $xactions[] = id(new PhabricatorProjectColumnTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); } - $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT; $xactions[] = id(new PhabricatorProjectColumnTransaction()) ->setTransactionType($type_limit) ->setNewValue($v_limit); @@ -97,6 +98,7 @@ final class PhabricatorProjectColumnEditController $editor = id(new PhabricatorProjectColumnTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->applyTransactions($column, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); From e9f4ca6ca338142b2cf44e61d313d3c6810435d4 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 23 Feb 2016 07:23:58 -0800 Subject: [PATCH 21/31] Redesign PonderQuestionView Summary: Full new UI, testing some upcoming treatments for consideration in other View controllers. Small tweaks to allow PHUITwoColumnView to have fixed and fluid width, and let TransactionCommentView go fullWidth. Test Plan: Tested a number of Ponder cases, New Question, with and without summary, with and without answers, with and without comments. Mobile, Tablet, and Desktop layouts. Verify Project and Profile UI's still in tact. {F1120961} {F1120962} {F1120963} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15315 --- resources/celerity/map.php | 18 +- src/__phutil_library_map__.php | 12 - ...PhabricatorPeopleProfileViewController.php | 1 + .../PhabricatorPonderApplication.php | 2 - .../ponder/constants/PonderVote.php | 8 - .../PonderHelpfulSaveController.php | 60 ----- .../PonderQuestionViewController.php | 244 ++++++++++++------ .../PonderAnswerHasVotingUserEdgeType.php | 105 -------- .../PonderVotingUserHasAnswerEdgeType.php | 105 -------- .../ponder/editor/PonderVoteEditor.php | 77 ------ .../ponder/query/PonderAnswerQuery.php | 27 -- .../ponder/query/PonderQuestionQuery.php | 1 - .../ponder/storage/PonderAnswer.php | 33 +-- .../ponder/storage/PonderVotableInterface.php | 8 - .../ponder/view/PonderAddAnswerView.php | 48 +--- .../ponder/view/PonderAnswerView.php | 46 +--- .../ponder/view/PonderFooterView.php | 8 +- .../PhabricatorProjectProfileController.php | 1 + ...catorApplicationTransactionCommentView.php | 7 + src/view/phui/PHUITwoColumnView.php | 22 +- .../css/application/ponder/ponder-view.css | 206 ++++++++++++--- .../css/application/project/project-view.css | 5 +- webroot/rsrc/css/phui/phui-header-view.css | 6 + .../rsrc/css/phui/phui-two-column-view.css | 21 ++ 24 files changed, 425 insertions(+), 646 deletions(-) delete mode 100644 src/applications/ponder/constants/PonderVote.php delete mode 100644 src/applications/ponder/controller/PonderHelpfulSaveController.php delete mode 100644 src/applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php delete mode 100644 src/applications/ponder/edge/PonderVotingUserHasAnswerEdgeType.php delete mode 100644 src/applications/ponder/editor/PonderVoteEditor.php delete mode 100644 src/applications/ponder/storage/PonderVotableInterface.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 7b96b6b720..1f15126966 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '7935f211', + 'core.pkg.css' => 'ecdca229', 'core.pkg.js' => '7d8faf57', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -92,9 +92,9 @@ return array( 'rsrc/css/application/policy/policy-edit.css' => '815c66f7', 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', - 'rsrc/css/application/ponder/ponder-view.css' => 'b40dc156', + 'rsrc/css/application/ponder/ponder-view.css' => 'fdd4629b', 'rsrc/css/application/project/project-card-view.css' => '9418c97d', - 'rsrc/css/application/project/project-view.css' => '83bb6654', + 'rsrc/css/application/project/project-view.css' => '298b7c5b', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', @@ -134,7 +134,7 @@ return array( 'rsrc/css/phui/phui-fontkit.css' => '9cda225e', 'rsrc/css/phui/phui-form-view.css' => '4a1a0f5e', 'rsrc/css/phui/phui-form.css' => 'aac1d51d', - 'rsrc/css/phui/phui-header-view.css' => '50c5cb6a', + 'rsrc/css/phui/phui-header-view.css' => 'a6d7b20d', 'rsrc/css/phui/phui-hovercard.css' => 'de1a2119', 'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad', 'rsrc/css/phui/phui-icon.css' => '3f33ab57', @@ -154,7 +154,7 @@ return array( 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', - 'rsrc/css/phui/phui-two-column-view.css' => '0763177e', + 'rsrc/css/phui/phui-two-column-view.css' => 'a317616a', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7', 'rsrc/css/phui/workboards/phui-workboard.css' => 'e6d89647', 'rsrc/css/phui/workboards/phui-workcard.css' => '3646fb96', @@ -818,7 +818,7 @@ return array( 'phui-fontkit-css' => '9cda225e', 'phui-form-css' => 'aac1d51d', 'phui-form-view-css' => '4a1a0f5e', - 'phui-header-view-css' => '50c5cb6a', + 'phui-header-view-css' => 'a6d7b20d', 'phui-hovercard' => '1bd28176', 'phui-hovercard-view-css' => 'de1a2119', 'phui-icon-set-selector-css' => '1ab67aad', @@ -841,7 +841,7 @@ return array( 'phui-tag-view-css' => '9d5d4400', 'phui-theme-css' => '027ba77e', 'phui-timeline-view-css' => '2efceff8', - 'phui-two-column-view-css' => '0763177e', + 'phui-two-column-view-css' => 'a317616a', 'phui-workboard-color-css' => 'ac6fe6a7', 'phui-workboard-view-css' => 'e6d89647', 'phui-workcard-view-css' => '3646fb96', @@ -855,9 +855,9 @@ return array( 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', - 'ponder-view-css' => 'b40dc156', + 'ponder-view-css' => 'fdd4629b', 'project-card-view-css' => '9418c97d', - 'project-view-css' => '83bb6654', + 'project-view-css' => '298b7c5b', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', 'releeph-request-differential-create-dialog' => '8d8b92cd', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4de280e4f9..a4b133893b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3810,7 +3810,6 @@ phutil_register_library_map(array( 'PonderAnswerCommentController' => 'applications/ponder/controller/PonderAnswerCommentController.php', 'PonderAnswerEditController' => 'applications/ponder/controller/PonderAnswerEditController.php', 'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php', - 'PonderAnswerHasVotingUserEdgeType' => 'applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php', 'PonderAnswerHistoryController' => 'applications/ponder/controller/PonderAnswerHistoryController.php', 'PonderAnswerMailReceiver' => 'applications/ponder/mail/PonderAnswerMailReceiver.php', 'PonderAnswerPHIDType' => 'applications/ponder/phid/PonderAnswerPHIDType.php', @@ -3828,7 +3827,6 @@ phutil_register_library_map(array( 'PonderDefaultViewCapability' => 'applications/ponder/capability/PonderDefaultViewCapability.php', 'PonderEditor' => 'applications/ponder/editor/PonderEditor.php', 'PonderFooterView' => 'applications/ponder/view/PonderFooterView.php', - 'PonderHelpfulSaveController' => 'applications/ponder/controller/PonderHelpfulSaveController.php', 'PonderModerateCapability' => 'applications/ponder/capability/PonderModerateCapability.php', 'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php', 'PonderQuestionCommentController' => 'applications/ponder/controller/PonderQuestionCommentController.php', @@ -3850,10 +3848,6 @@ phutil_register_library_map(array( 'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php', 'PonderRemarkupRule' => 'applications/ponder/remarkup/PonderRemarkupRule.php', 'PonderSchemaSpec' => 'applications/ponder/storage/PonderSchemaSpec.php', - 'PonderVotableInterface' => 'applications/ponder/storage/PonderVotableInterface.php', - 'PonderVote' => 'applications/ponder/constants/PonderVote.php', - 'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php', - 'PonderVotingUserHasAnswerEdgeType' => 'applications/ponder/edge/PonderVotingUserHasAnswerEdgeType.php', 'ProjectAddProjectsEmailCommand' => 'applications/project/command/ProjectAddProjectsEmailCommand.php', 'ProjectBoardTaskCard' => 'applications/project/view/ProjectBoardTaskCard.php', 'ProjectCanLockProjectsCapability' => 'applications/project/capability/ProjectCanLockProjectsCapability.php', @@ -8479,7 +8473,6 @@ phutil_register_library_map(array( 'PonderDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMarkupInterface', - 'PonderVotableInterface', 'PhabricatorPolicyInterface', 'PhabricatorFlaggableInterface', 'PhabricatorSubscribableInterface', @@ -8488,7 +8481,6 @@ phutil_register_library_map(array( 'PonderAnswerCommentController' => 'PonderController', 'PonderAnswerEditController' => 'PonderController', 'PonderAnswerEditor' => 'PonderEditor', - 'PonderAnswerHasVotingUserEdgeType' => 'PhabricatorEdgeType', 'PonderAnswerHistoryController' => 'PonderController', 'PonderAnswerMailReceiver' => 'PhabricatorObjectMailReceiver', 'PonderAnswerPHIDType' => 'PhabricatorPHIDType', @@ -8506,7 +8498,6 @@ phutil_register_library_map(array( 'PonderDefaultViewCapability' => 'PhabricatorPolicyCapability', 'PonderEditor' => 'PhabricatorApplicationTransactionEditor', 'PonderFooterView' => 'AphrontTagView', - 'PonderHelpfulSaveController' => 'PonderController', 'PonderModerateCapability' => 'PhabricatorPolicyCapability', 'PonderQuestion' => array( 'PonderDAO', @@ -8540,9 +8531,6 @@ phutil_register_library_map(array( 'PonderQuestionViewController' => 'PonderController', 'PonderRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PonderSchemaSpec' => 'PhabricatorConfigSchemaSpec', - 'PonderVote' => 'PonderConstants', - 'PonderVoteEditor' => 'PhabricatorEditor', - 'PonderVotingUserHasAnswerEdgeType' => 'PhabricatorEdgeType', 'ProjectAddProjectsEmailCommand' => 'MetaMTAEmailTransactionCommand', 'ProjectBoardTaskCard' => 'Phobject', 'ProjectCanLockProjectsCapability' => 'PhabricatorPolicyCapability', diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index ce15aa2b38..698ff983f1 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -60,6 +60,7 @@ final class PhabricatorPeopleProfileViewController $home = id(new PHUITwoColumnView()) ->setHeader($header) + ->setFluid(true) ->addClass('project-view-home') ->setMainColumn( array( diff --git a/src/applications/ponder/application/PhabricatorPonderApplication.php b/src/applications/ponder/application/PhabricatorPonderApplication.php index 94a06edffa..460acd989e 100644 --- a/src/applications/ponder/application/PhabricatorPonderApplication.php +++ b/src/applications/ponder/application/PhabricatorPonderApplication.php @@ -49,8 +49,6 @@ final class PhabricatorPonderApplication extends PhabricatorApplication { => 'PonderAnswerCommentController', 'answer/history/(?P\d+)/' => 'PonderAnswerHistoryController', - 'answer/helpful/(?Padd|remove)/(?P[1-9]\d*)/' - => 'PonderHelpfulSaveController', 'question/edit/(?:(?P\d+)/)?' => 'PonderQuestionEditController', 'question/create/' diff --git a/src/applications/ponder/constants/PonderVote.php b/src/applications/ponder/constants/PonderVote.php deleted file mode 100644 index c3e9745eec..0000000000 --- a/src/applications/ponder/constants/PonderVote.php +++ /dev/null @@ -1,8 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - $action = $request->getURIData('action'); - - $answer = id(new PonderAnswerQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->needViewerVotes(true) - ->executeOne(); - - if (!$answer) { - return new Aphront404Response(); - } - - $edit_uri = '/Q'.$answer->getQuestionID(); - - switch ($action) { - case 'add': - $newvote = PonderVote::VOTE_UP; - break; - case 'remove': - $newvote = PonderVote::VOTE_NONE; - break; - } - - if ($request->isFormPost()) { - - $editor = id(new PonderVoteEditor()) - ->setVotable($answer) - ->setActor($viewer) - ->setVote($newvote) - ->saveVote(); - - return id(new AphrontRedirectResponse())->setURI($edit_uri); - } - - if ($action == 'add') { - $title = pht('Mark Answer as Helpful?'); - $body = pht('This answer will be marked as helpful.'); - $button = pht('Mark Helpful'); - } else { - $title = pht('Remove Helpful From Answer?'); - $body = pht('This answer will no longer be marked as helpful.'); - $button = pht('Remove Helpful'); - } - - $dialog = $this->newDialog(); - $dialog->setTitle($title); - $dialog->appendChild($body); - $dialog->addCancelButton($edit_uri); - $dialog->addSubmitButton($button); - - return id(new AphrontDialogResponse())->setDialog($dialog); - } -} diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 38ca4cad30..8f38a0573c 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -31,6 +31,7 @@ final class PonderQuestionViewController extends PonderController { $header->setHeader($question->getTitle()); $header->setUser($viewer); $header->setPolicyObject($question); + $header->setProfileHeader(true); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $header->setStatus('fa-square-o', 'bluegrey', pht('Open')); @@ -43,7 +44,32 @@ final class PonderQuestionViewController extends PonderController { } $actions = $this->buildActionListView($question); - $properties = $this->buildPropertyListView($question, $actions); + $properties = $this->buildPropertyListView($question); + $details = $this->buildDetailsPropertyView($question); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $question, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = '/question/edit/'.$question->getID().'/'; + $edit_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Edit')) + ->setHref($this->getApplicationURI($edit_uri)) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit); + + $action_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Actions')) + ->setHref('#') + ->setIcon('fa-bars') + ->setDropdownMenu($actions); + + $header->addActionLink($action_button); + $header->addActionLink($edit_button); $content_id = celerity_generate_unique_node_id(); $timeline = $this->buildTransactionTimeline( @@ -55,11 +81,14 @@ final class PonderQuestionViewController extends PonderController { $add_comment = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($question->getPHID()) + ->setFullWidth(true) ->setShowPreview(false) - ->setHeaderText(pht('Question Comment')) ->setAction($this->getApplicationURI("/question/comment/{$id}/")) ->setSubmitButtonName(pht('Comment')); + $add_comment = phutil_tag_div( + 'ponder-question-add-comment-view', $add_comment); + $comment_view = phutil_tag( 'div', array( @@ -75,38 +104,61 @@ final class PonderQuestionViewController extends PonderController { ->setContentID($content_id) ->setCount(count($xactions)); - $object_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties) - ->appendChild($footer); - $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()); $crumbs->addTextCrumb('Q'.$id, '/Q'.$id); + $crumbs->setBorder(true); $answer_wiki = null; if ($question->getAnswerWiki()) { - $answer = phutil_tag_div('mlt mlb msr msl', $question->getAnswerWiki()); + $wiki = new PHUIRemarkupView($viewer, $question->getAnswerWiki()); + $wiki_header = phutil_tag( + 'div', + array( + 'class' => 'ponder-answer-wiki-header', + ), + pht('Answer Summary')); + $answer_wiki = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Answer Summary')) - ->setColor(PHUIObjectBoxView::COLOR_BLUE) - ->appendChild($answer); + ->setBackground(PHUIObjectBoxView::BLUE) + ->appendChild($wiki_header) + ->appendChild($wiki) + ->addClass('ponder-answer-wiki'); } - return $this->buildApplicationPage( + require_celerity_resource('ponder-view-css'); + + $ponder_content = phutil_tag( + 'div', array( - $crumbs, - $object_box, + 'class' => 'ponder-question-content', + ), + array( + $details, + $footer, $comment_view, $answer_wiki, $answers, $answer_add_panel, - ), - array( - 'title' => 'Q'.$question->getID().' '.$question->getTitle(), - 'pageObjects' => array_merge( - array($question->getPHID()), - mpull($question->getAnswers(), 'getPHID')), )); + + $ponder_view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setMainColumn($ponder_content) + ->setSideColumn($properties) + ->addClass('ponder-question-view'); + + $page_objects = array_merge( + array($question->getPHID()), + mpull($question->getAnswers(), 'getPHID')); + + return $this->newPage() + ->setTitle('Q'.$question->getID().' '.$question->getTitle()) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs($page_objects) + ->appendChild( + array( + $ponder_view, + )); } private function buildActionListView(PonderQuestion $question) { @@ -123,14 +175,6 @@ final class PonderQuestionViewController extends PonderController { ->setUser($viewer) ->setObject($question); - $view->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-pencil') - ->setName(pht('Edit Question')) - ->setHref($this->getApplicationURI("/question/edit/{$id}/")) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); - if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $name = pht('Close Question'); $icon = 'fa-check-square-o'; @@ -157,49 +201,89 @@ final class PonderQuestionViewController extends PonderController { } private function buildPropertyListView( - PonderQuestion $question, - PhabricatorActionListView $actions) { + PonderQuestion $question) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($question) - ->setActionList($actions); - - $view->addProperty( - pht('Author'), - $viewer->renderHandle($question->getAuthorPHID())); - - $view->addProperty( - pht('Created'), - phabricator_datetime($question->getDateCreated(), $viewer)); + ->setStacked(true); $view->invokeWillRenderEvent(); - $details = PhabricatorMarkupEngine::renderOneObject( - $question, - $question->getMarkupField(), - $viewer); - - if ($details) { - $view->addSectionHeader( - pht('Details'), - PHUIPropertyListView::ICON_SUMMARY); - - $view->addTextContent( - array( - phutil_tag( - 'div', - array( - 'class' => 'phabricator-remarkup', - ), - $details), - )); + if (!$view->hasAnyProperties()) { + return null; } + $view = id(new PHUIObjectBoxView()) + ->appendChild($view) + ->setBackground(PHUIObjectBoxView::GREY) + ->addClass('ponder-view-properties'); + return $view; } + private function buildDetailsPropertyView( + PonderQuestion $question) { + $viewer = $this->getViewer(); + + $question_details = PhabricatorMarkupEngine::renderOneObject( + $question, + $question->getMarkupField(), + $viewer); + + if (!$question_details) { + $question_details = phutil_tag( + 'em', + array(), + pht('No further details for this question.')); + } + + $asker = $viewer->renderHandle($question->getAuthorPHID())->render(); + $date = phabricator_datetime($question->getDateCreated(), $viewer); + $asker = phutil_tag('strong', array(), $asker); + + $author = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($question->getAuthorPHID())) + ->needProfileImage(true) + ->executeOne(); + + $image_uri = $author->getProfileImageURI(); + $image_href = '/p/'.$author->getUsername(); + + $image = phutil_tag( + 'a', + array( + 'class' => 'ponder-details-author-image', + 'style' => 'background-image: url('.$image_uri.');', + 'href' => $image_href, + )); + + $details_header = phutil_tag( + 'div', + array( + 'class' => 'ponder-details-subtitle', + ), + array( + $image, + pht('Asked by %s on %s.', $asker, $date), + )); + + $details = phutil_tag( + 'div', + array( + 'class' => 'ponder-detail-view', + ), + array( + $details_header, + phutil_tag_div('phabricator-remarkup', $question_details), + )); + + + return $details; + } + /** * This is fairly non-standard; building N timelines at once (N = number of * answers) is tricky business. @@ -211,32 +295,38 @@ final class PonderQuestionViewController extends PonderController { $viewer = $this->getViewer(); $answers = $question->getAnswers(); - $author_phids = mpull($answers, 'getAuthorPHID'); - $handles = $this->loadViewerHandles($author_phids); - $answers_sort = array_reverse(msort($answers, 'getVoteCount')); + if ($answers) { + $author_phids = mpull($answers, 'getAuthorPHID'); + $handles = $this->loadViewerHandles($author_phids); - $view = array(); - foreach ($answers_sort as $answer) { - $id = $answer->getID(); - $handle = $handles[$answer->getAuthorPHID()]; + $view = array(); + foreach ($answers as $answer) { + $id = $answer->getID(); + $handle = $handles[$answer->getAuthorPHID()]; - $timeline = $this->buildTransactionTimeline( - $answer, - id(new PonderAnswerTransactionQuery()) - ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); - $xactions = $timeline->getTransactions(); + $timeline = $this->buildTransactionTimeline( + $answer, + id(new PonderAnswerTransactionQuery()) + ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))); + $xactions = $timeline->getTransactions(); - $view[] = id(new PonderAnswerView()) - ->setUser($viewer) - ->setAnswer($answer) - ->setTransactions($xactions) - ->setTimeline($timeline) - ->setHandle($handle); + $view[] = id(new PonderAnswerView()) + ->setUser($viewer) + ->setAnswer($answer) + ->setTransactions($xactions) + ->setTimeline($timeline) + ->setHandle($handle); + } + + $header = id(new PHUIHeaderView()) + ->setHeader('Answers'); + return array($header, $view); } - return $view; + return null; + } } diff --git a/src/applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php b/src/applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php deleted file mode 100644 index 74447dbf0d..0000000000 --- a/src/applications/ponder/edge/PonderAnswerHasVotingUserEdgeType.php +++ /dev/null @@ -1,105 +0,0 @@ -answer = $answer; - return $this; - } - - public function setVotable($votable) { - $this->votable = $votable; - return $this; - } - - public function setVote($vote) { - $this->vote = $vote; - return $this; - } - - public function saveVote() { - $actor = $this->requireActor(); - if (!$this->votable) { - throw new PhutilInvalidStateException('setVotable'); - } - - $votable = $this->votable; - $newvote = $this->vote; - - // prepare vote add, or update if this user is amending an - // earlier vote - $editor = id(new PhabricatorEdgeEditor()) - ->addEdge( - $actor->getPHID(), - $votable->getUserVoteEdgeType(), - $votable->getVotablePHID(), - array('data' => $newvote)) - ->removeEdge( - $actor->getPHID(), - $votable->getUserVoteEdgeType(), - $votable->getVotablePHID()); - - $conn = $votable->establishConnection('w'); - $trans = $conn->openTransaction(); - $trans->beginReadLocking(); - - $votable->reload(); - $curvote = (int)PhabricatorEdgeQuery::loadSingleEdgeData( - $actor->getPHID(), - $votable->getUserVoteEdgeType(), - $votable->getVotablePHID()); - - if (!$curvote) { - $curvote = PonderVote::VOTE_NONE; - } - - // Adjust votable's score by this much. - $delta = $newvote - $curvote; - - queryfx($conn, - 'UPDATE %T as t - SET t.voteCount = t.voteCount + %d - WHERE t.PHID = %s', - $votable->getTableName(), - $delta, - $votable->getVotablePHID()); - - $editor->save(); - - $trans->endReadLocking(); - $trans->saveTransaction(); - } -} diff --git a/src/applications/ponder/query/PonderAnswerQuery.php b/src/applications/ponder/query/PonderAnswerQuery.php index 4e874aeb60..2901f4d6a5 100644 --- a/src/applications/ponder/query/PonderAnswerQuery.php +++ b/src/applications/ponder/query/PonderAnswerQuery.php @@ -8,9 +8,6 @@ final class PonderAnswerQuery private $authorPHIDs; private $questionIDs; - private $needViewerVotes; - - public function withIDs(array $ids) { $this->ids = $ids; return $this; @@ -31,11 +28,6 @@ final class PonderAnswerQuery return $this; } - public function needViewerVotes($need_viewer_votes) { - $this->needViewerVotes = $need_viewer_votes; - return $this; - } - protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); @@ -86,25 +78,6 @@ final class PonderAnswerQuery $answer->attachQuestion($question); } - if ($this->needViewerVotes) { - $viewer_phid = $this->getViewer()->getPHID(); - - $etype = PonderAnswerHasVotingUserEdgeType::EDGECONST; - $edges = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs(mpull($answers, 'getPHID')) - ->withDestinationPHIDs(array($viewer_phid)) - ->withEdgeTypes(array($etype)) - ->needEdgeData(true) - ->execute(); - foreach ($answers as $answer) { - $user_edge = idx( - $edges[$answer->getPHID()][$etype], - $viewer_phid, - array()); - $answer->attachUserVote($viewer_phid, idx($user_edge, 'data', 0)); - } - } - return $answers; } diff --git a/src/applications/ponder/query/PonderQuestionQuery.php b/src/applications/ponder/query/PonderQuestionQuery.php index 9762f925f5..b2de14e52c 100644 --- a/src/applications/ponder/query/PonderQuestionQuery.php +++ b/src/applications/ponder/query/PonderQuestionQuery.php @@ -98,7 +98,6 @@ final class PonderQuestionQuery $aquery = id(new PonderAnswerQuery()) ->setViewer($this->getViewer()) ->setOrderVector(array('-id')) - ->needViewerVotes(true) ->withQuestionIDs(mpull($questions, 'getID')); $answers = $aquery->execute(); diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index 4da222d5a5..5fa1a1fc7a 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -4,7 +4,6 @@ final class PonderAnswer extends PonderDAO implements PhabricatorApplicationTransactionInterface, PhabricatorMarkupInterface, - PonderVotableInterface, PhabricatorPolicyInterface, PhabricatorFlaggableInterface, PhabricatorSubscribableInterface, @@ -18,14 +17,10 @@ final class PonderAnswer extends PonderDAO protected $content; protected $mailKey; protected $status; - protected $voteCount; - private $vote; private $question = self::ATTACHABLE; private $comments; - private $userVotes = array(); - public static function initializeNewAnswer( PhabricatorUser $actor, PonderQuestion $question) { @@ -39,7 +34,7 @@ final class PonderAnswer extends PonderDAO ->setContent('') ->attachQuestion($question) ->setAuthorPHID($actor->getPHID()) - ->setVoteCount(0) + ->setVoteCount('0') ->setStatus(PonderAnswerStatus::ANSWER_STATUS_VISIBLE); } @@ -57,23 +52,6 @@ final class PonderAnswer extends PonderDAO return '/Q'.$this->getQuestionID().'#A'.$this->getID(); } - public function setUserVote($vote) { - $this->vote = $vote['data']; - if (!$this->vote) { - $this->vote = PonderVote::VOTE_NONE; - } - return $this; - } - - public function attachUserVote($user_phid, $vote) { - $this->vote = $vote; - return $this; - } - - public function getUserVote() { - return $this->vote; - } - public function setComments($comments) { $this->comments = $comments; return $this; @@ -181,15 +159,6 @@ final class PonderAnswer extends PonderDAO return (bool)$this->getID(); } - // votable interface - public function getUserVoteEdgeType() { - return PonderVotingUserHasAnswerEdgeType::EDGECONST; - } - - public function getVotablePHID() { - return $this->getPHID(); - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/ponder/storage/PonderVotableInterface.php b/src/applications/ponder/storage/PonderVotableInterface.php deleted file mode 100644 index 6350a20e80..0000000000 --- a/src/applications/ponder/storage/PonderVotableInterface.php +++ /dev/null @@ -1,8 +0,0 @@ -getAuthorPHID() == $viewer->getPHID()) { - $box_style = 'display: none;'; - $open_link = javelin_tag( - 'a', - array( - 'sigil' => 'reveal-content', - 'class' => 'mml', - 'id' => $hide_action_id, - 'href' => '#', - 'meta' => array( - 'showIDs' => array($show_action_id), - 'hideIDs' => array($hide_action_id), - ), - ), - pht('Add an answer.')); - $own_question = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) - ->setID($hide_action_id) - ->appendChild( - pht( - 'This is your own question. You are welcome to provide - an answer if you have found a resolution.')) - ->appendChild($open_link); - } - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Add Answer')); + ->setHeader(pht('New Answer')) + ->addClass('ponder-add-answer-header'); $form = new AphrontFormView(); $form ->setUser($this->user) ->setAction($this->actionURI) ->setWorkflow(true) + ->setFullWidth(true) ->addHiddenInput('question_id', $question->getID()) ->appendChild( id(new PhabricatorRemarkupControl()) @@ -103,22 +77,14 @@ final class PonderAddAnswerView extends AphrontView { } $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->appendChild($form); + ->appendChild($form) + ->setBackground(PHUIObjectBoxView::GREY) + ->addClass('ponder-add-answer-view'); if ($info_panel) { $box->setInfoView($info_panel); } - $box = phutil_tag( - 'div', - array( - 'style' => $box_style, - 'class' => 'mlt', - 'id' => $show_action_id, - ), - $box); - - return array($own_question, $box); + return array($header, $box); } } diff --git a/src/applications/ponder/view/PonderAnswerView.php b/src/applications/ponder/view/PonderAnswerView.php index c0e90f7eca..474661381c 100644 --- a/src/applications/ponder/view/PonderAnswerView.php +++ b/src/applications/ponder/view/PonderAnswerView.php @@ -95,7 +95,7 @@ final class PonderAnswerView extends AphrontTagView { $content = phutil_tag( 'div', array( - 'class' => 'phabricator-remarkup mlt mlb msr msl', + 'class' => 'phabricator-remarkup', ), PhabricatorMarkupEngine::renderOneObject( $answer, @@ -110,26 +110,14 @@ final class PonderAnswerView extends AphrontTagView { ->setContentID($content_id) ->setCount(count($this->transactions)); - $votes = $answer->getVoteCount(); - $vote_class = null; - if ($votes > 0) { - $vote_class = 'ponder-footer-action-helpful'; - } - $icon = id(new PHUIIconView()) - ->setIcon('fa-thumbs-up msr'); - $helpful = phutil_tag( - 'span', - array( - 'class' => 'ponder-footer-action '.$vote_class, - ), - array($icon, $votes)); - $footer->addAction($helpful); + $content = phutil_tag_div( + 'ponder-answer-content', array($anchor, $content, $footer)); $answer_view = id(new PHUIObjectBoxView()) ->setHeader($header) - ->appendChild($anchor) - ->appendChild($content) - ->appendChild($footer); + ->setBackground(PHUIObjectBoxView::GREY) + ->addClass('ponder-answer') + ->appendChild($content); $comment_view = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) @@ -170,28 +158,6 @@ final class PonderAnswerView extends AphrontTagView { ->setUser($viewer) ->setObject($answer); - $user_marked = $answer->getUserVote(); - $can_vote = $viewer->isLoggedIn(); - - if ($user_marked) { - $helpful_uri = "/ponder/answer/helpful/remove/{$id}/"; - $helpful_icon = 'fa-times'; - $helpful_text = pht('Remove Helpful'); - } else { - $helpful_uri = "/ponder/answer/helpful/add/{$id}/"; - $helpful_icon = 'fa-thumbs-up'; - $helpful_text = pht('Mark as Helpful'); - } - - $view->addAction( - id(new PhabricatorActionView()) - ->setIcon($helpful_icon) - ->setName($helpful_text) - ->setHref($helpful_uri) - ->setRenderAsForm(true) - ->setDisabled(!$can_vote) - ->setWorkflow($can_vote)); - $view->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') diff --git a/src/applications/ponder/view/PonderFooterView.php b/src/applications/ponder/view/PonderFooterView.php index be33863a9a..6d814d2b71 100644 --- a/src/applications/ponder/view/PonderFooterView.php +++ b/src/applications/ponder/view/PonderFooterView.php @@ -36,12 +36,8 @@ final class PonderFooterView extends AphrontTagView { $content_id = $this->contentID; if ($this->count == 0) { - $icon = id(new PHUIIconView()) - ->setIcon('fa-comments msr'); $text = pht('Add a Comment'); } else { - $icon = id(new PHUIIconView()) - ->setIcon('fa-comments msr'); $text = pht('Show %d Comment(s)', new PhutilNumber($this->count)); } @@ -58,7 +54,7 @@ final class PonderFooterView extends AphrontTagView { 'hideIDs' => array($hide_action_id), ), ), - array($icon, $text)); + array($text)); $show_action = javelin_tag( 'a', @@ -73,7 +69,7 @@ final class PonderFooterView extends AphrontTagView { 'hideIDs' => array($content_id, $show_action_id), ), ), - array($icon, pht('Hide Comments'))); + array(pht('Hide Comments'))); $actions[] = $hide_action; $actions[] = $show_action; diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 8220592074..ddbd0ffb5b 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -87,6 +87,7 @@ final class PhabricatorProjectProfileController $home = id(new PHUITwoColumnView()) ->setHeader($header) + ->setFluid(true) ->addClass('project-view-home') ->setMainColumn( array( diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index 819ab6c8e4..ff1eaae553 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -19,6 +19,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { private $objectPHID; private $headerText; private $noPermission; + private $fullWidth; private $currentVersion; private $versionedDraft; @@ -102,6 +103,11 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { return $this; } + public function setFullWidth($fw) { + $this->fullWidth = $fw; + return $this; + } + public function setCommentActions(array $comment_actions) { assert_instances_of($comment_actions, 'PhabricatorEditEngineCommentAction'); $this->commentActions = $comment_actions; @@ -209,6 +215,7 @@ class PhabricatorApplicationTransactionCommentView extends AphrontView { ->setUser($this->getUser()) ->addSigil('transaction-append') ->setWorkflow(true) + ->setFullWidth($this->fullWidth) ->setMetadata( array( 'objectPHID' => $this->getObjectPHID(), diff --git a/src/view/phui/PHUITwoColumnView.php b/src/view/phui/PHUITwoColumnView.php index 97ff269d83..5a47dc925a 100644 --- a/src/view/phui/PHUITwoColumnView.php +++ b/src/view/phui/PHUITwoColumnView.php @@ -5,6 +5,7 @@ final class PHUITwoColumnView extends AphrontTagView { private $mainColumn; private $sideColumn; private $display; + private $fluid; private $header; const DISPLAY_LEFT = 'phui-side-column-left'; @@ -25,12 +26,17 @@ final class PHUITwoColumnView extends AphrontTagView { return $this; } + public function setFluid($fluid) { + $this->fluid = $fluid; + return $this; + } + public function setDisplay($display) { $this->display = $display; return $this; } - public function getDisplay() { + private function getDisplay() { if ($this->display) { return $this->display; } else { @@ -43,6 +49,10 @@ final class PHUITwoColumnView extends AphrontTagView { $classes[] = 'phui-two-column-view'; $classes[] = $this->getDisplay(); + if ($this->fluid) { + $classes[] = 'phui-two-column-fluid'; + } + return array( 'class' => implode(' ', $classes), ); @@ -79,6 +89,14 @@ final class PHUITwoColumnView extends AphrontTagView { $header = phutil_tag_div('phui-two-column-header', $this->header); } - return array($header, $table); + return phutil_tag( + 'div', + array( + 'class' => 'phui-two-column-container', + ), + array( + $header, + $table, + )); } } diff --git a/webroot/rsrc/css/application/ponder/ponder-view.css b/webroot/rsrc/css/application/ponder/ponder-view.css index da0eab6e38..290bc77f06 100644 --- a/webroot/rsrc/css/application/ponder/ponder-view.css +++ b/webroot/rsrc/css/application/ponder/ponder-view.css @@ -2,16 +2,112 @@ * @provides ponder-view-css */ -.ponder-show-comments { - text-align: center; - padding: 8px; - margin: 0 16px; - float: right; - font-weight: bold; +.ponder-question-view { background: #fff; - border-bottom: 1px solid {$blueborder}; - border-left: 1px solid {$lightblueborder}; - border-right: 1px solid {$lightblueborder}; + padding-bottom: 64px; +} + +.device-desktop .ponder-question-view.phui-two-column-view .phui-side-column { + width: 300px; +} + +.ponder-question-view .phui-object-box, +.ponder-question-view .phui-info-view { + margin-left: 0; + margin-right: 0; +} + +.device-phone .ponder-question-view .phui-profile-header.phui-header-shell + .phui-header-header { + font-size: 20px; +} + +.ponder-question-container { + border-top: 1px solid {$thinblueborder}; +} + +.ponder-question-content { + margin: 0 24px; + padding: 24px 0; + border-top: 1px solid rgba({$alphagrey}, .15); +} + +.device-phone .ponder-question-content { + margin: 0 16px; +} + +.device .ponder-view-properties { + border-left: none; + border-right: none; + border-radius: 0; +} + +.device .ponder-question-view .phui-timeline-view, +.device .ponder-question-view .phui-timeline-event-view { + margin: 0; + padding: 0; +} + +.ponder-view-properties .phui-property-list-container { + margin: 0; + padding: 0; +} + +.ponder-question-view .phui-document-container { + border: none; +} + +.ponder-view-properties .phui-property-list-stacked + .phui-property-list-properties .phui-property-list-key { + padding: 0; +} + +.ponder-view-properties .phui-property-list-stacked + .phui-property-list-properties .phui-property-list-value { + margin-bottom: 16px; + padding: 0; +} + +.phui-box.ponder-answer-wiki { + padding: 16px; + margin: 24px 0; +} + +.ponder-details-subtitle { + height: 24px; + line-height: 24px; + margin-bottom: 12px; + color: {$greytext}; + position: relative; + padding-left: 32px; +} + +.ponder-details-subtitle a { + color: {$darkgreytext}; +} + +.ponder-details-author-image { + height: 24px; + width: 24px; + background-size: 100%; + margin-right: 8px; + border-radius: 3px; + display: inline-block; + position: absolute; + top: 0; + left: 0; +} + +.ponder-detail-view .phabricator-remarkup { + margin-left: 32px; +} + +.ponder-question-content .phui-timeline-view { + padding-right: 0; +} + +.ponder-question-content .phui-timeline-view .phui-timeline-core-content { + background-color: {$lightbluebackground}; } .ponder-answer-view { @@ -27,52 +123,102 @@ margin-left: 12px; } -.ponder-answer-view .phui-header-shell { - padding-bottom: 8px; +.ponder-question-view .ponder-answer-view .phui-header-shell { + padding: 4px 8px 3px 8px; +} + +.ponder-answer-view .phui-header-image-href { + display: flex; } .ponder-answer-view .phui-header-view .phui-header-header { - font-size: 16px; + font-size: 15px; } .ponder-answer-view .phui-header-col1 { - width: 45px; + width: 40px; +} + +.ponder-answer-view .ponder-answer-content { + background-color: #fff; + padding: 16px 16px 0 16px; +} + +.device-phone .ponder-answer-view .ponder-answer-content { + padding: 12px 12px 0 12px; } .ponder-answer-view .phui-header-image { - height: 35px; - width: 35px; + height: 30px; + width: 30px; border-radius: 3px; } +.ponder-answer-wiki-header { + font-weight: bold; + border-bottom: 1px solid {$lightblueborder}; + color: {$bluetext}; + padding-bottom: 8px; + margin-bottom: 16px; +} + .ponder-footer-view { - margin: 0 0 -4px; text-align: left; + margin-top: 16px; + border-bottom: 1px solid {$thinblueborder}; +} + +.ponder-answer-view .ponder-footer-view { + margin-top: 24px; + border-top: 1px solid rgba({$alphagrey}, .15); + border-bottom: none; +} + +body .phui-main-column .ponder-question-content .ponder-answer-view + .phui-object-box.ponder-answer { + margin: 0; + padding: 0; } .ponder-footer-view .ponder-footer-action { - padding: 4px 8px; + padding: 8px 0; margin-right: 8px; - color: {$anchor}; display: inline-block; - background-color: rgba({$alphablue}, 0.06); } -.ponder-footer-view .ponder-footer-action.ponder-footer-action-helpful { - background-color: {$lightyellow}; - color: {$bluetext}; +.ponder-add-answer-header { + margin-top: 64px; } -.ponder-footer-view .ponder-footer-action.ponder-footer-action-helpful - .phui-icon-view { - color: {$bluetext}; +.ponder-add-answer-view { + margin-top: 16px; } -.ponder-footer-view .ponder-footer-action .phui-icon-view { - color: {$anchor}; +.ponder-question-content div.ponder-question-add-comment-view + div.phui-box.phui-object-box { + background: {$lightbluebackground}; + margin-right: 0; + margin-left: 32px; } -.ponder-footer-view a:hover { - text-decoration: none; - background-color: rgba({$alphablue}, 0.10); +.device .ponder-question-content div.ponder-question-add-comment-view + div.phui-box.phui-object-box { + margin: 0; +} + +.ponder-add-answer-view .phui-form-full-width.phui-form-view + label.aphront-form-label, +.ponder-question-add-comment-view .phui-form-full-width.phui-form-view + label.aphront-form-label{ + display: none; +} + +.ponder-add-answer-view.phui-box-grey .phui-header-shell { + border: none; + padding-bottom: 8px; +} + +.ponder-add-answer-view .remarkup-assist-textarea, +.ponder-question-add-comment-view .remarkup-assist-textarea { + height: 8em; } diff --git a/webroot/rsrc/css/application/project/project-view.css b/webroot/rsrc/css/application/project/project-view.css index f84790cf12..be3e8b67f4 100644 --- a/webroot/rsrc/css/application/project/project-view.css +++ b/webroot/rsrc/css/application/project/project-view.css @@ -85,12 +85,9 @@ padding: 4px 8px 0 8px; } -.device-desktop .phui-two-column-view.project-view-badges .phui-side-column { - width: 366px; -} - .project-view-badges .phui-badge-flex-view { background-color: #fff; + width: 340px; } .project-view-home .phui-box-grey .phui-object-item-attribute .phui-icon-view { diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index 5dc0586667..c364059fc5 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -98,6 +98,12 @@ body .phui-header-shell.phui-bleed-header font-size: {$normalfontsize}; } +.phui-header-action-link { + margin-bottom: 4px; + margin-top: 4px; + float: right; +} + .device-phone .phui-header-action-link .phui-button-text { visibility: hidden; width: 0; diff --git a/webroot/rsrc/css/phui/phui-two-column-view.css b/webroot/rsrc/css/phui/phui-two-column-view.css index e08bd5a603..6e0880a4d9 100644 --- a/webroot/rsrc/css/phui/phui-two-column-view.css +++ b/webroot/rsrc/css/phui/phui-two-column-view.css @@ -2,6 +2,27 @@ * @provides phui-two-column-view-css */ +.phui-two-column-view { + background-color: #fff; +} + +.phui-two-column-container { + max-width: 1024px; + margin: 0 auto; +} + +.phui-two-column-view .phui-two-column-header .phui-header-shell { + padding-bottom: 32px; +} + +.device-phone .phui-two-column-view .phui-two-column-header .phui-header-shell { + padding-bottom: 20px; +} + +.phui-two-column-fluid .phui-two-column-container { + max-width: 100%; +} + .phui-two-column-content { display: table; width: 100%; From 03d6e7f1b699d89c829e92ba0da2178b41ad1d6a Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 23 Feb 2016 17:21:27 -0800 Subject: [PATCH 22/31] Correct an old issue with Paste by restoring the file attachment edge Summary: I'm having trouble figuring out exactly what the timeframe on this was, but for a while in November we were not writing edges between pastes and their attached files correctly. An example of this on this install is here: https://secure.phabricator.com/P1893 That will start working once the migration runs, but until it does it shows this: {F1126605} This got fixed so recent stuff works fine, but it looks like WMF updated while the bug was active so they have more affected pastes than we do (we only have about 10). Test Plan: Ran this query to find pastes with missing edges: ``` select id, FROM_UNIXTIME(p.dateCreated) from pastebin_paste p LEFT JOIN edge ON edge.src = p.phid AND edge.type = 25 WHERE edge.dst IS NULL order by id; ``` Ran the migration. Verified the edges were fixed. Viewed one of the affected pastes, things now worked properly. Reviewers: chad Reviewed By: chad Subscribers: 20after4 Differential Revision: https://secure.phabricator.com/D15340 --- .../autopatches/20160223.paste.fileedges.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 resources/sql/autopatches/20160223.paste.fileedges.php diff --git a/resources/sql/autopatches/20160223.paste.fileedges.php b/resources/sql/autopatches/20160223.paste.fileedges.php new file mode 100644 index 0000000000..915a2fcd0e --- /dev/null +++ b/resources/sql/autopatches/20160223.paste.fileedges.php @@ -0,0 +1,21 @@ +getPHID(); + $file_phid = $paste->getFilePHID(); + + if (!$file_phid) { + continue; + } + + id(new PhabricatorEdgeEditor()) + ->addEdge($paste_phid, $edge_type, $file_phid) + ->save(); +} From be0e84a81abc2f478a274ef46dfe51018ae0a9ed Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 23 Feb 2016 20:30:43 -0800 Subject: [PATCH 23/31] Fix Ponder Exception, spacing Summary: Evidently I only tested adding a question, not an answer. Properly set the getter. Also, fixed some header spacing. Test Plan: Add a question, add an answer. See everything work, proper spacing. Reviewers: epriestley, avivey Reviewed By: avivey Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15341 --- resources/celerity/map.php | 4 ++-- .../ponder/controller/PonderQuestionViewController.php | 7 ++++++- src/applications/ponder/storage/PonderAnswer.php | 3 ++- webroot/rsrc/css/application/ponder/ponder-view.css | 6 +++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 1f15126966..44383032b7 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -92,7 +92,7 @@ return array( 'rsrc/css/application/policy/policy-edit.css' => '815c66f7', 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', - 'rsrc/css/application/ponder/ponder-view.css' => 'fdd4629b', + 'rsrc/css/application/ponder/ponder-view.css' => '212495e0', 'rsrc/css/application/project/project-card-view.css' => '9418c97d', 'rsrc/css/application/project/project-view.css' => '298b7c5b', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', @@ -855,7 +855,7 @@ return array( 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', - 'ponder-view-css' => 'fdd4629b', + 'ponder-view-css' => '212495e0', 'project-card-view-css' => '9418c97d', 'project-view-css' => '298b7c5b', 'releeph-core' => '9b3c5733', diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 8f38a0573c..1b5b21e464 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -322,7 +322,12 @@ final class PonderQuestionViewController extends PonderController { $header = id(new PHUIHeaderView()) ->setHeader('Answers'); - return array($header, $view); + + + return id(new PHUIBoxView()) + ->addClass('ponder-answer-section') + ->appendChild($header) + ->appendChild($view); } return null; diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index 5fa1a1fc7a..025c020637 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -17,6 +17,7 @@ final class PonderAnswer extends PonderDAO protected $content; protected $mailKey; protected $status; + protected $voteCount; private $question = self::ATTACHABLE; private $comments; @@ -34,7 +35,7 @@ final class PonderAnswer extends PonderDAO ->setContent('') ->attachQuestion($question) ->setAuthorPHID($actor->getPHID()) - ->setVoteCount('0') + ->setVoteCount(0) ->setStatus(PonderAnswerStatus::ANSWER_STATUS_VISIBLE); } diff --git a/webroot/rsrc/css/application/ponder/ponder-view.css b/webroot/rsrc/css/application/ponder/ponder-view.css index 290bc77f06..a5bd01a6b1 100644 --- a/webroot/rsrc/css/application/ponder/ponder-view.css +++ b/webroot/rsrc/css/application/ponder/ponder-view.css @@ -169,7 +169,7 @@ } .ponder-answer-view .ponder-footer-view { - margin-top: 24px; + margin-top: 16px; border-top: 1px solid rgba({$alphagrey}, .15); border-bottom: none; } @@ -186,6 +186,10 @@ body .phui-main-column .ponder-question-content .ponder-answer-view display: inline-block; } +.ponder-answer-section { + margin-top: 32px; +} + .ponder-add-answer-header { margin-top: 64px; } From 944539a786796d460de673acc25774e7c82ab1fc Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 23 Feb 2016 16:23:40 -0800 Subject: [PATCH 24/31] Simplify locking of Almanac cluster services Summary: Fixes T6741. Ref T10246. Broadly, we want to protect Almanac cluster services: - Today, against users in the Phacility cluster accidentally breaking their own instances. - In the future, against attackers compromising administrative accounts and adding a new "cluster database" which points at hardware they control. The way this works right now is really complicated: there's a global "can create cluster services" setting, and then separate per-service and per-device locks. Instead, change "Can Create Cluster Services" into "Can Manage Cluster Services". Require this permission (in addition to normal permissions) to edit or create any cluster service. This permission can be locked to "No One" via config (as we do in the Phacility cluster) so we only need this one simple setting. There's also zero reason to individually lock //some// of the cluster services. Also improve extended policy errors. The UI here is still a little heavy-handed, but should be good enough for the moment. Test Plan: - Ran migrations. - Verified that cluster services and bindings reported that they belonged to the cluster. - Edited a cluster binding. - Verified that the bound device was marked as a cluster device - Moved a cluster binding, verified the old device was unmarked as a cluster device. - Tried to edit a cluster device as an unprivileged user, got a sensible error. {F1126552} Reviewers: chad Reviewed By: chad Maniphest Tasks: T6741, T10246 Differential Revision: https://secure.phabricator.com/D15339 --- .../autopatches/20160223.almanac.1.bound.sql | 2 + .../20160223.almanac.2.lockbind.sql | 2 + .../20160223.almanac.3.devicelock.sql | 2 + .../20160223.almanac.4.servicelock.sql | 2 + src/__phutil_library_map__.php | 11 ++- .../PhabricatorAlmanacApplication.php | 2 +- ...lmanacManageClusterServicesCapability.php} | 6 +- .../AlmanacBindingViewController.php | 8 ++- .../almanac/controller/AlmanacController.php | 24 ++++++- .../AlmanacDeviceViewController.php | 22 +++--- .../AlmanacServiceEditController.php | 4 +- .../AlmanacServiceViewController.php | 14 ++-- .../almanac/editor/AlmanacBindingEditor.php | 30 ++++++++ .../almanac/editor/AlmanacServiceEditor.php | 25 ------- .../AlmanacManagementLockWorkflow.php | 49 ------------- .../AlmanacManagementUnlockWorkflow.php | 49 ------------- .../query/AlmanacDeviceSearchEngine.php | 4 ++ .../almanac/query/AlmanacServiceQuery.php | 13 ---- .../query/AlmanacServiceSearchEngine.php | 9 --- .../servicetype/AlmanacClusterServiceType.php | 24 ------- .../servicetype/AlmanacServiceType.php | 4 -- .../almanac/storage/AlmanacBinding.php | 30 +++++--- .../almanac/storage/AlmanacDevice.php | 72 +++++++++++-------- .../almanac/storage/AlmanacInterface.php | 7 -- .../almanac/storage/AlmanacService.php | 37 ++++++---- .../policy/filter/PhabricatorPolicyFilter.php | 11 +-- src/docs/user/configuration/cluster.diviner | 19 +---- src/docs/user/userguide/almanac.diviner | 32 --------- 28 files changed, 193 insertions(+), 321 deletions(-) create mode 100644 resources/sql/autopatches/20160223.almanac.1.bound.sql create mode 100644 resources/sql/autopatches/20160223.almanac.2.lockbind.sql create mode 100644 resources/sql/autopatches/20160223.almanac.3.devicelock.sql create mode 100644 resources/sql/autopatches/20160223.almanac.4.servicelock.sql rename src/applications/almanac/capability/{AlmanacCreateClusterServicesCapability.php => AlmanacManageClusterServicesCapability.php} (57%) delete mode 100644 src/applications/almanac/management/AlmanacManagementLockWorkflow.php delete mode 100644 src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php diff --git a/resources/sql/autopatches/20160223.almanac.1.bound.sql b/resources/sql/autopatches/20160223.almanac.1.bound.sql new file mode 100644 index 0000000000..3e30fa458a --- /dev/null +++ b/resources/sql/autopatches/20160223.almanac.1.bound.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_device + ADD isBoundToClusterService BOOL NOT NULL; diff --git a/resources/sql/autopatches/20160223.almanac.2.lockbind.sql b/resources/sql/autopatches/20160223.almanac.2.lockbind.sql new file mode 100644 index 0000000000..93f5e8b0b5 --- /dev/null +++ b/resources/sql/autopatches/20160223.almanac.2.lockbind.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_almanac.almanac_device + SET isBoundToClusterService = isLocked; diff --git a/resources/sql/autopatches/20160223.almanac.3.devicelock.sql b/resources/sql/autopatches/20160223.almanac.3.devicelock.sql new file mode 100644 index 0000000000..fdb879fe28 --- /dev/null +++ b/resources/sql/autopatches/20160223.almanac.3.devicelock.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_device + DROP isLocked; diff --git a/resources/sql/autopatches/20160223.almanac.4.servicelock.sql b/resources/sql/autopatches/20160223.almanac.4.servicelock.sql new file mode 100644 index 0000000000..e716054c0c --- /dev/null +++ b/resources/sql/autopatches/20160223.almanac.4.servicelock.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_service + DROP isLocked; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a4b133893b..8ea70ec452 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -27,7 +27,6 @@ phutil_register_library_map(array( 'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', - 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', 'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', @@ -57,10 +56,9 @@ phutil_register_library_map(array( 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', - 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', + 'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php', 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', - 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', @@ -3996,6 +3994,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacBindingEditController' => 'AlmanacServiceController', 'AlmanacBindingEditor' => 'AlmanacEditor', @@ -4013,7 +4012,6 @@ phutil_register_library_map(array( 'AlmanacConduitAPIMethod' => 'ConduitAPIMethod', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', - 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', @@ -4030,6 +4028,7 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', @@ -4057,10 +4056,9 @@ phutil_register_library_map(array( 'AlmanacInterfaceQuery' => 'AlmanacQuery', 'AlmanacInterfaceTableView' => 'AphrontView', 'AlmanacKeys' => 'Phobject', - 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', + 'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', - 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacNames' => 'Phobject', @@ -4130,6 +4128,7 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php index 2e413b9fee..d0c543e9e1 100644 --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -93,7 +93,7 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { AlmanacCreateNamespacesCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), - AlmanacCreateClusterServicesCapability::CAPABILITY => array( + AlmanacManageClusterServicesCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, ), ); diff --git a/src/applications/almanac/capability/AlmanacCreateClusterServicesCapability.php b/src/applications/almanac/capability/AlmanacManageClusterServicesCapability.php similarity index 57% rename from src/applications/almanac/capability/AlmanacCreateClusterServicesCapability.php rename to src/applications/almanac/capability/AlmanacManageClusterServicesCapability.php index e0b1a8fb22..35d701b592 100644 --- a/src/applications/almanac/capability/AlmanacCreateClusterServicesCapability.php +++ b/src/applications/almanac/capability/AlmanacManageClusterServicesCapability.php @@ -1,17 +1,17 @@ setHeader($header) ->addPropertyList($property_list); - if ($binding->getService()->getIsLocked()) { - $this->addLockMessage( + if ($binding->getService()->isClusterService()) { + $this->addClusterMessage( $box, + pht('The service for this binding is a cluster service.'), pht( - 'This service for this binding is locked, so the binding can '. + 'The service for this binding is a cluster service. You do not '. + 'have permission to manage cluster services, so this binding can '. 'not be edited.')); } diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index 9673a0ef4a..369da4a4e7 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -166,7 +166,14 @@ abstract class AlmanacController ->setTable($table); } - protected function addLockMessage(PHUIObjectBoxView $box, $message) { + protected function addClusterMessage( + PHUIObjectBoxView $box, + $positive, + $negative) { + + $can_manage = $this->hasApplicationCapability( + AlmanacManageClusterServicesCapability::CAPABILITY); + $doc_link = phutil_tag( 'a', array( @@ -175,11 +182,22 @@ abstract class AlmanacController ), pht('Learn More')); + if ($can_manage) { + $severity = PHUIInfoView::SEVERITY_NOTICE; + $message = $positive; + } else { + $severity = PHUIInfoView::SEVERITY_WARNING; + $message = $negative; + } + + $icon = id(new PHUIIconView()) + ->setIcon('fa-sitemap'); + $error_view = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setSeverity($severity) ->setErrors( array( - array($message, ' ', $doc_link), + array($icon, ' ', $message, ' ', $doc_link), )); $box->setInfoView($error_view); diff --git a/src/applications/almanac/controller/AlmanacDeviceViewController.php b/src/applications/almanac/controller/AlmanacDeviceViewController.php index 89ec7198e3..4836198763 100644 --- a/src/applications/almanac/controller/AlmanacDeviceViewController.php +++ b/src/applications/almanac/controller/AlmanacDeviceViewController.php @@ -21,10 +21,6 @@ final class AlmanacDeviceViewController return new Aphront404Response(); } - // We rebuild locks on a device when viewing the detail page, so they - // automatically get corrected if they fall out of sync. - $device->rebuildDeviceLocks(); - $title = pht('Device %s', $device->getName()); $property_list = $this->buildPropertyList($device); @@ -40,12 +36,14 @@ final class AlmanacDeviceViewController ->setHeader($header) ->addPropertyList($property_list); - if ($device->getIsLocked()) { - $this->addLockMessage( + if ($device->isClusterDevice()) { + $this->addClusterMessage( $box, + pht('This device is bound to a cluster service.'), pht( - 'This device is bound to a locked service, so it can not be '. - 'edited.')); + 'This device is bound to a cluster service. You do not have '. + 'permission to manage cluster services, so the device can not '. + 'be edited.')); } $interfaces = $this->buildInterfaceList($device); @@ -219,14 +217,14 @@ final class AlmanacDeviceViewController $handles = $viewer->loadHandles(mpull($services, 'getPHID')); - $icon_lock = id(new PHUIIconView()) - ->setIcon('fa-lock'); + $icon_cluster = id(new PHUIIconView()) + ->setIcon('fa-sitemap'); $rows = array(); foreach ($services as $service) { $rows[] = array( - ($service->getIsLocked() - ? $icon_lock + ($service->isClusterService() + ? $icon_cluster : null), $handles->renderHandle($service->getPHID()), ); diff --git a/src/applications/almanac/controller/AlmanacServiceEditController.php b/src/applications/almanac/controller/AlmanacServiceEditController.php index 20a7528704..9a0d8d5d17 100644 --- a/src/applications/almanac/controller/AlmanacServiceEditController.php +++ b/src/applications/almanac/controller/AlmanacServiceEditController.php @@ -43,7 +43,7 @@ final class AlmanacServiceEditController $service_type = $service_types[$service_class]; if ($service_type->isClusterServiceType()) { $this->requireApplicationCapability( - AlmanacCreateClusterServicesCapability::CAPABILITY); + AlmanacManageClusterServicesCapability::CAPABILITY); } $service = AlmanacService::initializeNewService(); @@ -190,7 +190,7 @@ final class AlmanacServiceEditController } list($can_cluster, $cluster_link) = $this->explainApplicationCapability( - AlmanacCreateClusterServicesCapability::CAPABILITY, + AlmanacManageClusterServicesCapability::CAPABILITY, pht('You have permission to create cluster services.'), pht('You do not have permission to create new cluster services.')); diff --git a/src/applications/almanac/controller/AlmanacServiceViewController.php b/src/applications/almanac/controller/AlmanacServiceViewController.php index debc1bc063..735da73bcf 100644 --- a/src/applications/almanac/controller/AlmanacServiceViewController.php +++ b/src/applications/almanac/controller/AlmanacServiceViewController.php @@ -36,15 +36,13 @@ final class AlmanacServiceViewController ->setHeader($header) ->addPropertyList($property_list); - $messages = $service->getServiceType()->getStatusMessages($service); - if ($messages) { - $box->setFormErrors($messages); - } - - if ($service->getIsLocked()) { - $this->addLockMessage( + if ($service->isClusterService()) { + $this->addClusterMessage( $box, - pht('This service is locked, and can not be edited.')); + pht('This is a cluster service.'), + pht( + 'This service is a cluster service. You do not have permission to '. + 'edit cluster services, so you can not edit this service.')); } $bindings = $this->buildBindingList($service); diff --git a/src/applications/almanac/editor/AlmanacBindingEditor.php b/src/applications/almanac/editor/AlmanacBindingEditor.php index 6eea189cdd..86646c132f 100644 --- a/src/applications/almanac/editor/AlmanacBindingEditor.php +++ b/src/applications/almanac/editor/AlmanacBindingEditor.php @@ -3,6 +3,8 @@ final class AlmanacBindingEditor extends AlmanacEditor { + private $devicePHID; + public function getEditorObjectsDescription() { return pht('Almanac Binding'); } @@ -62,6 +64,34 @@ final class AlmanacBindingEditor switch ($xaction->getTransactionType()) { case AlmanacBindingTransaction::TYPE_INTERFACE: + $interface_phids = array(); + + $interface_phids[] = $xaction->getOldValue(); + $interface_phids[] = $xaction->getNewValue(); + + $interface_phids = array_filter($interface_phids); + $interface_phids = array_unique($interface_phids); + + $interfaces = id(new AlmanacInterfaceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($interface_phids) + ->execute(); + + $device_phids = array(); + foreach ($interfaces as $interface) { + $device_phids[] = $interface->getDevicePHID(); + } + + $device_phids = array_unique($device_phids); + + $devices = id(new AlmanacDeviceQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($device_phids) + ->execute(); + + foreach ($devices as $device) { + $device->rebuildClusterBindingStatus(); + } return; } diff --git a/src/applications/almanac/editor/AlmanacServiceEditor.php b/src/applications/almanac/editor/AlmanacServiceEditor.php index 8b8d2d2d67..1f6fe09cfa 100644 --- a/src/applications/almanac/editor/AlmanacServiceEditor.php +++ b/src/applications/almanac/editor/AlmanacServiceEditor.php @@ -11,7 +11,6 @@ final class AlmanacServiceEditor $types = parent::getTransactionTypes(); $types[] = AlmanacServiceTransaction::TYPE_NAME; - $types[] = AlmanacServiceTransaction::TYPE_LOCK; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -25,8 +24,6 @@ final class AlmanacServiceEditor switch ($xaction->getTransactionType()) { case AlmanacServiceTransaction::TYPE_NAME: return $object->getName(); - case AlmanacServiceTransaction::TYPE_LOCK: - return (bool)$object->getIsLocked(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -39,8 +36,6 @@ final class AlmanacServiceEditor switch ($xaction->getTransactionType()) { case AlmanacServiceTransaction::TYPE_NAME: return $xaction->getNewValue(); - case AlmanacServiceTransaction::TYPE_LOCK: - return (bool)$xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); @@ -54,9 +49,6 @@ final class AlmanacServiceEditor case AlmanacServiceTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; - case AlmanacServiceTransaction::TYPE_LOCK: - $object->setIsLocked((int)$xaction->getNewValue()); - return; } return parent::applyCustomInternalTransaction($object, $xaction); @@ -69,23 +61,6 @@ final class AlmanacServiceEditor switch ($xaction->getTransactionType()) { case AlmanacServiceTransaction::TYPE_NAME: return; - case AlmanacServiceTransaction::TYPE_LOCK: - $service = id(new AlmanacServiceQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPHIDs(array($object->getPHID())) - ->needBindings(true) - ->executeOne(); - - $devices = array(); - foreach ($service->getBindings() as $binding) { - $device = $binding->getInterface()->getDevice(); - $devices[$device->getPHID()] = $device; - } - - foreach ($devices as $device) { - $device->rebuildDeviceLocks(); - } - return; } return parent::applyCustomExternalTransaction($object, $xaction); diff --git a/src/applications/almanac/management/AlmanacManagementLockWorkflow.php b/src/applications/almanac/management/AlmanacManagementLockWorkflow.php deleted file mode 100644 index 60783d1bc5..0000000000 --- a/src/applications/almanac/management/AlmanacManagementLockWorkflow.php +++ /dev/null @@ -1,49 +0,0 @@ -setName('lock') - ->setSynopsis(pht('Lock a service to prevent it from being edited.')) - ->setArguments( - array( - array( - 'name' => 'services', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $services = $this->loadServices($args->getArg('services')); - if (!$services) { - throw new PhutilArgumentUsageException( - pht('Specify at least one service to lock.')); - } - - foreach ($services as $service) { - if ($service->getIsLocked()) { - throw new PhutilArgumentUsageException( - pht( - 'Service "%s" is already locked!', - $service->getName())); - } - } - - foreach ($services as $service) { - $this->updateServiceLock($service, true); - - $console->writeOut( - "** %s ** %s\n", - pht('LOCKED'), - pht('Service "%s" was locked.', $service->getName())); - } - - return 0; - } - -} diff --git a/src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php b/src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php deleted file mode 100644 index da8ba3dcc7..0000000000 --- a/src/applications/almanac/management/AlmanacManagementUnlockWorkflow.php +++ /dev/null @@ -1,49 +0,0 @@ -setName('unlock') - ->setSynopsis(pht('Unlock a service to allow it to be edited.')) - ->setArguments( - array( - array( - 'name' => 'services', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $services = $this->loadServices($args->getArg('services')); - if (!$services) { - throw new PhutilArgumentUsageException( - pht('Specify at least one service to unlock.')); - } - - foreach ($services as $service) { - if (!$service->getIsLocked()) { - throw new PhutilArgumentUsageException( - pht( - 'Service "%s" is not locked!', - $service->getName())); - } - } - - foreach ($services as $service) { - $this->updateServiceLock($service, false); - - $console->writeOut( - "** %s ** %s\n", - pht('UNLOCKED'), - pht('Service "%s" was unlocked.', $service->getName())); - } - - return 0; - } - -} diff --git a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php index a85c38e188..6e28d7e5b8 100644 --- a/src/applications/almanac/query/AlmanacDeviceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacDeviceSearchEngine.php @@ -84,6 +84,10 @@ final class AlmanacDeviceSearchEngine ->setHref($device->getURI()) ->setObject($device); + if ($device->isClusterDevice()) { + $item->addIcon('fa-sitemap', pht('Cluster Device')); + } + $list->addItem($item); } diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php index 2cfb7edb8f..e45cfec04b 100644 --- a/src/applications/almanac/query/AlmanacServiceQuery.php +++ b/src/applications/almanac/query/AlmanacServiceQuery.php @@ -8,7 +8,6 @@ final class AlmanacServiceQuery private $names; private $serviceClasses; private $devicePHIDs; - private $locked; private $namePrefix; private $nameSuffix; @@ -39,11 +38,6 @@ final class AlmanacServiceQuery return $this; } - public function withLocked($locked) { - $this->locked = $locked; - return $this; - } - public function withNamePrefix($prefix) { $this->namePrefix = $prefix; return $this; @@ -129,13 +123,6 @@ final class AlmanacServiceQuery $this->devicePHIDs); } - if ($this->locked !== null) { - $where[] = qsprintf( - $conn, - 'service.isLocked = %d', - (int)$this->locked); - } - if ($this->namePrefix !== null) { $where[] = qsprintf( $conn, diff --git a/src/applications/almanac/query/AlmanacServiceSearchEngine.php b/src/applications/almanac/query/AlmanacServiceSearchEngine.php index 4011d4fbc5..1a15e17d46 100644 --- a/src/applications/almanac/query/AlmanacServiceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacServiceSearchEngine.php @@ -101,15 +101,6 @@ final class AlmanacServiceSearchEngine $service->getServiceType()->getServiceTypeIcon(), $service->getServiceType()->getServiceTypeShortName()); - if ($service->getIsLocked() || - $service->getServiceType()->isClusterServiceType()) { - if ($service->getIsLocked()) { - $item->addIcon('fa-lock', pht('Locked')); - } else { - $item->addIcon('fa-unlock-alt red', pht('Unlocked')); - } - } - $list->addItem($item); } diff --git a/src/applications/almanac/servicetype/AlmanacClusterServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterServiceType.php index 7edbe52a73..2873141185 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterServiceType.php @@ -11,28 +11,4 @@ abstract class AlmanacClusterServiceType return 'fa-sitemap'; } - public function getStatusMessages(AlmanacService $service) { - $messages = parent::getStatusMessages($service); - - if (!$service->getIsLocked()) { - $doc_href = PhabricatorEnv::getDoclink( - 'User Guide: Phabricator Clusters'); - - $doc_link = phutil_tag( - 'a', - array( - 'href' => $doc_href, - 'target' => '_blank', - ), - pht('Learn More')); - - $messages[] = pht( - 'This is an unlocked cluster service. After you finish editing '. - 'it, you should lock it. %s.', - $doc_link); - } - - return $messages; - } - } diff --git a/src/applications/almanac/servicetype/AlmanacServiceType.php b/src/applications/almanac/servicetype/AlmanacServiceType.php index 408224f1a9..043da7d52e 100644 --- a/src/applications/almanac/servicetype/AlmanacServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacServiceType.php @@ -55,10 +55,6 @@ abstract class AlmanacServiceType extends Phobject { return array(); } - public function getStatusMessages(AlmanacService $service) { - return array(); - } - /** * List all available service type implementations. * diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 0d9cfa6fa3..030a3c7c57 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -6,7 +6,8 @@ final class AlmanacBinding PhabricatorPolicyInterface, PhabricatorApplicationTransactionInterface, AlmanacPropertyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorExtendedPolicyInterface { protected $servicePHID; protected $devicePHID; @@ -157,17 +158,30 @@ final class AlmanacBinding 'interface.'), ); - if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { - if ($this->getService()->getIsLocked()) { - $notes[] = pht( - 'The service for this binding is locked, so it can not be edited.'); - } - } - return $notes; } +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_EDIT: + if ($this->getService()->isClusterService()) { + return array( + array( + new PhabricatorAlmanacApplication(), + AlmanacManageClusterServicesCapability::CAPABILITY, + ), + ); + } + break; + } + + return array(); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index 7ef17bbc2b..f46b03600f 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -10,14 +10,15 @@ final class AlmanacDevice AlmanacPropertyInterface, PhabricatorDestructibleInterface, PhabricatorNgramsInterface, - PhabricatorConduitResultInterface { + PhabricatorConduitResultInterface, + PhabricatorExtendedPolicyInterface { protected $name; protected $nameIndex; protected $mailKey; protected $viewPolicy; protected $editPolicy; - protected $isLocked; + protected $isBoundToClusterService; private $almanacProperties = self::ATTACHABLE; @@ -26,7 +27,7 @@ final class AlmanacDevice ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) ->attachAlmanacProperties(array()) - ->setIsLocked(0); + ->setIsBoundToClusterService(0); } protected function getConfiguration() { @@ -36,7 +37,7 @@ final class AlmanacDevice 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', - 'isLocked' => 'bool', + 'isBoundToClusterService' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( @@ -70,30 +71,28 @@ final class AlmanacDevice return '/almanac/device/view/'.$this->getName().'/'; } - - /** - * Find locked services which are bound to this device, updating the device - * lock flag if necessary. - * - * @return list List of locking service PHIDs. - */ - public function rebuildDeviceLocks() { + public function rebuildClusterBindingStatus() { $services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withDevicePHIDs(array($this->getPHID())) - ->withLocked(true) ->execute(); - $locked = (bool)count($services); + $is_cluster = false; + foreach ($services as $service) { + if ($service->isClusterService()) { + $is_cluster = true; + break; + } + } - if ($locked != $this->getIsLocked()) { - $this->setIsLocked((int)$locked); + if ($is_cluster != $this->getIsBoundToClusterService()) { + $this->setIsBoundToClusterService((int)$is_cluster); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); queryfx( $this->establishConnection('w'), - 'UPDATE %T SET isLocked = %d WHERE id = %d', + 'UPDATE %T SET isBoundToClusterService = %d WHERE id = %d', $this->getTableName(), - $this->getIsLocked(), + $this->getIsBoundToClusterService(), $this->getID()); unset($unguarded); } @@ -101,6 +100,10 @@ final class AlmanacDevice return $this; } + public function isClusterDevice() { + return $this->getIsBoundToClusterService(); + } + /* -( AlmanacPropertyInterface )------------------------------------------- */ @@ -156,11 +159,7 @@ final class AlmanacDevice case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: - if ($this->getIsLocked()) { - return PhabricatorPolicies::POLICY_NOONE; - } else { - return $this->getEditPolicy(); - } + return $this->getEditPolicy(); } } @@ -169,15 +168,28 @@ final class AlmanacDevice } public function describeAutomaticCapability($capability) { - if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { - if ($this->getIsLocked()) { - return pht( - 'This device is bound to a locked service, so it can not '. - 'be edited.'); - } + return null; + } + + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_EDIT: + if ($this->isClusterDevice()) { + return array( + array( + new PhabricatorAlmanacApplication(), + AlmanacManageClusterServicesCapability::CAPABILITY, + ), + ); + } + break; } - return null; + return array(); } diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index 729e95aa9a..d1bd4f0aa4 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -101,13 +101,6 @@ final class AlmanacInterface 'view the interface.'), ); - if ($capability === PhabricatorPolicyCapability::CAN_EDIT) { - if ($this->getDevice()->getIsLocked()) { - $notes[] = pht( - 'The device for this interface is locked, so it can not be edited.'); - } - } - return $notes; } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 48eee1700b..92a5551188 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -9,7 +9,8 @@ final class AlmanacService AlmanacPropertyInterface, PhabricatorDestructibleInterface, PhabricatorNgramsInterface, - PhabricatorConduitResultInterface { + PhabricatorConduitResultInterface, + PhabricatorExtendedPolicyInterface { protected $name; protected $nameIndex; @@ -17,7 +18,6 @@ final class AlmanacService protected $viewPolicy; protected $editPolicy; protected $serviceClass; - protected $isLocked; private $almanacProperties = self::ATTACHABLE; private $bindings = self::ATTACHABLE; @@ -27,8 +27,7 @@ final class AlmanacService return id(new AlmanacService()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) - ->attachAlmanacProperties(array()) - ->setIsLocked(0); + ->attachAlmanacProperties(array()); } protected function getConfiguration() { @@ -39,7 +38,6 @@ final class AlmanacService 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', 'serviceClass' => 'text64', - 'isLocked' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( @@ -94,6 +92,10 @@ final class AlmanacService return $this; } + public function isClusterService() { + return $this->getServiceType()->isClusterServiceType(); + } + /* -( AlmanacPropertyInterface )------------------------------------------- */ @@ -149,11 +151,7 @@ final class AlmanacService case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: - if ($this->getIsLocked()) { - return PhabricatorPolicies::POLICY_NOONE; - } else { - return $this->getEditPolicy(); - } + return $this->getEditPolicy(); } } @@ -162,15 +160,28 @@ final class AlmanacService } public function describeAutomaticCapability($capability) { + return null; + } + + +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { switch ($capability) { case PhabricatorPolicyCapability::CAN_EDIT: - if ($this->getIsLocked()) { - return pht('This service is locked and can not be edited.'); + if ($this->isClusterService()) { + return array( + array( + new PhabricatorAlmanacApplication(), + AlmanacManageClusterServicesCapability::CAPABILITY, + ), + ); } break; } - return null; + return array(); } diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php index f70b77f99a..c3ac7fc016 100644 --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -381,10 +381,13 @@ final class PhabricatorPolicyFilter extends Phobject { $reject = $extended_objects[$extended_key]; unset($extended_objects[$extended_key]); - // TODO: This isn't as user-friendly as it could be. It's possible - // that we're rejecting this object for multiple capability/policy - // failures, though. - $this->rejectObject($reject, false, ''); + // It's possible that we're rejecting this object for multiple + // capability/policy failures, but just pick the first one to show + // to the user. + $first_capability = head($capabilities); + $first_policy = $object_in->getPolicy($first_capability); + + $this->rejectObject($reject, $first_policy, $first_capability); } } } diff --git a/src/docs/user/configuration/cluster.diviner b/src/docs/user/configuration/cluster.diviner index 708376ec23..160c42c8bc 100644 --- a/src/docs/user/configuration/cluster.diviner +++ b/src/docs/user/configuration/cluster.diviner @@ -12,20 +12,7 @@ yet. This document is mostly a placeholder. Locking Services ================ -Because cluster configuration is defined in Phabricator itself, an attacker -who compromises an account that can edit the cluster definition has significant -power. For example, the attacker might be able to configure Phabricator to -replicate the database to a server they control. +Very briefly, you can set "Can Manage Cluster Services" to "No One" to lock +the cluster definition. -To mitigate this attack, services in Almanac can be locked to prevent them -from being edited from the web UI. An attacker would then need significantly -greater access (to the CLI, or directly to the database) in order to change -the cluster configuration. - -You should normally keep cluster services in a locked state, and unlock them -only to edit them. Once you're finished making changes, lock the service again. -The web UI will warn you when you're viewing an unlocked cluster service, as -a reminder that you should lock it again once you're finished editing. - -For details on how to lock and unlock a service, see -@{article:Almanac User Guide}. +See also @{article:Almanac User Guide}. diff --git a/src/docs/user/userguide/almanac.diviner b/src/docs/user/userguide/almanac.diviner index d84176c57d..5a820c6ae2 100644 --- a/src/docs/user/userguide/almanac.diviner +++ b/src/docs/user/userguide/almanac.diviner @@ -177,35 +177,3 @@ Users can edit services and devices within a namespace if they have edit permission on the service or device itself, as long as they don't try to rename the service or device to move it into a namespace they don't have permission to access. - - -Locking and Unlocking Services -============================== - -Services can be locked to prevent edits from the web UI. This primarily hardens -Almanac against attacks involving account compromise. Notably, locking cluster -services prevents an attacker from modifying the Phabricator cluster definition. -For more details on this scenario, see -@{article:User Guide: Phabricator Clusters}. - -Beyond hardening cluster definitions, you might also want to lock a critical -service to prevent accidental edits. - -To lock a service, run: - - phabricator/ $ ./bin/almanac lock - -To unlock a service later, run: - - phabricator/ $ ./bin/almanac unlock - -Locking a service also locks all of the service's bindings and properties, as -well as the devices connected to the service. Generally, no part of the -service definition can be modified while it is locked. - -Devices (and their properties) will remain locked as long as they are bound to -at least one locked service. To edit a device, you'll need to unlock all the -services it is bound to. - -Locked services and devices will show that they are locked in the web UI, and -editing options will be unavailable. From 5b9d8aeae70118d1330b1d87717efd70c51d1f42 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 25 Feb 2016 03:07:47 -0800 Subject: [PATCH 25/31] Fix two issues with callsign-free repositories Summary: Ref T4245. These callsites don't quite do the right thing if a repository has no callsign. See also . Test Plan: Made a comment on a commit in a repository with no callsign. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4245 Differential Revision: https://secure.phabricator.com/D15344 --- .../audit/controller/PhabricatorAuditAddCommentController.php | 4 +--- .../diffusion/controller/DiffusionExternalController.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php index b4bb950806..5e50b2ae9d 100644 --- a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php +++ b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php @@ -82,9 +82,7 @@ final class PhabricatorAuditAddCommentController $draft->delete(); } - $monogram = $commit->getRepository()->getMonogram(); - $identifier = $commit->getCommitIdentifier(); - $uri = '/'.$monogram.$identifier; + $uri = $commit->getURI(); return id(new AphrontRedirectResponse())->setURI($uri); } diff --git a/src/applications/diffusion/controller/DiffusionExternalController.php b/src/applications/diffusion/controller/DiffusionExternalController.php index 09687f11d0..07a131c826 100644 --- a/src/applications/diffusion/controller/DiffusionExternalController.php +++ b/src/applications/diffusion/controller/DiffusionExternalController.php @@ -103,7 +103,7 @@ final class DiffusionExternalController extends DiffusionController { array( 'href' => $href, ), - $commit->getMonogram()), + $commit->getURI()), $commit->loadCommitData()->getSummary(), ); } From 4c97d88aa4b3564ff3a22baa1735d523585951dd Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 25 Feb 2016 03:52:41 -0800 Subject: [PATCH 26/31] Allow Almanac bindings to be disabled and unused interfaces to be removed Summary: Fixes T9762. Ref T10246. **Disabling Bindings**: Previously, there was no formal way to disable bindings. The internal callers sometimes check some informal property on the binding, but this is a common need and deserves first-class support in the UI. Allow bindings to be disabled. **Deleting Interfaces**: Previously, you could not delete interfaces. Now, you can delete unused interfaces. Also some minor cleanup and slightly less mysterious documentation. Test Plan: Disabled bindings and deleted interfaces. Reviewers: chad Reviewed By: chad Subscribers: yelirekim Maniphest Tasks: T9762, T10246 Differential Revision: https://secure.phabricator.com/D15345 --- .../20160225.almanac.1.disablebinding.sql | 2 + src/__phutil_library_map__.php | 5 ++ .../PhabricatorAlmanacApplication.php | 16 ++++- .../AlmanacBindingDisableController.php | 69 ++++++++++++++++++ .../AlmanacBindingViewController.php | 22 ++++++ .../almanac/controller/AlmanacController.php | 3 +- .../AlmanacInterfaceDeleteController.php | 72 +++++++++++++++++++ .../AlmanacServiceViewController.php | 3 +- .../almanac/editor/AlmanacBindingEditor.php | 10 +++ .../almanac/editor/AlmanacDeviceEditor.php | 13 ++++ .../AlmanacSearchEngineAttachment.php | 1 + .../almanac/storage/AlmanacBinding.php | 5 +- .../storage/AlmanacBindingTransaction.php | 12 ++++ .../storage/AlmanacDeviceTransaction.php | 2 +- .../almanac/storage/AlmanacInterface.php | 34 ++++++++- .../almanac/view/AlmanacBindingTableView.php | 36 ++++++++++ .../view/AlmanacInterfaceTableView.php | 17 ++++- ...anacServiceHostBlueprintImplementation.php | 5 ++ src/docs/user/configuration/cluster.diviner | 44 ++++++++++-- 19 files changed, 357 insertions(+), 14 deletions(-) create mode 100644 resources/sql/autopatches/20160225.almanac.1.disablebinding.sql create mode 100644 src/applications/almanac/controller/AlmanacBindingDisableController.php create mode 100644 src/applications/almanac/controller/AlmanacInterfaceDeleteController.php diff --git a/resources/sql/autopatches/20160225.almanac.1.disablebinding.sql b/resources/sql/autopatches/20160225.almanac.1.disablebinding.sql new file mode 100644 index 0000000000..0252e50ca7 --- /dev/null +++ b/resources/sql/autopatches/20160225.almanac.1.disablebinding.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_binding + ADD isDisabled BOOL NOT NULL; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8ea70ec452..c63eaa4b57 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -11,6 +11,7 @@ phutil_register_library_map(array( 'class' => array( 'AlmanacAddress' => 'applications/almanac/util/AlmanacAddress.php', 'AlmanacBinding' => 'applications/almanac/storage/AlmanacBinding.php', + 'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php', 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', @@ -51,6 +52,7 @@ phutil_register_library_map(array( 'AlmanacEditor' => 'applications/almanac/editor/AlmanacEditor.php', 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', + 'AlmanacInterfaceDeleteController' => 'applications/almanac/controller/AlmanacInterfaceDeleteController.php', 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', @@ -3996,6 +3998,7 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorExtendedPolicyInterface', ), + 'AlmanacBindingDisableController' => 'AlmanacServiceController', 'AlmanacBindingEditController' => 'AlmanacServiceController', 'AlmanacBindingEditor' => 'AlmanacEditor', 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', @@ -4049,8 +4052,10 @@ phutil_register_library_map(array( 'AlmanacDAO', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacInterfaceDatasource' => 'PhabricatorTypeaheadDatasource', + 'AlmanacInterfaceDeleteController' => 'AlmanacDeviceController', 'AlmanacInterfaceEditController' => 'AlmanacDeviceController', 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 'AlmanacInterfaceQuery' => 'AlmanacQuery', diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php index d0c543e9e1..08b9c5fbc8 100644 --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -55,9 +55,11 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { ), 'interface/' => array( 'edit/(?:(?P\d+)/)?' => 'AlmanacInterfaceEditController', + 'delete/(?:(?P\d+)/)?' => 'AlmanacInterfaceDeleteController', ), 'binding/' => array( 'edit/(?:(?P\d+)/)?' => 'AlmanacBindingEditController', + 'disable/(?:(?P\d+)/)?' => 'AlmanacBindingDisableController', '(?P\d+)/' => 'AlmanacBindingViewController', ), 'network/' => array( @@ -80,6 +82,17 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { } protected function getCustomCapabilities() { + $cluster_caption = pht( + 'This permission is very dangerous. %s', + phutil_tag( + 'a', + array( + 'href' => PhabricatorEnv::getDoclink( + 'User Guide: Phabricator Clusters'), + 'target' => '_blank', + ), + pht('Learn More'))); + return array( AlmanacCreateServicesCapability::CAPABILITY => array( 'default' => PhabricatorPolicies::POLICY_ADMIN, @@ -94,7 +107,8 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { 'default' => PhabricatorPolicies::POLICY_ADMIN, ), AlmanacManageClusterServicesCapability::CAPABILITY => array( - 'default' => PhabricatorPolicies::POLICY_ADMIN, + 'default' => PhabricatorPolicies::POLICY_NOONE, + 'caption' => $cluster_caption, ), ); } diff --git a/src/applications/almanac/controller/AlmanacBindingDisableController.php b/src/applications/almanac/controller/AlmanacBindingDisableController.php new file mode 100644 index 0000000000..eb1f093125 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacBindingDisableController.php @@ -0,0 +1,69 @@ +getViewer(); + + $id = $request->getURIData('id'); + $binding = id(new AlmanacBindingQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$binding) { + return new Aphront404Response(); + } + + $id = $binding->getID(); + $is_disable = !$binding->getIsDisabled(); + $done_uri = $binding->getURI(); + + if ($is_disable) { + $disable_title = pht('Disable Binding'); + $disable_body = pht('Disable this binding?'); + $disable_button = pht('Disable Binding'); + + $v_disable = 1; + } else { + $disable_title = pht('Enable Binding'); + $disable_body = pht('Enable this binding?'); + $disable_button = pht('Enable Binding'); + + $v_disable = 0; + } + + + if ($request->isFormPost()) { + $type_disable = AlmanacBindingTransaction::TYPE_DISABLE; + + $xactions = array(); + + $xactions[] = id(new AlmanacBindingTransaction()) + ->setTransactionType($type_disable) + ->setNewValue($v_disable); + + $editor = id(new AlmanacBindingEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($binding, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + return $this->newDialog() + ->setTitle($disable_title) + ->appendParagraph($disable_body) + ->addSubmitButton($disable_button) + ->addCancelButton($done_uri); + } + +} diff --git a/src/applications/almanac/controller/AlmanacBindingViewController.php b/src/applications/almanac/controller/AlmanacBindingViewController.php index 81f105b8eb..b75565525c 100644 --- a/src/applications/almanac/controller/AlmanacBindingViewController.php +++ b/src/applications/almanac/controller/AlmanacBindingViewController.php @@ -35,6 +35,10 @@ final class AlmanacBindingViewController ->setHeader($title) ->setPolicyObject($binding); + if ($binding->getIsDisabled()) { + $header->setStatus('fa-ban', 'red', pht('Disabled')); + } + $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($property_list); @@ -114,6 +118,24 @@ final class AlmanacBindingViewController ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); + if ($binding->getIsDisabled()) { + $disable_icon = 'fa-check'; + $disable_text = pht('Enable Binding'); + } else { + $disable_icon = 'fa-ban'; + $disable_text = pht('Disable Binding'); + } + + $disable_href = $this->getApplicationURI("binding/disable/{$id}/"); + + $actions->addAction( + id(new PhabricatorActionView()) + ->setIcon($disable_icon) + ->setName($disable_text) + ->setHref($disable_href) + ->setWorkflow(true) + ->setDisabled(!$can_edit)); + return $actions; } diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index 369da4a4e7..3b354b0803 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -177,7 +177,8 @@ abstract class AlmanacController $doc_link = phutil_tag( 'a', array( - 'href' => PhabricatorEnv::getDoclink('Almanac User Guide'), + 'href' => PhabricatorEnv::getDoclink( + 'User Guide: Phabricator Clusters'), 'target' => '_blank', ), pht('Learn More')); diff --git a/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php b/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php new file mode 100644 index 0000000000..ddbe979af6 --- /dev/null +++ b/src/applications/almanac/controller/AlmanacInterfaceDeleteController.php @@ -0,0 +1,72 @@ +getViewer(); + + $id = $request->getURIData('id'); + $interface = id(new AlmanacInterfaceQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$interface) { + return new Aphront404Response(); + } + + $device = $interface->getDevice(); + $device_uri = $device->getURI(); + + if ($interface->loadIsInUse()) { + return $this->newDialog() + ->setTitle(pht('Interface In Use')) + ->appendParagraph( + pht( + 'You can not delete this interface because it is currently in '. + 'use. One or more services are bound to it.')) + ->addCancelButton($device_uri); + } + + if ($request->isFormPost()) { + $type_interface = AlmanacDeviceTransaction::TYPE_INTERFACE; + + $xactions = array(); + + $v_old = array( + 'id' => $interface->getID(), + ) + $interface->toAddress()->toDictionary(); + + $xactions[] = id(new AlmanacDeviceTransaction()) + ->setTransactionType($type_interface) + ->setOldValue($v_old) + ->setNewValue(null); + + $editor = id(new AlmanacDeviceEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($device, $xactions); + + return id(new AphrontRedirectResponse())->setURI($device_uri); + } + + return $this->newDialog() + ->setTitle(pht('Delete Interface')) + ->appendParagraph( + pht( + 'Remove interface %s on device %s?', + phutil_tag('strong', array(), $interface->renderDisplayAddress()), + phutil_tag('strong', array(), $device->getName()))) + ->addCancelButton($device_uri) + ->addSubmitButton(pht('Delete Interface')); + } + +} diff --git a/src/applications/almanac/controller/AlmanacServiceViewController.php b/src/applications/almanac/controller/AlmanacServiceViewController.php index 735da73bcf..230cb56731 100644 --- a/src/applications/almanac/controller/AlmanacServiceViewController.php +++ b/src/applications/almanac/controller/AlmanacServiceViewController.php @@ -122,7 +122,8 @@ final class AlmanacServiceViewController ->setNoDataString( pht('This service has not been bound to any device interfaces yet.')) ->setUser($viewer) - ->setBindings($bindings); + ->setBindings($bindings) + ->setHideServiceColumn(true); $header = id(new PHUIHeaderView()) ->setHeader(pht('Service Bindings')) diff --git a/src/applications/almanac/editor/AlmanacBindingEditor.php b/src/applications/almanac/editor/AlmanacBindingEditor.php index 86646c132f..6532bf671f 100644 --- a/src/applications/almanac/editor/AlmanacBindingEditor.php +++ b/src/applications/almanac/editor/AlmanacBindingEditor.php @@ -13,6 +13,7 @@ final class AlmanacBindingEditor $types = parent::getTransactionTypes(); $types[] = AlmanacBindingTransaction::TYPE_INTERFACE; + $types[] = AlmanacBindingTransaction::TYPE_DISABLE; return $types; } @@ -23,6 +24,8 @@ final class AlmanacBindingEditor switch ($xaction->getTransactionType()) { case AlmanacBindingTransaction::TYPE_INTERFACE: return $object->getInterfacePHID(); + case AlmanacBindingTransaction::TYPE_DISABLE: + return $object->getIsDisabled(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -35,6 +38,8 @@ final class AlmanacBindingEditor switch ($xaction->getTransactionType()) { case AlmanacBindingTransaction::TYPE_INTERFACE: return $xaction->getNewValue(); + case AlmanacBindingTransaction::TYPE_DISABLE: + return (int)$xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); @@ -53,6 +58,9 @@ final class AlmanacBindingEditor $object->setDevicePHID($interface->getDevicePHID()); $object->setInterfacePHID($interface->getPHID()); return; + case AlmanacBindingTransaction::TYPE_DISABLE: + $object->setIsDisabled($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); @@ -63,6 +71,8 @@ final class AlmanacBindingEditor PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { + case AlmanacBindingTransaction::TYPE_DISABLE: + return; case AlmanacBindingTransaction::TYPE_INTERFACE: $interface_phids = array(); diff --git a/src/applications/almanac/editor/AlmanacDeviceEditor.php b/src/applications/almanac/editor/AlmanacDeviceEditor.php index 90928b1d7a..64b8de7bad 100644 --- a/src/applications/almanac/editor/AlmanacDeviceEditor.php +++ b/src/applications/almanac/editor/AlmanacDeviceEditor.php @@ -310,6 +310,19 @@ final class AlmanacDeviceEditor pht('You can not edit an invalid or restricted interface.'), $xaction); $errors[] = $error; + continue; + } + + $new = $xaction->getNewValue(); + if (!$new) { + if ($interface->loadIsInUse()) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('In Use'), + pht('You can not delete an interface which is still in use.'), + $xaction); + $errors[] = $error; + } } } } diff --git a/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php index 0875294bff..266b281990 100644 --- a/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php +++ b/src/applications/almanac/engineextension/AlmanacSearchEngineAttachment.php @@ -28,6 +28,7 @@ abstract class AlmanacSearchEngineAttachment 'phid' => $binding->getPHID(), 'properties' => $this->getAlmanacPropertyList($binding), 'interface' => $this->getAlmanacInterfaceDictionary($interface), + 'disabled' => (bool)$binding->getIsDisabled(), ); } diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 030a3c7c57..9d0fbc1622 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -13,6 +13,7 @@ final class AlmanacBinding protected $devicePHID; protected $interfacePHID; protected $mailKey; + protected $isDisabled; private $service = self::ATTACHABLE; private $device = self::ATTACHABLE; @@ -22,7 +23,8 @@ final class AlmanacBinding public static function initializeNewBinding(AlmanacService $service) { return id(new AlmanacBinding()) ->setServicePHID($service->getPHID()) - ->attachAlmanacProperties(array()); + ->attachAlmanacProperties(array()) + ->setIsDisabled(0); } protected function getConfiguration() { @@ -30,6 +32,7 @@ final class AlmanacBinding self::CONFIG_AUX_PHID => true, self::CONFIG_COLUMN_SCHEMA => array( 'mailKey' => 'bytes20', + 'isDisabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_service' => array( diff --git a/src/applications/almanac/storage/AlmanacBindingTransaction.php b/src/applications/almanac/storage/AlmanacBindingTransaction.php index bc9f071928..11120f9827 100644 --- a/src/applications/almanac/storage/AlmanacBindingTransaction.php +++ b/src/applications/almanac/storage/AlmanacBindingTransaction.php @@ -4,6 +4,7 @@ final class AlmanacBindingTransaction extends AlmanacTransaction { const TYPE_INTERFACE = 'almanac:binding:interface'; + const TYPE_DISABLE = 'almanac:binding:disable'; public function getApplicationName() { return 'almanac'; @@ -57,6 +58,17 @@ final class AlmanacBindingTransaction $this->renderHandleLink($new)); } break; + case self::TYPE_DISABLE: + if ($new) { + return pht( + '%s disabled this binding.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s enabled this binding.', + $this->renderHandleLink($author_phid)); + } + break; } return parent::getTitle(); diff --git a/src/applications/almanac/storage/AlmanacDeviceTransaction.php b/src/applications/almanac/storage/AlmanacDeviceTransaction.php index 5f42ec0210..fd1a21f782 100644 --- a/src/applications/almanac/storage/AlmanacDeviceTransaction.php +++ b/src/applications/almanac/storage/AlmanacDeviceTransaction.php @@ -69,7 +69,7 @@ final class AlmanacDeviceTransaction return pht( '%s removed the interface %s from this device.', $this->renderHandleLink($author_phid), - $this->describeInterface($new)); + $this->describeInterface($old)); } else if ($new) { return pht( '%s added the interface %s to this device.', diff --git a/src/applications/almanac/storage/AlmanacInterface.php b/src/applications/almanac/storage/AlmanacInterface.php index d1bd4f0aa4..d55c88b791 100644 --- a/src/applications/almanac/storage/AlmanacInterface.php +++ b/src/applications/almanac/storage/AlmanacInterface.php @@ -4,7 +4,8 @@ final class AlmanacInterface extends AlmanacDAO implements PhabricatorPolicyInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorExtendedPolicyInterface { protected $devicePHID; protected $networkPHID; @@ -74,6 +75,16 @@ final class AlmanacInterface return $this->getAddress().':'.$this->getPort(); } + public function loadIsInUse() { + $binding = id(new AlmanacBindingQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withInterfacePHIDs(array($this->getPHID())) + ->setLimit(1) + ->executeOne(); + + return (bool)$binding; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -105,6 +116,27 @@ final class AlmanacInterface } +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_EDIT: + if ($this->getDevice()->isClusterDevice()) { + return array( + array( + new PhabricatorAlmanacApplication(), + AlmanacManageClusterServicesCapability::CAPABILITY, + ), + ); + } + break; + } + + return array(); + } + + /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/almanac/view/AlmanacBindingTableView.php b/src/applications/almanac/view/AlmanacBindingTableView.php index 7e83d9e3d2..06797c912d 100644 --- a/src/applications/almanac/view/AlmanacBindingTableView.php +++ b/src/applications/almanac/view/AlmanacBindingTableView.php @@ -5,6 +5,8 @@ final class AlmanacBindingTableView extends AphrontView { private $bindings; private $noDataString; + private $hideServiceColumn; + public function setNoDataString($no_data_string) { $this->noDataString = $no_data_string; return $this; @@ -23,6 +25,15 @@ final class AlmanacBindingTableView extends AphrontView { return $this->bindings; } + public function setHideServiceColumn($hide_service_column) { + $this->hideServiceColumn = $hide_service_column; + return $this; + } + + public function getHideServiceColumn() { + return $this->hideServiceColumn; + } + public function render() { $bindings = $this->getBindings(); $viewer = $this->getUser(); @@ -35,6 +46,22 @@ final class AlmanacBindingTableView extends AphrontView { } $handles = $viewer->loadHandles($phids); + $icon_disabled = id(new PHUIIconView()) + ->setIcon('fa-ban') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Disabled'), + )); + + $icon_active = id(new PHUIIconView()) + ->setIcon('fa-check') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Active'), + )); + $rows = array(); foreach ($bindings as $binding) { $addr = $binding->getInterface()->getAddress(); @@ -42,6 +69,7 @@ final class AlmanacBindingTableView extends AphrontView { $rows[] = array( $binding->getID(), + ($binding->getIsDisabled() ? $icon_disabled : $icon_active), $handles->renderHandle($binding->getServicePHID()), $handles->renderHandle($binding->getDevicePHID()), $handles->renderHandle($binding->getInterface()->getNetworkPHID()), @@ -61,6 +89,7 @@ final class AlmanacBindingTableView extends AphrontView { ->setHeaders( array( pht('ID'), + null, pht('Service'), pht('Device'), pht('Network'), @@ -70,11 +99,18 @@ final class AlmanacBindingTableView extends AphrontView { ->setColumnClasses( array( '', + 'icon', '', '', '', 'wide', 'action', + )) + ->setColumnVisibility( + array( + true, + true, + !$this->getHideServiceColumn(), )); return $table; diff --git a/src/applications/almanac/view/AlmanacInterfaceTableView.php b/src/applications/almanac/view/AlmanacInterfaceTableView.php index 48214da078..f937e34f44 100644 --- a/src/applications/almanac/view/AlmanacInterfaceTableView.php +++ b/src/applications/almanac/view/AlmanacInterfaceTableView.php @@ -27,7 +27,9 @@ final class AlmanacInterfaceTableView extends AphrontView { $interfaces = $this->getInterfaces(); $viewer = $this->getUser(); - if ($this->getCanEdit()) { + $can_edit = $this->getCanEdit(); + + if ($can_edit) { $button_class = 'small grey button'; } else { $button_class = 'small grey button disabled'; @@ -42,13 +44,22 @@ final class AlmanacInterfaceTableView extends AphrontView { $handles->renderHandle($interface->getNetworkPHID()), $interface->getAddress(), $interface->getPort(), - phutil_tag( + javelin_tag( 'a', array( 'class' => $button_class, 'href' => '/almanac/interface/edit/'.$interface->getID().'/', + 'sigil' => ($can_edit ? null : 'workflow'), ), pht('Edit')), + javelin_tag( + 'a', + array( + 'class' => $button_class, + 'href' => '/almanac/interface/delete/'.$interface->getID().'/', + 'sigil' => 'workflow', + ), + pht('Delete')), ); } @@ -60,6 +71,7 @@ final class AlmanacInterfaceTableView extends AphrontView { pht('Address'), pht('Port'), null, + null, )) ->setColumnClasses( array( @@ -68,6 +80,7 @@ final class AlmanacInterfaceTableView extends AphrontView { '', '', 'action', + 'action', )); return $table; diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php index 19e476c4ba..3a742a72c2 100644 --- a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -267,6 +267,11 @@ final class DrydockAlmanacServiceHostBlueprintImplementation $free = array(); foreach ($bindings as $binding) { + // Don't consider disabled bindings to be available. + if ($binding->getIsDisabled()) { + continue; + } + if (empty($allocated_phids[$binding->getPHID()])) { $free[] = $binding; } diff --git a/src/docs/user/configuration/cluster.diviner b/src/docs/user/configuration/cluster.diviner index 160c42c8bc..c11565dc4d 100644 --- a/src/docs/user/configuration/cluster.diviner +++ b/src/docs/user/configuration/cluster.diviner @@ -1,7 +1,7 @@ @title User Guide: Phabricator Clusters @group config -Guide on scaling Phabricator across multiple machines, for large installs. +Guide on scaling Phabricator across multiple machines. Overview ======== @@ -9,10 +9,42 @@ Overview IMPORTANT: Phabricator clustering is in its infancy and does not work at all yet. This document is mostly a placeholder. -Locking Services -================ - -Very briefly, you can set "Can Manage Cluster Services" to "No One" to lock -the cluster definition. +IMPORTANT: DO NOT CONFIGURE CLUSTER SERVICES UNLESS YOU HAVE **TWENTY YEARS OF +EXPERIENCE WITH PHABRICATOR** AND **A MINIMUM OF 17 PHABRICATOR PHDs**. YOU +WILL BREAK YOUR INSTALL AND BE UNABLE TO REPAIR IT. See also @{article:Almanac User Guide}. + + +Managing Cluster Configuration +============================== + +Cluster configuration is managed primarily from the **Almanac** application. + +To define cluster services and create or edit cluster configuration, you must +have the **Can Manage Cluster Services** application permission in Almanac. If +you do not have this permission, all cluster services and all connected devices +will be locked and not editable. + +The **Can Manage Cluster Services** permission is stronger than service and +device policies, and overrides them. You can never edit a cluster service if +you don't have this permission, even if the **Can Edit** policy on the service +itself is very permissive. + + +Locking Cluster Configuration +============================= + +IMPORTANT: Managing cluster services is **dangerous** and **fragile**. + +If you make a mistake, you can break your install. Because the install is +broken, you will be unable to load the web interface in order to repair it. + +IMPORTANT: Currently, broken clusters must be repaired by manually fixing them +in the database. There are no instructions available on how to do this, and no +tools to help you. Do not configure cluster services. + +If an attacker gains access to an account with permission to manage cluster +services, they can add devices they control as database servers. These servers +will then receive sensitive data and traffic, and allow the attacker to +escalate their access and completely compromise an install. From 7d4b323da2bd6ba7f110ad4b88c2d775e2a55f97 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 25 Feb 2016 05:05:36 -0800 Subject: [PATCH 27/31] Store Almanac "service types" instead of "service classes" Summary: Ref T10449. Currently, we store classes (like "AlmanacClusterRepositoryServiceType") in the database. Instead, store types (like "cluster.repository"). This is a small change, but types are a little more flexible (they let us freely reanme classes), a little cleaner (fewer magic strings in the codebase), and a little better for API usage (they're more human readable). Make this minor usability change now, before we unprototype. Also make services searchable by type. Also remove old Almanac API endpoints. Test Plan: - Ran migration, verified all data migrated properly. - Created, edited, rebound, and changed properties of services. - Searched for services by service type. - Reviewed available Conduit methods. Reviewers: chad Reviewed By: chad Subscribers: yelirekim Maniphest Tasks: T10449 Differential Revision: https://secure.phabricator.com/D15346 --- .../autopatches/20160225.almanac.2.stype.sql | 2 + .../autopatches/20160225.almanac.3.stype.php | 30 +++++++ src/__phutil_library_map__.php | 8 +- .../conduit/AlmanacConduitAPIMethod.php | 73 ---------------- .../AlmanacQueryDevicesConduitAPIMethod.php | 63 -------------- .../AlmanacQueryServicesConduitAPIMethod.php | 86 ------------------- .../AlmanacServiceEditController.php | 26 +++--- .../AlmanacServiceViewController.php | 2 +- .../almanac/query/AlmanacServiceQuery.php | 24 +++--- .../query/AlmanacServiceSearchEngine.php | 15 +++- .../AlmanacClusterDatabaseServiceType.php | 2 + .../AlmanacClusterRepositoryServiceType.php | 2 + .../servicetype/AlmanacCustomServiceType.php | 2 + .../AlmanacDrydockPoolServiceType.php | 2 + .../servicetype/AlmanacServiceType.php | 8 +- .../almanac/storage/AlmanacProperty.php | 40 +++++++++ .../almanac/storage/AlmanacService.php | 56 +++++++++--- .../typeahead/AlmanacServiceDatasource.php | 6 +- .../AlmanacServiceTypeDatasource.php | 43 ++++++++++ .../PhabricatorRepositoriesSetupCheck.php | 4 +- .../DiffusionRepositoryCreateController.php | 4 +- ...anacServiceHostBlueprintImplementation.php | 8 +- .../storage/PhabricatorRepository.php | 2 +- 23 files changed, 226 insertions(+), 282 deletions(-) create mode 100644 resources/sql/autopatches/20160225.almanac.2.stype.sql create mode 100644 resources/sql/autopatches/20160225.almanac.3.stype.php delete mode 100644 src/applications/almanac/conduit/AlmanacConduitAPIMethod.php delete mode 100644 src/applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php delete mode 100644 src/applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php create mode 100644 src/applications/almanac/typeahead/AlmanacServiceTypeDatasource.php diff --git a/resources/sql/autopatches/20160225.almanac.2.stype.sql b/resources/sql/autopatches/20160225.almanac.2.stype.sql new file mode 100644 index 0000000000..fcd054ff23 --- /dev/null +++ b/resources/sql/autopatches/20160225.almanac.2.stype.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_almanac.almanac_service + CHANGE serviceClass serviceType VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160225.almanac.3.stype.php b/resources/sql/autopatches/20160225.almanac.3.stype.php new file mode 100644 index 0000000000..f6f19eabd3 --- /dev/null +++ b/resources/sql/autopatches/20160225.almanac.3.stype.php @@ -0,0 +1,30 @@ +establishConnection('w'); + +foreach (new LiskMigrationIterator($table) as $service) { + + $new_type = null; + try { + $old_type = $service->getServiceType(); + $object = newv($old_type, array()); + $new_type = $object->getServiceTypeConstant(); + } catch (Exception $ex) { + continue; + } + + if (!$new_type) { + continue; + } + + queryfx( + $conn_w, + 'UPDATE %T SET serviceType = %s WHERE id = %d', + $table->getTableName(), + $new_type, + $service->getID()); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index c63eaa4b57..d01c2d6af9 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -25,7 +25,6 @@ phutil_register_library_map(array( 'AlmanacClusterDatabaseServiceType' => 'applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php', 'AlmanacClusterRepositoryServiceType' => 'applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php', 'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php', - 'AlmanacConduitAPIMethod' => 'applications/almanac/conduit/AlmanacConduitAPIMethod.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', @@ -101,8 +100,6 @@ phutil_register_library_map(array( 'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php', 'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php', 'AlmanacQuery' => 'applications/almanac/query/AlmanacQuery.php', - 'AlmanacQueryDevicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php', - 'AlmanacQueryServicesConduitAPIMethod' => 'applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php', 'AlmanacSchemaSpec' => 'applications/almanac/storage/AlmanacSchemaSpec.php', 'AlmanacSearchEngineAttachment' => 'applications/almanac/engineextension/AlmanacSearchEngineAttachment.php', 'AlmanacService' => 'applications/almanac/storage/AlmanacService.php', @@ -120,6 +117,7 @@ phutil_register_library_map(array( 'AlmanacServiceTransaction' => 'applications/almanac/storage/AlmanacServiceTransaction.php', 'AlmanacServiceTransactionQuery' => 'applications/almanac/query/AlmanacServiceTransactionQuery.php', 'AlmanacServiceType' => 'applications/almanac/servicetype/AlmanacServiceType.php', + 'AlmanacServiceTypeDatasource' => 'applications/almanac/typeahead/AlmanacServiceTypeDatasource.php', 'AlmanacServiceTypeTestCase' => 'applications/almanac/servicetype/__tests__/AlmanacServiceTypeTestCase.php', 'AlmanacServiceViewController' => 'applications/almanac/controller/AlmanacServiceViewController.php', 'AlmanacTransaction' => 'applications/almanac/storage/AlmanacTransaction.php', @@ -4012,7 +4010,6 @@ phutil_register_library_map(array( 'AlmanacClusterDatabaseServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterRepositoryServiceType' => 'AlmanacClusterServiceType', 'AlmanacClusterServiceType' => 'AlmanacServiceType', - 'AlmanacConduitAPIMethod' => 'ConduitAPIMethod', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', @@ -4120,8 +4117,6 @@ phutil_register_library_map(array( 'AlmanacPropertyEditEngine' => 'PhabricatorEditEngine', 'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'AlmanacQueryDevicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', - 'AlmanacQueryServicesConduitAPIMethod' => 'AlmanacConduitAPIMethod', 'AlmanacSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'AlmanacSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'AlmanacService' => array( @@ -4149,6 +4144,7 @@ phutil_register_library_map(array( 'AlmanacServiceTransaction' => 'AlmanacTransaction', 'AlmanacServiceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacServiceType' => 'Phobject', + 'AlmanacServiceTypeDatasource' => 'PhabricatorTypeaheadDatasource', 'AlmanacServiceTypeTestCase' => 'PhabricatorTestCase', 'AlmanacServiceViewController' => 'AlmanacServiceController', 'AlmanacTransaction' => 'PhabricatorApplicationTransaction', diff --git a/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php deleted file mode 100644 index b7eec60cb4..0000000000 --- a/src/applications/almanac/conduit/AlmanacConduitAPIMethod.php +++ /dev/null @@ -1,73 +0,0 @@ - (int)$service->getID(), - 'phid' => $service->getPHID(), - 'name' => $service->getName(), - 'uri' => PhabricatorEnv::getProductionURI($service->getURI()), - 'serviceClass' => $service->getServiceClass(), - 'properties' => $this->getPropertiesDictionary($service), - ); - } - - protected function getBindingDictionary(AlmanacBinding $binding) { - return array( - 'id' => (int)$binding->getID(), - 'phid' => $binding->getPHID(), - 'properties' => $this->getPropertiesDictionary($binding), - 'interface' => $this->getInterfaceDictionary($binding->getInterface()), - ); - } - - protected function getPropertiesDictionary(AlmanacPropertyInterface $obj) { - $properties = $obj->getAlmanacProperties(); - return (object)mpull($properties, 'getFieldValue', 'getFieldName'); - } - - protected function getInterfaceDictionary(AlmanacInterface $interface) { - return array( - 'id' => (int)$interface->getID(), - 'phid' => $interface->getPHID(), - 'address' => $interface->getAddress(), - 'port' => (int)$interface->getPort(), - 'device' => $this->getDeviceDictionary($interface->getDevice()), - 'network' => $this->getNetworkDictionary($interface->getNetwork()), - ); - } - - protected function getDeviceDictionary(AlmanacDevice $device) { - return array( - 'id' => (int)$device->getID(), - 'phid' => $device->getPHID(), - 'name' => $device->getName(), - 'properties' => $this->getPropertiesDictionary($device), - ); - } - - protected function getNetworkDictionary(AlmanacNetwork $network) { - return array( - 'id' => (int)$network->getID(), - 'phid' => $network->getPHID(), - 'name' => $network->getName(), - ); - } - -} diff --git a/src/applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php deleted file mode 100644 index b0a593a381..0000000000 --- a/src/applications/almanac/conduit/AlmanacQueryDevicesConduitAPIMethod.php +++ /dev/null @@ -1,63 +0,0 @@ - 'optional list', - 'phids' => 'optional list', - 'names' => 'optional list', - ) + self::getPagerParamTypes(); - } - - protected function defineReturnType() { - return 'list'; - } - - protected function execute(ConduitAPIRequest $request) { - $viewer = $request->getUser(); - - $query = id(new AlmanacDeviceQuery()) - ->setViewer($viewer); - - $ids = $request->getValue('ids'); - if ($ids !== null) { - $query->withIDs($ids); - } - - $phids = $request->getValue('phids'); - if ($phids !== null) { - $query->withPHIDs($phids); - } - - $names = $request->getValue('names'); - if ($names !== null) { - $query->withNames($names); - } - - $pager = $this->newPager($request); - - $devices = $query->executeWithCursorPager($pager); - - $data = array(); - foreach ($devices as $device) { - $data[] = $this->getDeviceDictionary($device); - } - - $results = array( - 'data' => $data, - ); - - return $this->addPagerResults($results, $pager); - } - -} diff --git a/src/applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php b/src/applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php deleted file mode 100644 index 81e59d072f..0000000000 --- a/src/applications/almanac/conduit/AlmanacQueryServicesConduitAPIMethod.php +++ /dev/null @@ -1,86 +0,0 @@ - 'optional list', - 'phids' => 'optional list', - 'names' => 'optional list', - 'devicePHIDs' => 'optional list', - 'serviceClasses' => 'optional list', - ) + self::getPagerParamTypes(); - } - - protected function defineReturnType() { - return 'list'; - } - - protected function execute(ConduitAPIRequest $request) { - $viewer = $request->getUser(); - - $query = id(new AlmanacServiceQuery()) - ->setViewer($viewer) - ->needBindings(true); - - $ids = $request->getValue('ids'); - if ($ids !== null) { - $query->withIDs($ids); - } - - $phids = $request->getValue('phids'); - if ($phids !== null) { - $query->withPHIDs($phids); - } - - $names = $request->getValue('names'); - if ($names !== null) { - $query->withNames($names); - } - - $classes = $request->getValue('serviceClasses'); - if ($classes !== null) { - $query->withServiceClasses($classes); - } - - $device_phids = $request->getValue('devicePHIDs'); - if ($device_phids !== null) { - $query->withDevicePHIDs($device_phids); - } - - $pager = $this->newPager($request); - - $services = $query->executeWithCursorPager($pager); - - $data = array(); - foreach ($services as $service) { - $phid = $service->getPHID(); - - $service_bindings = $service->getBindings(); - $service_bindings = array_values($service_bindings); - foreach ($service_bindings as $key => $service_binding) { - $service_bindings[$key] = $this->getBindingDictionary($service_binding); - } - - $data[] = $this->getServiceDictionary($service) + array( - 'bindings' => $service_bindings, - ); - } - - $results = array( - 'data' => $data, - ); - - return $this->addPagerResults($results, $pager); - } - -} diff --git a/src/applications/almanac/controller/AlmanacServiceEditController.php b/src/applications/almanac/controller/AlmanacServiceEditController.php index 9a0d8d5d17..2b6f5a9f5d 100644 --- a/src/applications/almanac/controller/AlmanacServiceEditController.php +++ b/src/applications/almanac/controller/AlmanacServiceEditController.php @@ -34,21 +34,19 @@ final class AlmanacServiceEditController $this->requireApplicationCapability( AlmanacCreateServicesCapability::CAPABILITY); - $service_class = $request->getStr('serviceClass'); - $service_types = AlmanacServiceType::getAllServiceTypes(); - if (empty($service_types[$service_class])) { - return $this->buildServiceTypeResponse($service_types, $cancel_uri); + $service_type = $request->getStr('serviceType'); + + try { + $service = AlmanacService::initializeNewService($service_type); + } catch (Exception $ex) { + return $this->buildServiceTypeResponse($cancel_uri); } - $service_type = $service_types[$service_class]; - if ($service_type->isClusterServiceType()) { + if ($service->isClusterService()) { $this->requireApplicationCapability( AlmanacManageClusterServicesCapability::CAPABILITY); } - $service = AlmanacService::initializeNewService(); - $service->setServiceClass($service_class); - $service->attachServiceType($service_type); $is_new = true; $title = pht('Create Service'); @@ -125,7 +123,7 @@ final class AlmanacServiceEditController $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('edit', true) - ->addHiddenInput('serviceClass', $service->getServiceClass()) + ->addHiddenInput('serviceType', $service->getServiceType()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) @@ -177,7 +175,9 @@ final class AlmanacServiceEditController )); } - private function buildServiceTypeResponse(array $service_types, $cancel_uri) { + private function buildServiceTypeResponse($cancel_uri) { + $service_types = AlmanacServiceType::getAllServiceTypes(); + $request = $this->getRequest(); $viewer = $this->getViewer(); @@ -197,7 +197,7 @@ final class AlmanacServiceEditController $type_control = id(new AphrontFormRadioButtonControl()) ->setLabel(pht('Service Type')) - ->setName('serviceClass') + ->setName('serviceType') ->setError($e_service); foreach ($service_types as $service_type) { @@ -211,7 +211,7 @@ final class AlmanacServiceEditController } $type_control->addButton( - get_class($service_type), + $service_type->getServiceTypeConstant(), $service_type->getServiceTypeName(), array( $service_type->getServiceTypeDescription(), diff --git a/src/applications/almanac/controller/AlmanacServiceViewController.php b/src/applications/almanac/controller/AlmanacServiceViewController.php index 230cb56731..68defec4b9 100644 --- a/src/applications/almanac/controller/AlmanacServiceViewController.php +++ b/src/applications/almanac/controller/AlmanacServiceViewController.php @@ -76,7 +76,7 @@ final class AlmanacServiceViewController $properties->addProperty( pht('Service Type'), - $service->getServiceType()->getServiceTypeShortName()); + $service->getServiceImplementation()->getServiceTypeShortName()); return $properties; } diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php index e45cfec04b..3374413e5b 100644 --- a/src/applications/almanac/query/AlmanacServiceQuery.php +++ b/src/applications/almanac/query/AlmanacServiceQuery.php @@ -6,7 +6,7 @@ final class AlmanacServiceQuery private $ids; private $phids; private $names; - private $serviceClasses; + private $serviceTypes; private $devicePHIDs; private $namePrefix; private $nameSuffix; @@ -28,8 +28,8 @@ final class AlmanacServiceQuery return $this; } - public function withServiceClasses(array $classes) { - $this->serviceClasses = $classes; + public function withServiceTypes(array $types) { + $this->serviceTypes = $types; return $this; } @@ -109,11 +109,11 @@ final class AlmanacServiceQuery $hashes); } - if ($this->serviceClasses !== null) { + if ($this->serviceTypes !== null) { $where[] = qsprintf( $conn, - 'service.serviceClass IN (%Ls)', - $this->serviceClasses); + 'service.serviceType IN (%Ls)', + $this->serviceTypes); } if ($this->devicePHIDs !== null) { @@ -141,17 +141,19 @@ final class AlmanacServiceQuery } protected function willFilterPage(array $services) { - $service_types = AlmanacServiceType::getAllServiceTypes(); + $service_map = AlmanacServiceType::getAllServiceTypes(); foreach ($services as $key => $service) { - $service_class = $service->getServiceClass(); - $service_type = idx($service_types, $service_class); - if (!$service_type) { + $implementation = idx($service_map, $service->getServiceType()); + + if (!$implementation) { $this->didRejectResult($service); unset($services[$key]); continue; } - $service->attachServiceType($service_type); + + $implementation = clone $implementation; + $service->attachServiceImplementation($implementation); } return $services; diff --git a/src/applications/almanac/query/AlmanacServiceSearchEngine.php b/src/applications/almanac/query/AlmanacServiceSearchEngine.php index 1a15e17d46..1a9509a2c2 100644 --- a/src/applications/almanac/query/AlmanacServiceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacServiceSearchEngine.php @@ -16,7 +16,7 @@ final class AlmanacServiceSearchEngine } public function newResultObject() { - return AlmanacService::initializeNewService(); + return new AlmanacService(); } protected function buildQueryFromParameters(array $map) { @@ -34,6 +34,10 @@ final class AlmanacServiceSearchEngine $query->withDevicePHIDs($map['devicePHIDs']); } + if ($map['serviceTypes']) { + $query->withServiceTypes($map['serviceTypes']); + } + return $query; } @@ -48,6 +52,11 @@ final class AlmanacServiceSearchEngine ->setLabel(pht('Exact Names')) ->setKey('names') ->setDescription(pht('Search for services with specific names.')), + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Service Types')) + ->setKey('serviceTypes') + ->setDescription(pht('Find services by type.')) + ->setDatasource(id(new AlmanacServiceTypeDatasource())), id(new PhabricatorPHIDsSearchField()) ->setLabel(pht('Devices')) ->setKey('devicePHIDs') @@ -98,8 +107,8 @@ final class AlmanacServiceSearchEngine ->setHref($service->getURI()) ->setObject($service) ->addIcon( - $service->getServiceType()->getServiceTypeIcon(), - $service->getServiceType()->getServiceTypeShortName()); + $service->getServiceImplementation()->getServiceTypeIcon(), + $service->getServiceImplementation()->getServiceTypeShortName()); $list->addItem($item); } diff --git a/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php index bbf7e9878c..ba06a78d52 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterDatabaseServiceType.php @@ -3,6 +3,8 @@ final class AlmanacClusterDatabaseServiceType extends AlmanacClusterServiceType { + const SERVICETYPE = 'cluster.database'; + public function getServiceTypeShortName() { return pht('Cluster Database'); } diff --git a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php index d33ee7167b..259092b5b1 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php @@ -3,6 +3,8 @@ final class AlmanacClusterRepositoryServiceType extends AlmanacClusterServiceType { + const SERVICETYPE = 'cluster.repository'; + public function getServiceTypeShortName() { return pht('Cluster Repository'); } diff --git a/src/applications/almanac/servicetype/AlmanacCustomServiceType.php b/src/applications/almanac/servicetype/AlmanacCustomServiceType.php index 3a4affbb8e..f4df57a868 100644 --- a/src/applications/almanac/servicetype/AlmanacCustomServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacCustomServiceType.php @@ -2,6 +2,8 @@ final class AlmanacCustomServiceType extends AlmanacServiceType { + const SERVICETYPE = 'almanac.custom'; + public function getServiceTypeShortName() { return pht('Custom'); } diff --git a/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php b/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php index 24880565d0..4c7de3a361 100644 --- a/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php @@ -2,6 +2,8 @@ final class AlmanacDrydockPoolServiceType extends AlmanacServiceType { + const SERVICETYPE = 'drydock.pool'; + public function getServiceTypeShortName() { return pht('Drydock Pool'); } diff --git a/src/applications/almanac/servicetype/AlmanacServiceType.php b/src/applications/almanac/servicetype/AlmanacServiceType.php index 043da7d52e..2eb56dc8c4 100644 --- a/src/applications/almanac/servicetype/AlmanacServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacServiceType.php @@ -30,6 +30,11 @@ abstract class AlmanacServiceType extends Phobject { abstract public function getServiceTypeDescription(); + final public function getServiceTypeConstant() { + return $this->getPhobjectClassConstant('SERVICETYPE', 64); + } + + public function getServiceTypeIcon() { return 'fa-cog'; } @@ -38,7 +43,7 @@ abstract class AlmanacServiceType extends Phobject { * Return `true` if this service type is a Phabricator cluster service type. * * These special services change the behavior of Phabricator, and require - * elevated permission to create. + * elevated permission to create and edit. * * @return bool True if this is a Phabricator cluster service type. */ @@ -63,6 +68,7 @@ abstract class AlmanacServiceType extends Phobject { public static function getAllServiceTypes() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getServiceTypeConstant') ->setSortMethod('getServiceTypeName') ->execute(); } diff --git a/src/applications/almanac/storage/AlmanacProperty.php b/src/applications/almanac/storage/AlmanacProperty.php index e695da59ec..0d317f8c8e 100644 --- a/src/applications/almanac/storage/AlmanacProperty.php +++ b/src/applications/almanac/storage/AlmanacProperty.php @@ -39,6 +39,46 @@ final class AlmanacProperty return $this; } + public static function newPropertyUpdateTransactions( + AlmanacPropertyInterface $object, + array $properties, + $only_builtins = false) { + + $template = $object->getApplicationTransactionTemplate(); + $builtins = $object->getAlmanacPropertyFieldSpecifications(); + + $xactions = array(); + foreach ($properties as $name => $property) { + if ($only_builtins && empty($builtins[$name])) { + continue; + } + + $xactions[] = id(clone $template) + ->setTransactionType(AlmanacTransaction::TYPE_PROPERTY_UPDATE) + ->setMetadataValue('almanac.property', $name) + ->setNewValue($property); + } + + return $xactions; + } + + public static function newPropertyRemoveTransactions( + AlmanacPropertyInterface $object, + array $properties) { + + $template = $object->getApplicationTransactionTemplate(); + + $xactions = array(); + foreach ($properties as $property) { + $xactions[] = id(clone $template) + ->setTransactionType(AlmanacTransaction::TYPE_PROPERTY_REMOVE) + ->setMetadataValue('almanac.property', $property) + ->setNewValue(null); + } + + return $xactions; + } + public function save() { $hash = PhabricatorHash::digestForIndex($this->getFieldName()); $this->setFieldIndex($hash); diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index 92a5551188..b280de9ba8 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -17,17 +17,29 @@ final class AlmanacService protected $mailKey; protected $viewPolicy; protected $editPolicy; - protected $serviceClass; + protected $serviceType; private $almanacProperties = self::ATTACHABLE; private $bindings = self::ATTACHABLE; - private $serviceType = self::ATTACHABLE; + private $serviceImplementation = self::ATTACHABLE; + + public static function initializeNewService($type) { + $type_map = AlmanacServiceType::getAllServiceTypes(); + + $implementation = idx($type_map, $type); + if (!$implementation) { + throw new Exception( + pht( + 'No Almanac service type "%s" exists!', + $type)); + } - public static function initializeNewService() { return id(new AlmanacService()) ->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setEditPolicy(PhabricatorPolicies::POLICY_ADMIN) - ->attachAlmanacProperties(array()); + ->attachAlmanacProperties(array()) + ->setServiceType($type) + ->attachServiceImplementation($implementation); } protected function getConfiguration() { @@ -37,7 +49,7 @@ final class AlmanacService 'name' => 'text128', 'nameIndex' => 'bytes12', 'mailKey' => 'bytes20', - 'serviceClass' => 'text64', + 'serviceType' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( @@ -47,8 +59,8 @@ final class AlmanacService 'key_nametext' => array( 'columns' => array('name'), ), - 'key_class' => array( - 'columns' => array('serviceClass'), + 'key_servicetype' => array( + 'columns' => array('serviceType'), ), ), ) + parent::getConfiguration(); @@ -78,22 +90,35 @@ final class AlmanacService return $this->assertAttached($this->bindings); } + public function getActiveBindings() { + $bindings = $this->getBindings(); + + // Filter out disabled bindings. + foreach ($bindings as $key => $binding) { + if ($binding->getIsDisabled()) { + unset($bindings[$key]); + } + } + + return $bindings; + } + public function attachBindings(array $bindings) { $this->bindings = $bindings; return $this; } - public function getServiceType() { - return $this->assertAttached($this->serviceType); + public function getServiceImplementation() { + return $this->assertAttached($this->serviceImplementation); } - public function attachServiceType(AlmanacServiceType $type) { - $this->serviceType = $type; + public function attachServiceImplementation(AlmanacServiceType $type) { + $this->serviceImplementation = $type; return $this; } public function isClusterService() { - return $this->getServiceType()->isClusterServiceType(); + return $this->getServiceImplementation()->isClusterServiceType(); } @@ -128,7 +153,7 @@ final class AlmanacService } public function getAlmanacPropertyFieldSpecifications() { - return $this->getServiceType()->getFieldSpecifications(); + return $this->getServiceImplementation()->getFieldSpecifications(); } public function newAlmanacPropertyEditEngine() { @@ -246,12 +271,17 @@ final class AlmanacService ->setKey('name') ->setType('string') ->setDescription(pht('The name of the service.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('serviceType') + ->setType('string') + ->setDescription(pht('The service type constant.')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), + 'serviceType' => $this->getServiceType(), ); } diff --git a/src/applications/almanac/typeahead/AlmanacServiceDatasource.php b/src/applications/almanac/typeahead/AlmanacServiceDatasource.php index 621b0408ae..4413465125 100644 --- a/src/applications/almanac/typeahead/AlmanacServiceDatasource.php +++ b/src/applications/almanac/typeahead/AlmanacServiceDatasource.php @@ -28,9 +28,9 @@ final class AlmanacServiceDatasource // selected, or show all services but mark the invalid ones disabled and // prevent their selection. - $service_classes = $this->getParameter('serviceClasses'); - if ($service_classes) { - $services->withServiceClasses($service_classes); + $service_types = $this->getParameter('serviceTypes'); + if ($service_types) { + $services->withServiceTypes($service_types); } $services = $this->executeQuery($services); diff --git a/src/applications/almanac/typeahead/AlmanacServiceTypeDatasource.php b/src/applications/almanac/typeahead/AlmanacServiceTypeDatasource.php new file mode 100644 index 0000000000..5314084244 --- /dev/null +++ b/src/applications/almanac/typeahead/AlmanacServiceTypeDatasource.php @@ -0,0 +1,43 @@ +buildResults(); + return $this->filterResultsAgainstTokens($results); + } + + protected function renderSpecialTokens(array $values) { + return $this->renderTokensFromResults($this->buildResults(), $values); + } + + private function buildResults() { + $results = array(); + + $types = AlmanacServiceType::getAllServiceTypes(); + + $results = array(); + foreach ($types as $key => $type) { + $results[$key] = id(new PhabricatorTypeaheadResult()) + ->setName($type->getServiceTypeName()) + ->setIcon($type->getServiceTypeIcon()) + ->setPHID($key); + } + + return $results; + } + +} diff --git a/src/applications/config/check/PhabricatorRepositoriesSetupCheck.php b/src/applications/config/check/PhabricatorRepositoriesSetupCheck.php index 89967f7eaa..64073ec1ed 100644 --- a/src/applications/config/check/PhabricatorRepositoriesSetupCheck.php +++ b/src/applications/config/check/PhabricatorRepositoriesSetupCheck.php @@ -10,9 +10,9 @@ final class PhabricatorRepositoriesSetupCheck extends PhabricatorSetupCheck { $cluster_services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withServiceClasses( + ->withServiceTypes( array( - 'AlmanacClusterRepositoryServiceType', + AlmanacClusterRepositoryServiceType::SERVICETYPE, )) ->setLimit(1) ->execute(); diff --git a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php index bfd01ff30c..a9113fee13 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryCreateController.php @@ -47,9 +47,9 @@ final class DiffusionRepositoryCreateController // allocations, we fail. $services = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withServiceClasses( + ->withServiceTypes( array( - 'AlmanacClusterRepositoryServiceType', + AlmanacClusterRepositoryServiceType::SERVICETYPE, )) ->needProperties(true) ->execute(); diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php index 3a742a72c2..443b0e8965 100644 --- a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -184,7 +184,7 @@ final class DrydockAlmanacServiceHostBlueprintImplementation 'type' => 'datasource', 'datasource.class' => 'AlmanacServiceDatasource', 'datasource.parameters' => array( - 'serviceClasses' => $this->getAlmanacServiceClasses(), + 'serviceTypes' => $this->getAlmanacServiceTypes(), ), 'required' => true, ), @@ -213,7 +213,7 @@ final class DrydockAlmanacServiceHostBlueprintImplementation $services = id(new AlmanacServiceQuery()) ->setViewer($viewer) ->withPHIDs($service_phids) - ->withServiceClasses($this->getAlmanacServiceClasses()) + ->withServiceTypes($this->getAlmanacServiceTypes()) ->needBindings(true) ->execute(); $services = mpull($services, null, 'getPHID'); @@ -283,9 +283,9 @@ final class DrydockAlmanacServiceHostBlueprintImplementation return $this->freeBindings; } - private function getAlmanacServiceClasses() { + private function getAlmanacServiceTypes() { return array( - 'AlmanacDrydockPoolServiceType', + AlmanacDrydockPoolServiceType::SERVICETYPE, ); } diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index e6510e7ad9..9896a8ef29 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2047,7 +2047,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO 'be loaded.')); } - $service_type = $service->getServiceType(); + $service_type = $service->getServiceImplementation(); if (!($service_type instanceof AlmanacClusterRepositoryServiceType)) { throw new Exception( pht( From 6861af0cbb0b7dfa5456be6223f2621dd6a1116e Mon Sep 17 00:00:00 2001 From: Nick Zheng Date: Fri, 26 Feb 2016 09:38:24 -0800 Subject: [PATCH 28/31] When the push phase of "Land Revision" fails, show the error in the UI Summary: T10447 Test Plan: tested on my dev instance Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: Korvin, yelirekim Differential Revision: https://secure.phabricator.com/D15350 --- src/__phutil_library_map__.php | 12 ++++++---- .../DrydockCommandError.php | 18 ++++++++++++++ ...dockWorkingCopyBlueprintImplementation.php | 24 +++---------------- .../DrydockLandRepositoryOperation.php | 16 ++++++++++++- .../storage/DrydockRepositoryOperation.php | 4 ++-- .../DrydockRepositoryOperationStatusView.php | 8 ++++++- ...DrydockRepositoryOperationUpdateWorker.php | 4 ++-- 7 files changed, 54 insertions(+), 32 deletions(-) create mode 100644 src/applications/drydock/DrydockCommandError/DrydockCommandError.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d01c2d6af9..bd41673e72 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -27,6 +27,7 @@ phutil_register_library_map(array( 'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', + 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', 'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', @@ -57,9 +58,10 @@ phutil_register_library_map(array( 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', - 'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php', + 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', + 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', @@ -971,6 +973,7 @@ phutil_register_library_map(array( 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', 'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php', 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', + 'DrydockCommandError' => 'applications/drydock/DrydockCommandError/DrydockCommandError.php', 'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php', 'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php', 'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php', @@ -3994,7 +3997,6 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', - 'PhabricatorExtendedPolicyInterface', ), 'AlmanacBindingDisableController' => 'AlmanacServiceController', 'AlmanacBindingEditController' => 'AlmanacServiceController', @@ -4012,6 +4014,7 @@ phutil_register_library_map(array( 'AlmanacClusterServiceType' => 'AlmanacServiceType', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', + 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', @@ -4028,7 +4031,6 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', - 'PhabricatorExtendedPolicyInterface', ), 'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', @@ -4058,9 +4060,10 @@ phutil_register_library_map(array( 'AlmanacInterfaceQuery' => 'AlmanacQuery', 'AlmanacInterfaceTableView' => 'AphrontView', 'AlmanacKeys' => 'Phobject', - 'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability', + 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', + 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacNames' => 'Phobject', @@ -4128,7 +4131,6 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', - 'PhabricatorExtendedPolicyInterface', ), 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', diff --git a/src/applications/drydock/DrydockCommandError/DrydockCommandError.php b/src/applications/drydock/DrydockCommandError/DrydockCommandError.php new file mode 100644 index 0000000000..d9acbe7456 --- /dev/null +++ b/src/applications/drydock/DrydockCommandError/DrydockCommandError.php @@ -0,0 +1,18 @@ + $phase, + 'command' => (string)$command, + 'raw' => (string)$ex->getCommand(), + 'err' => $ex->getError(), + 'stdout' => $ex->getStdout(), + 'stderr' => $ex->getStderr(), + ); + return $error; + } +} diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index d60a65580b..d2f7191c06 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -448,35 +448,17 @@ final class DrydockWorkingCopyBlueprintImplementation try { $interface->execx('%C', $real_command); } catch (CommandException $ex) { - $this->setWorkingCopyVCSErrorFromCommandException( - $lease, + $error = DrydockCommandError::newFromCommandException( self::PHASE_SQUASHMERGE, $show_command, $ex); + $lease->setAttribute('workingcopy.vcs.error', $error); throw $ex; } } - protected function setWorkingCopyVCSErrorFromCommandException( - DrydockLease $lease, - $phase, - $command, - CommandException $ex) { - - $error = array( - 'phase' => $phase, - 'command' => (string)$command, - 'raw' => (string)$ex->getCommand(), - 'err' => $ex->getError(), - 'stdout' => $ex->getStdout(), - 'stderr' => $ex->getStderr(), - ); - - $lease->setAttribute('workingcopy.vcs.error', $error); - } - - public function getWorkingCopyVCSError(DrydockLease $lease) { + public function getCommandError(DrydockLease $lease) { $error = $lease->getAttribute('workingcopy.vcs.error'); if (!$error) { return null; diff --git a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php index af2f9f271f..5beb18d07b 100644 --- a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php +++ b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php @@ -4,6 +4,7 @@ final class DrydockLandRepositoryOperation extends DrydockRepositoryOperationType { const OPCONST = 'land'; + const PHASE_PUSH = 'push'; public function getOperationDescription( DrydockRepositoryOperation $operation, @@ -122,10 +123,23 @@ final class DrydockLandRepositoryOperation ->write($commit_message) ->resolvex(); - $interface->execx( + try { + $interface->execx( + 'git push origin -- %s:%s', + 'HEAD', + $push_dst); + } catch (CommandException $ex) { + $show_command = csprintf( 'git push origin -- %s:%s', 'HEAD', $push_dst); + $error = DrydockCommandError::newFromCommandException( + self::PHASE_PUSH, + $show_command, + $ex); + $operation->setCommandError($error); + throw $ex; + } } private function getCommitterInfo(DrydockRepositoryOperation $operation) { diff --git a/src/applications/drydock/storage/DrydockRepositoryOperation.php b/src/applications/drydock/storage/DrydockRepositoryOperation.php index 87565edc86..190b5b1310 100644 --- a/src/applications/drydock/storage/DrydockRepositoryOperation.php +++ b/src/applications/drydock/storage/DrydockRepositoryOperation.php @@ -178,11 +178,11 @@ final class DrydockRepositoryOperation extends DrydockDAO return $this->getProperty('exec.leasePHID'); } - public function setWorkingCopyVCSError(array $error) { + public function setCommandError(array $error) { return $this->setProperty('exec.workingcopy.error', $error); } - public function getWorkingCopyVCSError() { + public function getCommandError() { return $this->getProperty('exec.workingcopy.error'); } diff --git a/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php b/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php index 07c5a7b706..23ad6b81fe 100644 --- a/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php +++ b/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php @@ -74,7 +74,7 @@ final class DrydockRepositoryOperationStatusView if ($state != DrydockRepositoryOperation::STATE_FAIL) { $item->addAttribute($operation->getOperationCurrentStatus($viewer)); } else { - $vcs_error = $operation->getWorkingCopyVCSError(); + $vcs_error = $operation->getCommandError(); if ($vcs_error) { switch ($vcs_error['phase']) { case DrydockWorkingCopyBlueprintImplementation::PHASE_SQUASHMERGE: @@ -82,6 +82,12 @@ final class DrydockRepositoryOperationStatusView 'This change did not merge cleanly. This usually indicates '. 'that the change is out of date and needs to be updated.'); break; + case DrydockLandRepositoryOperation::PHASE_PUSH: + $message = pht( + 'The push failed. This usually indicates '. + 'that the change is breaking some rules or '. + 'some custom commit hook has failed.'); + break; default: $message = pht( 'Operation encountered an error while performing repository '. diff --git a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php index 6160575fd0..9038b3b6d7 100644 --- a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php +++ b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php @@ -138,10 +138,10 @@ final class DrydockRepositoryOperationUpdateWorker } if (!$lease->isActive()) { - $vcs_error = $working_copy->getWorkingCopyVCSError($lease); + $vcs_error = $working_copy->getCommandError($lease); if ($vcs_error) { $operation - ->setWorkingCopyVCSError($vcs_error) + ->setCommandError($vcs_error) ->save(); } From 0290cf08168551eb0491b232eff95518bff2a38a Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 26 Feb 2016 09:42:23 -0800 Subject: [PATCH 29/31] Library Map Update Summary: Our of date, failed harbormaster build Test Plan: run `arc liberate` Reviewers: epriestley, nickz Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15351 --- src/__phutil_library_map__.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index bd41673e72..a4cea24c74 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -27,7 +27,6 @@ phutil_register_library_map(array( 'AlmanacClusterServiceType' => 'applications/almanac/servicetype/AlmanacClusterServiceType.php', 'AlmanacConsoleController' => 'applications/almanac/controller/AlmanacConsoleController.php', 'AlmanacController' => 'applications/almanac/controller/AlmanacController.php', - 'AlmanacCreateClusterServicesCapability' => 'applications/almanac/capability/AlmanacCreateClusterServicesCapability.php', 'AlmanacCreateDevicesCapability' => 'applications/almanac/capability/AlmanacCreateDevicesCapability.php', 'AlmanacCreateNamespacesCapability' => 'applications/almanac/capability/AlmanacCreateNamespacesCapability.php', 'AlmanacCreateNetworksCapability' => 'applications/almanac/capability/AlmanacCreateNetworksCapability.php', @@ -58,10 +57,9 @@ phutil_register_library_map(array( 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', - 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', + 'AlmanacManageClusterServicesCapability' => 'applications/almanac/capability/AlmanacManageClusterServicesCapability.php', 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', - 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', 'AlmanacManagementWorkflow' => 'applications/almanac/management/AlmanacManagementWorkflow.php', 'AlmanacNames' => 'applications/almanac/util/AlmanacNames.php', @@ -885,6 +883,7 @@ phutil_register_library_map(array( 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', 'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php', 'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php', + 'DrydockCommandError' => 'applications/drydock/DrydockCommandError/DrydockCommandError.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', 'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php', 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', @@ -973,7 +972,6 @@ phutil_register_library_map(array( 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', 'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php', 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', - 'DrydockCommandError' => 'applications/drydock/DrydockCommandError/DrydockCommandError.php', 'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php', 'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php', 'FeedPublisherHTTPWorker' => 'applications/feed/worker/FeedPublisherHTTPWorker.php', @@ -3997,6 +3995,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'AlmanacPropertyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacBindingDisableController' => 'AlmanacServiceController', 'AlmanacBindingEditController' => 'AlmanacServiceController', @@ -4014,7 +4013,6 @@ phutil_register_library_map(array( 'AlmanacClusterServiceType' => 'AlmanacServiceType', 'AlmanacConsoleController' => 'AlmanacController', 'AlmanacController' => 'PhabricatorController', - 'AlmanacCreateClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateDevicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNamespacesCapability' => 'PhabricatorPolicyCapability', 'AlmanacCreateNetworksCapability' => 'PhabricatorPolicyCapability', @@ -4031,6 +4029,7 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacDeviceController' => 'AlmanacController', 'AlmanacDeviceEditController' => 'AlmanacDeviceController', @@ -4060,10 +4059,9 @@ phutil_register_library_map(array( 'AlmanacInterfaceQuery' => 'AlmanacQuery', 'AlmanacInterfaceTableView' => 'AphrontView', 'AlmanacKeys' => 'Phobject', - 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', + 'AlmanacManageClusterServicesCapability' => 'PhabricatorPolicyCapability', 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', - 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow', 'AlmanacManagementWorkflow' => 'PhabricatorManagementWorkflow', 'AlmanacNames' => 'Phobject', @@ -4131,6 +4129,7 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', + 'PhabricatorExtendedPolicyInterface', ), 'AlmanacServiceController' => 'AlmanacController', 'AlmanacServiceDatasource' => 'PhabricatorTypeaheadDatasource', From c64b822bee621a14323e4615177c7cd36db9379a Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 26 Feb 2016 10:29:17 -0800 Subject: [PATCH 30/31] Remove obsolete, confusing Harbormaster builds steps Summary: Fixes T10458. These steps are obsolete and have not worked since the last updates to Drydock. They may eventually return in some form, but get rid of them for now since they're confusing. Test Plan: - Created a build plan with these steps. - Removed these steps. - Verified the build plan showed that the steps were invalid, and that I could delete them. - Deleted them. - Added new steps, no obsolete steps were available for selection. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10458 Differential Revision: https://secure.phabricator.com/D15352 --- src/__phutil_library_map__.php | 4 - .../HarbormasterPlanViewController.php | 22 +-- .../HarbormasterStepViewController.php | 21 ++- .../HarbormasterBuildStepCoreCustomField.php | 7 +- .../engine/HarbormasterBuildGraph.php | 7 +- ...ormasterCommandBuildStepImplementation.php | 149 ------------------ ...masterLeaseHostBuildStepImplementation.php | 74 --------- 7 files changed, 42 insertions(+), 242 deletions(-) delete mode 100644 src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php delete mode 100644 src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a4cea24c74..8528aa9a19 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1094,7 +1094,6 @@ phutil_register_library_map(array( 'HarbormasterBuildableTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildableTransactionQuery.php', 'HarbormasterBuildableViewController' => 'applications/harbormaster/controller/HarbormasterBuildableViewController.php', 'HarbormasterBuiltinBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterBuiltinBuildStepGroup.php', - 'HarbormasterCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php', 'HarbormasterConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterConduitAPIMethod.php', 'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php', 'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php', @@ -1108,7 +1107,6 @@ phutil_register_library_map(array( 'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php', 'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php', 'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php', - 'HarbormasterLeaseHostBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php', 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php', 'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php', 'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php', @@ -5259,7 +5257,6 @@ phutil_register_library_map(array( 'HarbormasterBuildableTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'HarbormasterBuildableViewController' => 'HarbormasterController', 'HarbormasterBuiltinBuildStepGroup' => 'HarbormasterBuildStepGroup', - 'HarbormasterCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterConduitAPIMethod' => 'ConduitAPIMethod', 'HarbormasterController' => 'PhabricatorController', 'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod', @@ -5273,7 +5270,6 @@ phutil_register_library_map(array( 'HarbormasterFileArtifact' => 'HarbormasterArtifact', 'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterHostArtifact' => 'HarbormasterDrydockLeaseArtifact', - 'HarbormasterLeaseHostBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLintMessagesController' => 'HarbormasterController', 'HarbormasterLintPropertyView' => 'AphrontView', diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index ed1381006b..f4212c38b3 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -105,31 +105,31 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { $i++; } + $step_id = $step->getID(); + $view_uri = $this->getApplicationURI("step/view/{$step_id}/"); + + $item = id(new PHUIObjectItemView()) + ->setObjectName(pht('Step %d.%d', $depth, $i)) + ->setHeader($step->getName()) + ->setHref($view_uri); + + $step_list->addItem($item); + $implementation = null; try { $implementation = $step->getStepImplementation(); } catch (Exception $ex) { // We can't initialize the implementation. This might be because // it's been renamed or no longer exists. - $item = id(new PHUIObjectItemView()) - ->setObjectName(pht('Step %d.%d', $depth, $i)) - ->setHeader(pht('Unknown Implementation')) + $item ->setStatusIcon('fa-warning red') ->addAttribute(pht( 'This step has an invalid implementation (%s).', $step->getClassName())); - $step_list->addItem($item); continue; } - $item = id(new PHUIObjectItemView()) - ->setObjectName(pht('Step %d.%d', $depth, $i)) - ->setHeader($step->getName()); $item->addAttribute($implementation->getDescription()); - - $step_id = $step->getID(); - - $view_uri = $this->getApplicationURI("step/view/{$step_id}/"); $item->setHref($view_uri); $depends = $step->getStepImplementation()->getDependencies($step); diff --git a/src/applications/harbormaster/controller/HarbormasterStepViewController.php b/src/applications/harbormaster/controller/HarbormasterStepViewController.php index d8d1d3f0b0..8f8abf6ea9 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepViewController.php @@ -18,8 +18,6 @@ final class HarbormasterStepViewController extends HarbormasterController { $plan_id = $plan->getID(); $plan_uri = $this->getApplicationURI("plan/{$plan_id}/"); - $implementation = $step->getStepImplementation(); - $field_list = PhabricatorCustomField::getObjectFields( $step, PhabricatorCustomField::ROLE_VIEW); @@ -65,6 +63,25 @@ final class HarbormasterStepViewController extends HarbormasterController { ->setUser($viewer) ->setObject($step); + try { + $implementation = $step->getStepImplementation(); + } catch (Exception $ex) { + $implementation = null; + } + + if ($implementation) { + $type = $implementation->getName(); + } else { + $type = phutil_tag( + 'em', + array(), + pht( + 'Invalid Implementation ("%s")!', + $step->getClassName())); + } + + $view->addProperty(pht('Step Type'), $type); + $view->addProperty( pht('Created'), phabricator_datetime($step->getDateCreated(), $viewer)); diff --git a/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php b/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php index 296669d2b5..dc0c759da6 100644 --- a/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php +++ b/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php @@ -9,7 +9,12 @@ final class HarbormasterBuildStepCoreCustomField } public function createFields($object) { - $impl = $object->getStepImplementation(); + try { + $impl = $object->getStepImplementation(); + } catch (Exception $ex) { + return array(); + } + $specs = $impl->getFieldSpecifications(); if ($impl->supportsWaitForMessage()) { diff --git a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php index 757f112d05..dc9e4b7691 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildGraph.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildGraph.php @@ -48,7 +48,12 @@ final class HarbormasterBuildGraph extends AbstractDirectedGraph { $map = array(); foreach ($nodes as $node) { $step = $this->stepMap[$node]; - $deps = $step->getStepImplementation()->getDependencies($step); + + try { + $deps = $step->getStepImplementation()->getDependencies($step); + } catch (Exception $ex) { + $deps = array(); + } $map[$node] = array(); foreach ($deps as $dep) { diff --git a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php deleted file mode 100644 index a99c56d0a3..0000000000 --- a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php +++ /dev/null @@ -1,149 +0,0 @@ -formatSettingForDescription('command'), - $this->formatSettingForDescription('hostartifact')); - } - - public function escapeCommand($pattern, array $args) { - array_unshift($args, $pattern); - - $mode = PhutilCommandString::MODE_DEFAULT; - if ($this->platform == 'windows') { - $mode = PhutilCommandString::MODE_POWERSHELL; - } - - return id(new PhutilCommandString($args)) - ->setEscapingMode($mode); - } - - public function execute( - HarbormasterBuild $build, - HarbormasterBuildTarget $build_target) { - $viewer = PhabricatorUser::getOmnipotentUser(); - - $settings = $this->getSettings(); - $variables = $build_target->getVariables(); - - $artifact = $build_target->loadArtifact($settings['hostartifact']); - $impl = $artifact->getArtifactImplementation(); - $lease = $impl->loadArtifactLease($viewer); - - $this->platform = $lease->getAttribute('platform'); - - $command = $this->mergeVariables( - array($this, 'escapeCommand'), - $settings['command'], - $variables); - - $this->platform = null; - - $interface = $lease->getInterface('command'); - - $future = $interface->getExecFuture('%C', $command); - - $log_stdout = $build->createLog($build_target, 'remote', 'stdout'); - $log_stderr = $build->createLog($build_target, 'remote', 'stderr'); - - $start_stdout = $log_stdout->start(); - $start_stderr = $log_stderr->start(); - - $build_update = 5; - - // Read the next amount of available output every second. - $futures = new FutureIterator(array($future)); - foreach ($futures->setUpdateInterval(1) as $key => $future_iter) { - if ($future_iter === null) { - - // Check to see if we should abort. - if ($build_update <= 0) { - $build->reload(); - if ($this->shouldAbort($build, $build_target)) { - $future->resolveKill(); - throw new HarbormasterBuildAbortedException(); - } else { - $build_update = 5; - } - } else { - $build_update -= 1; - } - - // Command is still executing. - - // Read more data as it is available. - list($stdout, $stderr) = $future->read(); - $log_stdout->append($stdout); - $log_stderr->append($stderr); - $future->discardBuffers(); - } else { - // Command execution is complete. - - // Get the return value so we can log that as well. - list($err) = $future->resolve(); - - // Retrieve the last few bits of information. - list($stdout, $stderr) = $future->read(); - $log_stdout->append($stdout); - $log_stderr->append($stderr); - $future->discardBuffers(); - - break; - } - } - - $log_stdout->finalize($start_stdout); - $log_stderr->finalize($start_stderr); - - if ($err) { - throw new HarbormasterBuildFailureException(); - } - } - - public function getArtifactInputs() { - return array( - array( - 'name' => pht('Run on Host'), - 'key' => $this->getSetting('hostartifact'), - 'type' => HarbormasterHostArtifact::ARTIFACTCONST, - ), - ); - } - - public function getFieldSpecifications() { - return array( - 'command' => array( - 'name' => pht('Command'), - 'type' => 'text', - 'required' => true, - 'caption' => pht( - "Under Windows, this is executed under PowerShell. ". - "Under UNIX, this is executed using the user's shell."), - ), - 'hostartifact' => array( - 'name' => pht('Host'), - 'type' => 'text', - 'required' => true, - ), - ); - } - -} diff --git a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php deleted file mode 100644 index b14d0975f7..0000000000 --- a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php +++ /dev/null @@ -1,74 +0,0 @@ -getSettings(); - - // Create the lease. - $lease = id(new DrydockLease()) - ->setResourceType('host') - ->setOwnerPHID($build_target->getPHID()) - ->setAttributes( - array( - 'platform' => $settings['platform'], - )) - ->queueForActivation(); - - // Wait until the lease is fulfilled. - // TODO: This will throw an exception if the lease can't be fulfilled; - // we should treat that as build failure not build error. - $lease->waitUntilActive(); - - // Create the associated artifact. - $artifact = $build_target->createArtifact( - PhabricatorUser::getOmnipotentUser(), - $settings['name'], - HarbormasterHostArtifact::ARTIFACTCONST, - array( - 'drydockLeasePHID' => $lease->getPHID(), - )); - } - - public function getArtifactOutputs() { - return array( - array( - 'name' => pht('Leased Host'), - 'key' => $this->getSetting('name'), - 'type' => HarbormasterHostArtifact::ARTIFACTCONST, - ), - ); - } - - public function getFieldSpecifications() { - return array( - 'name' => array( - 'name' => pht('Artifact Name'), - 'type' => 'text', - 'required' => true, - ), - 'platform' => array( - 'name' => pht('Platform'), - 'type' => 'text', - 'required' => true, - ), - ); - } - -} From 93b8f803a02b25f898310f5d0057e78c7c34906f Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 26 Feb 2016 11:24:03 -0800 Subject: [PATCH 31/31] Require "Can Edit" on a build plan to abort or pause associated builds Summary: Fixes T9614. This is kind of silly, but stop users from fighting turf wars over build resources or showing up on an install and just aborting a bunch of builds for the heck of it. Test Plan: - Restarted / paused / aborted / etc builds. - Tried to do the same for builds I didn't have edit permission on the build plan for, got errors. Reviewers: chad Reviewed By: chad Maniphest Tasks: T9614 Differential Revision: https://secure.phabricator.com/D15353 --- .../HarbormasterBuildActionController.php | 2 + .../HarbormasterBuildViewController.php | 27 ++++- .../HarbormasterBuildableActionController.php | 106 ++++++++++++++---- .../HarbormasterBuildableViewController.php | 21 +++- .../HarbormasterBuildTransactionEditor.php | 5 + .../storage/build/HarbormasterBuild.php | 36 ++++++ 6 files changed, 166 insertions(+), 31 deletions(-) diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php index d729d868b0..ddab677faf 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -39,6 +39,8 @@ final class HarbormasterBuildActionController return new Aphront400Response(); } + $build->assertCanIssueCommand($viewer, $action); + switch ($via) { case 'buildable': $return_uri = '/'.$build->getBuildable()->getMonogram(); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index a9cbb43ccf..25f3506c0b 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -441,10 +441,29 @@ final class HarbormasterBuildViewController ->setUser($viewer) ->setObject($build); - $can_restart = $build->canRestartBuild(); - $can_pause = $build->canPauseBuild(); - $can_resume = $build->canResumeBuild(); - $can_abort = $build->canAbortBuild(); + $can_restart = + $build->canRestartBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_RESTART); + + $can_pause = + $build->canPauseBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_PAUSE); + + $can_resume = + $build->canResumeBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_RESUME); + + $can_abort = + $build->canAbortBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_ABORT); $list->addAction( id(new PhabricatorActionView()) diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php index 9fa7d6f006..b3f60cd1dc 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php @@ -51,6 +51,14 @@ final class HarbormasterBuildableActionController } } + $restricted = false; + foreach ($issuable as $key => $build) { + if (!$build->canIssueCommand($viewer, $action)) { + $restricted = true; + unset($issuable[$key]); + } + } + $return_uri = '/'.$buildable->getMonogram(); if ($request->isDialogFormPost() && $issuable) { $editor = id(new HarbormasterBuildableTransactionEditor()) @@ -84,49 +92,101 @@ final class HarbormasterBuildableActionController switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($issuable) { - $title = pht('Really restart all builds?'); - $body = pht( - 'Progress on all builds will be discarded, and all builds will '. - 'restart. Side effects of the builds will occur again. Really '. - 'restart all builds?'); - $submit = pht('Restart All Builds'); + $title = pht('Really restart builds?'); + + if ($restricted) { + $body = pht( + 'You only have permission to restart some builds. Progress '. + 'on builds you have permission to restart will be discarded '. + 'and they will restart. Side effects of these builds will '. + 'occur again. Really restart all builds?'); + } else { + $body = pht( + 'Progress on all builds will be discarded, and all builds will '. + 'restart. Side effects of the builds will occur again. Really '. + 'restart all builds?'); + } + + $submit = pht('Restart Builds'); } else { $title = pht('Unable to Restart Builds'); - $body = pht('No builds can be restarted.'); + + if ($restricted) { + $body = pht('You do not have permission to restart any builds.'); + } else { + $body = pht('No builds can be restarted.'); + } } break; case HarbormasterBuildCommand::COMMAND_PAUSE: if ($issuable) { - $title = pht('Really pause all builds?'); - $body = pht( - 'If you pause all builds, work will halt once the current steps '. - 'complete. You can resume the builds later.'); - $submit = pht('Pause All Builds'); + $title = pht('Really pause builds?'); + + if ($restricted) { + $body = pht( + 'You only have permission to pause some builds. Once the '. + 'current steps complete, work will halt on builds you have '. + 'permission to pause. You can resume the builds later.'); + } else { + $body = pht( + 'If you pause all builds, work will halt once the current steps '. + 'complete. You can resume the builds later.'); + } + $submit = pht('Pause Builds'); } else { $title = pht('Unable to Pause Builds'); - $body = pht('No builds can be paused.'); + + if ($restricted) { + $body = pht('You do not have permission to pause any builds.'); + } else { + $body = pht('No builds can be paused.'); + } } break; case HarbormasterBuildCommand::COMMAND_ABORT: if ($issuable) { - $title = pht('Really abort all builds?'); - $body = pht( - 'If you abort all builds, work will halt immediately. Work '. - 'will be discarded, and builds must be completely restarted.'); - $submit = pht('Abort All Builds'); + $title = pht('Really abort builds?'); + if ($restricted) { + $body = pht( + 'You only have permission to abort some builds. Work will '. + 'halt immediately on builds you have permission to abort. '. + 'Progress will be discarded, and builds must be completely '. + 'restarted if you want them to complete.'); + } else { + $body = pht( + 'If you abort all builds, work will halt immediately. Work '. + 'will be discarded, and builds must be completely restarted.'); + } + $submit = pht('Abort Builds'); } else { $title = pht('Unable to Abort Builds'); - $body = pht('No builds can be aborted.'); + + if ($restricted) { + $body = pht('You do not have permission to abort any builds.'); + } else { + $body = pht('No builds can be aborted.'); + } } break; case HarbormasterBuildCommand::COMMAND_RESUME: if ($issuable) { - $title = pht('Really resume all builds?'); - $body = pht('Work will continue on all builds. Really resume?'); - $submit = pht('Resume All Builds'); + $title = pht('Really resume builds?'); + if ($restricted) { + $body = pht( + 'You only have permission to resume some builds. Work will '. + 'continue on builds you have permission to resume.'); + } else { + $body = pht('Work will continue on all builds. Really resume?'); + } + + $submit = pht('Resume Builds'); } else { $title = pht('Unable to Resume Builds'); - $body = pht('No builds can be resumed.'); + if ($restricted) { + $body = pht('You do not have permission to resume any builds.'); + } else { + $body = pht('No builds can be resumed.'); + } } break; } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index f9a0807ab1..91fb45176f 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -86,18 +86,31 @@ final class HarbormasterBuildableViewController $can_pause = false; $can_abort = false; + $command_restart = HarbormasterBuildCommand::COMMAND_RESTART; + $command_resume = HarbormasterBuildCommand::COMMAND_RESUME; + $command_pause = HarbormasterBuildCommand::COMMAND_PAUSE; + $command_abort = HarbormasterBuildCommand::COMMAND_ABORT; + foreach ($buildable->getBuilds() as $build) { if ($build->canRestartBuild()) { - $can_restart = true; + if ($build->canIssueCommand($viewer, $command_restart)) { + $can_restart = true; + } } if ($build->canResumeBuild()) { - $can_resume = true; + if ($build->canIssueCommand($viewer, $command_resume)) { + $can_resume = true; + } } if ($build->canPauseBuild()) { - $can_pause = true; + if ($build->canIssueCommand($viewer, $command_pause)) { + $can_pause = true; + } } if ($build->canAbortBuild()) { - $can_abort = true; + if ($build->canIssueCommand($viewer, $command_abort)) { + $can_abort = true; + } } } diff --git a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php index 0727a473ec..2615f3c5be 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php @@ -88,6 +88,11 @@ final class HarbormasterBuildTransactionEditor return; } + $actor = $this->getActor(); + if (!$build->canIssueCommand($actor, $command)) { + return; + } + id(new HarbormasterBuildCommand()) ->setAuthorPHID($xaction->getAuthorPHID()) ->setTargetPHID($build->getPHID()) diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index c96d0c2710..1bac8d3ecd 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -450,6 +450,42 @@ final class HarbormasterBuild extends HarbormasterDAO return $this; } + public function canIssueCommand(PhabricatorUser $viewer, $command) { + try { + $this->assertCanIssueCommand($viewer, $command); + return true; + } catch (Exception $ex) { + return false; + } + } + + public function assertCanIssueCommand(PhabricatorUser $viewer, $command) { + $need_edit = false; + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + break; + case HarbormasterBuildCommand::COMMAND_PAUSE: + case HarbormasterBuildCommand::COMMAND_RESUME: + case HarbormasterBuildCommand::COMMAND_ABORT: + $need_edit = true; + break; + default: + throw new Exception( + pht( + 'Invalid Harbormaster build command "%s".', + $command)); + } + + // Issuing these commands requires that you be able to edit the build, to + // prevent enemy engineers from sabotaging your builds. See T9614. + if ($need_edit) { + PhabricatorPolicyFilter::requireCapability( + $viewer, + $this->getBuildPlan(), + PhabricatorPolicyCapability::CAN_EDIT); + } + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */