@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator

Provide "almanac.binding.search" and "almanac.binding.edit"

Summary:
Depends on D19338. Ref T13120. Ref T12414. These are the last of the new API methods.

This stuff still doesn't work:

- You can't actually enable/disable bindings yet. I want to take a look at the use cases and consider changing "disabled" to "status", or providing a different way to solve the problem.
- You can't edit properties via the API. I expect to enable this for all `AlmanacPropertyInterface` objects with an extension in a future change.

Test Plan:
- Searched for bindings via API.
- Viewed binding web UI for API methods.
- Created bindings via API.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13120, T12414

Differential Revision: https://secure.phabricator.com/D19340

+385 -1
+11
src/__phutil_library_map__.php
··· 14 14 'AlmanacBindingDeletePropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingDeletePropertyTransaction.php', 15 15 'AlmanacBindingDisableController' => 'applications/almanac/controller/AlmanacBindingDisableController.php', 16 16 'AlmanacBindingDisableTransaction' => 'applications/almanac/xaction/AlmanacBindingDisableTransaction.php', 17 + 'AlmanacBindingEditConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingEditConduitAPIMethod.php', 17 18 'AlmanacBindingEditController' => 'applications/almanac/controller/AlmanacBindingEditController.php', 19 + 'AlmanacBindingEditEngine' => 'applications/almanac/editor/AlmanacBindingEditEngine.php', 18 20 'AlmanacBindingEditor' => 'applications/almanac/editor/AlmanacBindingEditor.php', 19 21 'AlmanacBindingInterfaceTransaction' => 'applications/almanac/xaction/AlmanacBindingInterfaceTransaction.php', 20 22 'AlmanacBindingPHIDType' => 'applications/almanac/phid/AlmanacBindingPHIDType.php', 21 23 'AlmanacBindingPropertyEditEngine' => 'applications/almanac/editor/AlmanacBindingPropertyEditEngine.php', 22 24 'AlmanacBindingQuery' => 'applications/almanac/query/AlmanacBindingQuery.php', 25 + 'AlmanacBindingSearchConduitAPIMethod' => 'applications/almanac/conduit/AlmanacBindingSearchConduitAPIMethod.php', 26 + 'AlmanacBindingSearchEngine' => 'applications/almanac/query/AlmanacBindingSearchEngine.php', 27 + 'AlmanacBindingServiceTransaction' => 'applications/almanac/xaction/AlmanacBindingServiceTransaction.php', 23 28 'AlmanacBindingSetPropertyTransaction' => 'applications/almanac/xaction/AlmanacBindingSetPropertyTransaction.php', 24 29 'AlmanacBindingTableView' => 'applications/almanac/view/AlmanacBindingTableView.php', 25 30 'AlmanacBindingTransaction' => 'applications/almanac/storage/AlmanacBindingTransaction.php', ··· 5206 5211 'AlmanacPropertyInterface', 5207 5212 'PhabricatorDestructibleInterface', 5208 5213 'PhabricatorExtendedPolicyInterface', 5214 + 'PhabricatorConduitResultInterface', 5209 5215 ), 5210 5216 'AlmanacBindingDeletePropertyTransaction' => 'AlmanacBindingTransactionType', 5211 5217 'AlmanacBindingDisableController' => 'AlmanacServiceController', 5212 5218 'AlmanacBindingDisableTransaction' => 'AlmanacBindingTransactionType', 5219 + 'AlmanacBindingEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 5213 5220 'AlmanacBindingEditController' => 'AlmanacServiceController', 5221 + 'AlmanacBindingEditEngine' => 'PhabricatorEditEngine', 5214 5222 'AlmanacBindingEditor' => 'AlmanacEditor', 5215 5223 'AlmanacBindingInterfaceTransaction' => 'AlmanacBindingTransactionType', 5216 5224 'AlmanacBindingPHIDType' => 'PhabricatorPHIDType', 5217 5225 'AlmanacBindingPropertyEditEngine' => 'AlmanacPropertyEditEngine', 5218 5226 'AlmanacBindingQuery' => 'AlmanacQuery', 5227 + 'AlmanacBindingSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 5228 + 'AlmanacBindingSearchEngine' => 'PhabricatorApplicationSearchEngine', 5229 + 'AlmanacBindingServiceTransaction' => 'AlmanacBindingTransactionType', 5219 5230 'AlmanacBindingSetPropertyTransaction' => 'AlmanacBindingTransactionType', 5220 5231 'AlmanacBindingTableView' => 'AphrontView', 5221 5232 'AlmanacBindingTransaction' => 'AlmanacModularTransaction',
+19
src/applications/almanac/conduit/AlmanacBindingEditConduitAPIMethod.php
··· 1 + <?php 2 + 3 + final class AlmanacBindingEditConduitAPIMethod 4 + extends PhabricatorEditEngineAPIMethod { 5 + 6 + public function getAPIMethodName() { 7 + return 'almanac.binding.edit'; 8 + } 9 + 10 + public function newEditEngine() { 11 + return new AlmanacBindingEditEngine(); 12 + } 13 + 14 + public function getMethodSummary() { 15 + return pht( 16 + 'Apply transactions to create a new binding or edit an existing one.'); 17 + } 18 + 19 + }
+18
src/applications/almanac/conduit/AlmanacBindingSearchConduitAPIMethod.php
··· 1 + <?php 2 + 3 + final class AlmanacBindingSearchConduitAPIMethod 4 + extends PhabricatorSearchEngineAPIMethod { 5 + 6 + public function getAPIMethodName() { 7 + return 'almanac.binding.search'; 8 + } 9 + 10 + public function newSearchEngine() { 11 + return new AlmanacBindingSearchEngine(); 12 + } 13 + 14 + public function getMethodSummary() { 15 + return pht('Read information about Almanac bindings.'); 16 + } 17 + 18 + }
+158
src/applications/almanac/editor/AlmanacBindingEditEngine.php
··· 1 + <?php 2 + 3 + final class AlmanacBindingEditEngine 4 + extends PhabricatorEditEngine { 5 + 6 + const ENGINECONST = 'almanac.binding'; 7 + 8 + private $service; 9 + 10 + public function setService(AlmanacService $service) { 11 + $this->service = $service; 12 + return $this; 13 + } 14 + 15 + public function getService() { 16 + if (!$this->service) { 17 + throw new PhutilInvalidStateException('setService'); 18 + } 19 + return $this->service; 20 + } 21 + 22 + public function isEngineConfigurable() { 23 + return false; 24 + } 25 + 26 + public function getEngineName() { 27 + return pht('Almanac Bindings'); 28 + } 29 + 30 + public function getSummaryHeader() { 31 + return pht('Edit Almanac Binding Configurations'); 32 + } 33 + 34 + public function getSummaryText() { 35 + return pht('This engine is used to edit Almanac bindings.'); 36 + } 37 + 38 + public function getEngineApplicationClass() { 39 + return 'PhabricatorAlmanacApplication'; 40 + } 41 + 42 + protected function newEditableObject() { 43 + $service = $this->getService(); 44 + return AlmanacBinding::initializeNewBinding($service); 45 + } 46 + 47 + protected function newEditableObjectForDocumentation() { 48 + $service_type = AlmanacCustomServiceType::SERVICETYPE; 49 + $service = AlmanacService::initializeNewService($service_type); 50 + $this->setService($service); 51 + return $this->newEditableObject(); 52 + } 53 + 54 + protected function newEditableObjectFromConduit(array $raw_xactions) { 55 + $service_phid = null; 56 + foreach ($raw_xactions as $raw_xaction) { 57 + if ($raw_xaction['type'] !== 'service') { 58 + continue; 59 + } 60 + 61 + $service_phid = $raw_xaction['value']; 62 + } 63 + 64 + if ($service_phid === null) { 65 + throw new Exception( 66 + pht( 67 + 'When creating a new Almanac binding via the Conduit API, you '. 68 + 'must provide a "service" transaction to select a service to bind.')); 69 + } 70 + 71 + $service = id(new AlmanacServiceQuery()) 72 + ->setViewer($this->getViewer()) 73 + ->withPHIDs(array($service_phid)) 74 + ->requireCapabilities( 75 + array( 76 + PhabricatorPolicyCapability::CAN_VIEW, 77 + PhabricatorPolicyCapability::CAN_EDIT, 78 + )) 79 + ->executeOne(); 80 + if (!$service) { 81 + throw new Exception( 82 + pht( 83 + 'Service "%s" is unrecognized, restricted, or you do not have '. 84 + 'permission to edit it.', 85 + $service_phid)); 86 + } 87 + 88 + $this->setService($service); 89 + 90 + return $this->newEditableObject(); 91 + } 92 + 93 + protected function newObjectQuery() { 94 + return new AlmanacBindingQuery(); 95 + } 96 + 97 + protected function getObjectCreateTitleText($object) { 98 + return pht('Create Binding'); 99 + } 100 + 101 + protected function getObjectCreateButtonText($object) { 102 + return pht('Create Binding'); 103 + } 104 + 105 + protected function getObjectEditTitleText($object) { 106 + return pht('Edit Binding'); 107 + } 108 + 109 + protected function getObjectEditShortText($object) { 110 + return pht('Edit Binding'); 111 + } 112 + 113 + protected function getObjectCreateShortText() { 114 + return pht('Create Binding'); 115 + } 116 + 117 + protected function getObjectName() { 118 + return pht('Binding'); 119 + } 120 + 121 + protected function getEditorURI() { 122 + return '/almanac/binding/edit/'; 123 + } 124 + 125 + protected function getObjectCreateCancelURI($object) { 126 + return '/almanac/binding/'; 127 + } 128 + 129 + protected function getObjectViewURI($object) { 130 + return $object->getURI(); 131 + } 132 + 133 + protected function buildCustomEditFields($object) { 134 + return array( 135 + id(new PhabricatorTextEditField()) 136 + ->setKey('service') 137 + ->setLabel(pht('Service')) 138 + ->setIsConduitOnly(true) 139 + ->setTransactionType( 140 + AlmanacBindingServiceTransaction::TRANSACTIONTYPE) 141 + ->setDescription(pht('Service to create a binding for.')) 142 + ->setConduitDescription(pht('Select the service to bind.')) 143 + ->setConduitTypeDescription(pht('Service PHID.')) 144 + ->setValue($object->getServicePHID()), 145 + id(new PhabricatorTextEditField()) 146 + ->setKey('interface') 147 + ->setLabel(pht('Interface')) 148 + ->setIsConduitOnly(true) 149 + ->setTransactionType( 150 + AlmanacBindingInterfaceTransaction::TRANSACTIONTYPE) 151 + ->setDescription(pht('Interface to bind the service to.')) 152 + ->setConduitDescription(pht('Set the interface to bind.')) 153 + ->setConduitTypeDescription(pht('Interface PHID.')) 154 + ->setValue($object->getInterfacePHID()), 155 + ); 156 + } 157 + 158 + }
+80
src/applications/almanac/query/AlmanacBindingSearchEngine.php
··· 1 + <?php 2 + 3 + final class AlmanacBindingSearchEngine 4 + extends PhabricatorApplicationSearchEngine { 5 + 6 + public function getResultTypeDescription() { 7 + return pht('Almanac Bindings'); 8 + } 9 + 10 + public function getApplicationClassName() { 11 + return 'PhabricatorAlmanacApplication'; 12 + } 13 + 14 + public function newQuery() { 15 + return new AlmanacBindingQuery(); 16 + } 17 + 18 + protected function buildCustomSearchFields() { 19 + return array( 20 + id(new PhabricatorPHIDsSearchField()) 21 + ->setLabel(pht('Services')) 22 + ->setKey('servicePHIDs') 23 + ->setAliases(array('service', 'servicePHID', 'services')) 24 + ->setDescription(pht('Search for bindings on particular services.')), 25 + id(new PhabricatorPHIDsSearchField()) 26 + ->setLabel(pht('Devices')) 27 + ->setKey('devicePHIDs') 28 + ->setAliases(array('device', 'devicePHID', 'devices')) 29 + ->setDescription(pht('Search for bindings on particular devices.')), 30 + ); 31 + } 32 + 33 + protected function buildQueryFromParameters(array $map) { 34 + $query = $this->newQuery(); 35 + 36 + if ($map['servicePHIDs']) { 37 + $query->withServicePHIDs($map['servicePHIDs']); 38 + } 39 + 40 + if ($map['devicePHIDs']) { 41 + $query->withDevicePHIDs($map['devicePHIDs']); 42 + } 43 + 44 + return $query; 45 + } 46 + 47 + protected function getURI($path) { 48 + return '/almanac/binding/'.$path; 49 + } 50 + 51 + protected function getBuiltinQueryNames() { 52 + $names = array( 53 + 'all' => pht('All Bindings'), 54 + ); 55 + 56 + return $names; 57 + } 58 + 59 + public function buildSavedQueryFromBuiltin($query_key) { 60 + $query = $this->newSavedQuery(); 61 + $query->setQueryKey($query_key); 62 + 63 + switch ($query_key) { 64 + case 'all': 65 + return $query; 66 + } 67 + 68 + return parent::buildSavedQueryFromBuiltin($query_key); 69 + } 70 + 71 + protected function renderResultList( 72 + array $devices, 73 + PhabricatorSavedQuery $query, 74 + array $handles) { 75 + 76 + // For now, this SearchEngine just supports API access via Conduit. 77 + throw new PhutilMethodNotImplementedException(); 78 + } 79 + 80 + }
+35 -1
src/applications/almanac/storage/AlmanacBinding.php
··· 7 7 PhabricatorApplicationTransactionInterface, 8 8 AlmanacPropertyInterface, 9 9 PhabricatorDestructibleInterface, 10 - PhabricatorExtendedPolicyInterface { 10 + PhabricatorExtendedPolicyInterface, 11 + PhabricatorConduitResultInterface { 11 12 12 13 protected $servicePHID; 13 14 protected $devicePHID; ··· 23 24 public static function initializeNewBinding(AlmanacService $service) { 24 25 return id(new AlmanacBinding()) 25 26 ->setServicePHID($service->getPHID()) 27 + ->attachService($service) 26 28 ->attachAlmanacProperties(array()) 27 29 ->setIsDisabled(0); 28 30 } ··· 224 226 $this->delete(); 225 227 } 226 228 229 + 230 + /* -( PhabricatorConduitResultInterface )---------------------------------- */ 231 + 232 + 233 + public function getFieldSpecificationsForConduit() { 234 + return array( 235 + id(new PhabricatorConduitSearchFieldSpecification()) 236 + ->setKey('servicePHID') 237 + ->setType('phid') 238 + ->setDescription(pht('The bound service.')), 239 + id(new PhabricatorConduitSearchFieldSpecification()) 240 + ->setKey('devicePHID') 241 + ->setType('phid') 242 + ->setDescription(pht('The device the service is bound to.')), 243 + id(new PhabricatorConduitSearchFieldSpecification()) 244 + ->setKey('interfacePHID') 245 + ->setType('phid') 246 + ->setDescription(pht('The interface the service is bound to.')), 247 + ); 248 + } 249 + 250 + public function getFieldValuesForConduit() { 251 + return array( 252 + 'servicePHID' => $this->getServicePHID(), 253 + 'devicePHID' => $this->getDevicePHID(), 254 + 'interfacePHID' => $this->getInterfacePHID(), 255 + ); 256 + } 257 + 258 + public function getConduitSearchAttachments() { 259 + return array(); 260 + } 227 261 228 262 }
+64
src/applications/almanac/xaction/AlmanacBindingServiceTransaction.php
··· 1 + <?php 2 + 3 + final class AlmanacBindingServiceTransaction 4 + extends AlmanacBindingTransactionType { 5 + 6 + const TRANSACTIONTYPE = 'almanac:binding:service'; 7 + 8 + public function generateOldValue($object) { 9 + return $object->getServicePHID(); 10 + } 11 + 12 + public function applyInternalEffects($object, $value) { 13 + $object->setServicePHID($value); 14 + } 15 + 16 + public function validateTransactions($object, array $xactions) { 17 + $errors = array(); 18 + 19 + $service_phid = $object->getServicePHID(); 20 + if ($this->isEmptyTextTransaction($service_phid, $xactions)) { 21 + $errors[] = $this->newRequiredError( 22 + pht('Bindings must have a service.')); 23 + } 24 + 25 + foreach ($xactions as $xaction) { 26 + if (!$this->isNewObject()) { 27 + $errors[] = $this->newInvalidError( 28 + pht( 29 + 'The service for a binding can not be changed once it has '. 30 + 'been created.'), 31 + $xaction); 32 + continue; 33 + } 34 + 35 + $service_phid = $xaction->getNewValue(); 36 + $services = id(new AlmanacServiceQuery()) 37 + ->setViewer($this->getActor()) 38 + ->withPHIDs(array($service_phid)) 39 + ->execute(); 40 + if (!$services) { 41 + $errors[] = $this->newInvalidError( 42 + pht('You can not bind a nonexistent or restricted service.'), 43 + $xaction); 44 + continue; 45 + } 46 + 47 + $service = head($services); 48 + $can_edit = PhabricatorPolicyFilter::hasCapability( 49 + $this->getActor(), 50 + $service, 51 + PhabricatorPolicyCapability::CAN_EDIT); 52 + if (!$can_edit) { 53 + $errors[] = $this->newInvalidError( 54 + pht( 55 + 'You can not bind a service which you do not have permission '. 56 + 'to edit.')); 57 + continue; 58 + } 59 + } 60 + 61 + return $errors; 62 + } 63 + 64 + }