@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refine core webhook implementation somewhat

Summary:
Depends on D19045. Ref T11330.

- View/regenerate HMAC keys.
- Pretty JSON.
- Readable status transactions.
- test, silent, secure flags.
- Dates on request view.
- More icons.
- Can test any object.
- GC for requests.

Test Plan: Went through each feature poking at it in the web UI and with `bin/webhook call ...` / `bin/garbage collect ...`.

Subscribers: ftdysa

Maniphest Tasks: T11330

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

+354 -30
+5
src/__phutil_library_map__.php
··· 1447 1447 'HeraldWebhookEditController' => 'applications/herald/controller/HeraldWebhookEditController.php', 1448 1448 'HeraldWebhookEditEngine' => 'applications/herald/editor/HeraldWebhookEditEngine.php', 1449 1449 'HeraldWebhookEditor' => 'applications/herald/editor/HeraldWebhookEditor.php', 1450 + 'HeraldWebhookKeyController' => 'applications/herald/controller/HeraldWebhookKeyController.php', 1450 1451 'HeraldWebhookListController' => 'applications/herald/controller/HeraldWebhookListController.php', 1451 1452 'HeraldWebhookManagementWorkflow' => 'applications/herald/management/HeraldWebhookManagementWorkflow.php', 1452 1453 'HeraldWebhookNameTransaction' => 'applications/herald/xaction/HeraldWebhookNameTransaction.php', 1453 1454 'HeraldWebhookPHIDType' => 'applications/herald/phid/HeraldWebhookPHIDType.php', 1454 1455 'HeraldWebhookQuery' => 'applications/herald/query/HeraldWebhookQuery.php', 1455 1456 'HeraldWebhookRequest' => 'applications/herald/storage/HeraldWebhookRequest.php', 1457 + 'HeraldWebhookRequestGarbageCollector' => 'applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php', 1456 1458 'HeraldWebhookRequestListView' => 'applications/herald/view/HeraldWebhookRequestListView.php', 1457 1459 'HeraldWebhookRequestPHIDType' => 'applications/herald/phid/HeraldWebhookRequestPHIDType.php', 1458 1460 'HeraldWebhookRequestQuery' => 'applications/herald/query/HeraldWebhookRequestQuery.php', ··· 6735 6737 'PhabricatorPolicyInterface', 6736 6738 'PhabricatorApplicationTransactionInterface', 6737 6739 'PhabricatorDestructibleInterface', 6740 + 'PhabricatorProjectInterface', 6738 6741 ), 6739 6742 'HeraldWebhookCallManagementWorkflow' => 'HeraldWebhookManagementWorkflow', 6740 6743 'HeraldWebhookController' => 'HeraldController', 6741 6744 'HeraldWebhookEditController' => 'HeraldWebhookController', 6742 6745 'HeraldWebhookEditEngine' => 'PhabricatorEditEngine', 6743 6746 'HeraldWebhookEditor' => 'PhabricatorApplicationTransactionEditor', 6747 + 'HeraldWebhookKeyController' => 'HeraldWebhookController', 6744 6748 'HeraldWebhookListController' => 'HeraldWebhookController', 6745 6749 'HeraldWebhookManagementWorkflow' => 'PhabricatorManagementWorkflow', 6746 6750 'HeraldWebhookNameTransaction' => 'HeraldWebhookTransactionType', ··· 6751 6755 'PhabricatorPolicyInterface', 6752 6756 'PhabricatorExtendedPolicyInterface', 6753 6757 ), 6758 + 'HeraldWebhookRequestGarbageCollector' => 'PhabricatorGarbageCollector', 6754 6759 'HeraldWebhookRequestListView' => 'AphrontView', 6755 6760 'HeraldWebhookRequestPHIDType' => 'PhabricatorPHIDType', 6756 6761 'HeraldWebhookRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+2 -4
src/applications/herald/application/PhabricatorHeraldApplication.php
··· 68 68 'HeraldWebhookViewController', 69 69 $this->getEditRoutePattern('edit/') => 'HeraldWebhookEditController', 70 70 'test/(?P<id>\d+)/' => 'HeraldWebhookTestController', 71 - 'key/' => array( 72 - 'view/(?P<id>\d+)/' => 'HeraldWebhookViewKeyController', 73 - 'cycle/(?P<id>\d+)/' => 'HeraldWebhookCycleKeyController', 74 - ), 71 + 'key/(?P<action>view|cycle)/(?P<id>\d+)/' => 72 + 'HeraldWebhookKeyController', 75 73 ), 76 74 ), 77 75 );
+56
src/applications/herald/controller/HeraldWebhookKeyController.php
··· 1 + <?php 2 + 3 + final class HeraldWebhookKeyController 4 + extends HeraldWebhookController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $this->getViewer(); 8 + 9 + $hook = id(new HeraldWebhookQuery()) 10 + ->setViewer($viewer) 11 + ->withIDs(array($request->getURIData('id'))) 12 + ->requireCapabilities( 13 + array( 14 + PhabricatorPolicyCapability::CAN_VIEW, 15 + PhabricatorPolicyCapability::CAN_EDIT, 16 + )) 17 + ->executeOne(); 18 + if (!$hook) { 19 + return new Aphront404Response(); 20 + } 21 + 22 + $action = $request->getURIData('action'); 23 + if ($action === 'cycle') { 24 + if (!$request->isFormPost()) { 25 + return $this->newDialog() 26 + ->setTitle(pht('Regenerate HMAC Key')) 27 + ->appendParagraph( 28 + pht( 29 + 'Regenerate the HMAC key used to sign requests made by this '. 30 + 'webhook?')) 31 + ->appendParagraph( 32 + pht( 33 + 'Requests which are currently authenticated with the old key '. 34 + 'may fail.')) 35 + ->addCancelButton($hook->getURI()) 36 + ->addSubmitButton(pht('Regnerate Key')); 37 + } else { 38 + $hook->regenerateHMACKey()->save(); 39 + } 40 + } 41 + 42 + $form = id(new AphrontFormView()) 43 + ->setViewer($viewer) 44 + ->appendControl( 45 + id(new AphrontFormTextControl()) 46 + ->setLabel(pht('HMAC Key')) 47 + ->setValue($hook->getHMACKey())); 48 + 49 + return $this->newDialog() 50 + ->setTitle(pht('Webhook HMAC Key')) 51 + ->appendForm($form) 52 + ->addCancelButton($hook->getURI(), pht('Done')); 53 + } 54 + 55 + 56 + }
+58 -10
src/applications/herald/controller/HeraldWebhookTestController.php
··· 19 19 return new Aphront404Response(); 20 20 } 21 21 22 + $v_object = null; 23 + $e_object = null; 24 + $errors = array(); 22 25 if ($request->isFormPost()) { 23 - $object = $hook; 24 26 25 - $request = HeraldWebhookRequest::initializeNewWebhookRequest($hook) 26 - ->setObjectPHID($object->getPHID()) 27 - ->save(); 27 + $v_object = $request->getStr('object'); 28 + if (!strlen($v_object)) { 29 + $object = $hook; 30 + } else { 31 + $objects = id(new PhabricatorObjectQuery()) 32 + ->setViewer($viewer) 33 + ->withNames(array($v_object)) 34 + ->execute(); 35 + if ($objects) { 36 + $object = head($objects); 37 + } else { 38 + $e_object = pht('Invalid'); 39 + $errors[] = pht('Specified object could not be loaded.'); 40 + } 41 + } 28 42 29 - $request->queueCall(); 43 + if (!$errors) { 44 + $xaction_query = 45 + PhabricatorApplicationTransactionQuery::newQueryForObject($object); 30 46 31 - $next_uri = $hook->getURI().'request/'.$request->getID().'/'; 47 + $xactions = $xaction_query 48 + ->withObjectPHIDs(array($object->getPHID())) 49 + ->setViewer($viewer) 50 + ->setLimit(10) 51 + ->execute(); 32 52 33 - return id(new AphrontRedirectResponse())->setURI($next_uri); 53 + $request = HeraldWebhookRequest::initializeNewWebhookRequest($hook) 54 + ->setObjectPHID($object->getPHID()) 55 + ->setIsTestAction(true) 56 + ->setTransactionPHIDs(mpull($xactions, 'getPHID')) 57 + ->save(); 58 + 59 + $request->queueCall(); 60 + 61 + $next_uri = $hook->getURI().'request/'.$request->getID().'/'; 62 + 63 + return id(new AphrontRedirectResponse())->setURI($next_uri); 64 + } 34 65 } 35 66 67 + $instructions = <<<EOREMARKUP 68 + Optionally, choose an object to generate test data for (like `D123` or `T234`). 69 + 70 + The 10 most recent transactions for the object will be submitted to the webhook. 71 + EOREMARKUP; 72 + 73 + $form = id(new AphrontFormView()) 74 + ->setViewer($viewer) 75 + ->appendControl( 76 + id(new AphrontFormTextControl()) 77 + ->setLabel(pht('Object')) 78 + ->setName('object') 79 + ->setError($e_object) 80 + ->setValue($v_object)); 81 + 36 82 return $this->newDialog() 83 + ->setErrors($errors) 84 + ->setWidth(AphrontDialogView::WIDTH_FORM) 37 85 ->setTitle(pht('New Test Request')) 38 - ->appendParagraph( 39 - pht('This will make a new test request to the configured URI.')) 86 + ->appendParagraph(new PHUIRemarkupView($viewer, $instructions)) 87 + ->appendForm($form) 40 88 ->addCancelButton($hook->getURI()) 41 - ->addSubmitButton(pht('Make Request')); 89 + ->addSubmitButton(pht('Test Webhook')); 42 90 } 43 91 44 92
+24
src/applications/herald/controller/HeraldWebhookViewController.php
··· 94 94 95 95 $title = $hook->getName(); 96 96 97 + $status_icon = $hook->getStatusIcon(); 98 + $status_color = $hook->getStatusColor(); 99 + $status_name = $hook->getStatusDisplayName(); 100 + 97 101 $header = id(new PHUIHeaderView()) 98 102 ->setHeader($title) 99 103 ->setViewer($viewer) 100 104 ->setPolicyObject($hook) 105 + ->setStatus($status_icon, $status_color, $status_name) 101 106 ->setHeaderIcon('fa-cloud-upload'); 102 107 103 108 return $header; ··· 117 122 $edit_uri = $this->getApplicationURI("webhook/edit/{$id}/"); 118 123 $test_uri = $this->getApplicationURI("webhook/test/{$id}/"); 119 124 125 + $key_view_uri = $this->getApplicationURI("webhook/key/view/{$id}/"); 126 + $key_cycle_uri = $this->getApplicationURI("webhook/key/cycle/{$id}/"); 127 + 120 128 $curtain->addAction( 121 129 id(new PhabricatorActionView()) 122 130 ->setName(pht('Edit Webhook')) ··· 132 140 ->setDisabled(!$can_edit) 133 141 ->setWorkflow(true) 134 142 ->setHref($test_uri)); 143 + 144 + $curtain->addAction( 145 + id(new PhabricatorActionView()) 146 + ->setName(pht('View HMAC Key')) 147 + ->setIcon('fa-key') 148 + ->setDisabled(!$can_edit) 149 + ->setWorkflow(true) 150 + ->setHref($key_view_uri)); 151 + 152 + $curtain->addAction( 153 + id(new PhabricatorActionView()) 154 + ->setName(pht('Regenerate HMAC Key')) 155 + ->setIcon('fa-refresh') 156 + ->setDisabled(!$can_edit) 157 + ->setWorkflow(true) 158 + ->setHref($key_cycle_uri)); 135 159 136 160 return $curtain; 137 161 }
+29
src/applications/herald/garbagecollector/HeraldWebhookRequestGarbageCollector.php
··· 1 + <?php 2 + 3 + final class HeraldWebhookRequestGarbageCollector 4 + extends PhabricatorGarbageCollector { 5 + 6 + const COLLECTORCONST = 'herald.webhooks'; 7 + 8 + public function getCollectorName() { 9 + return pht('Herald Webhooks'); 10 + } 11 + 12 + public function getDefaultRetentionPolicy() { 13 + return phutil_units('7 days in seconds'); 14 + } 15 + 16 + protected function collectGarbage() { 17 + $table = new HeraldWebhookRequest(); 18 + $conn_w = $table->establishConnection('w'); 19 + 20 + queryfx( 21 + $conn_w, 22 + 'DELETE FROM %T WHERE dateCreated < %d LIMIT 100', 23 + $table->getTableName(), 24 + $this->getGarbageEpoch()); 25 + 26 + return ($conn_w->getAffectedRows() == 100); 27 + } 28 + 29 + }
+42 -3
src/applications/herald/management/HeraldWebhookCallManagementWorkflow.php
··· 6 6 protected function didConstruct() { 7 7 $this 8 8 ->setName('call') 9 - ->setExamples('**call** --id __id__') 9 + ->setExamples('**call** --id __id__ [--object __object__]') 10 10 ->setSynopsis(pht('Call a webhook.')) 11 11 ->setArguments( 12 12 array( ··· 15 15 'param' => 'id', 16 16 'help' => pht('Webhook ID to call'), 17 17 ), 18 + array( 19 + 'name' => 'object', 20 + 'param' => 'object', 21 + 'help' => pht('Submit transactions for a particular object.'), 22 + ), 23 + array( 24 + 'name' => 'silent', 25 + 'help' => pht('Set the "silent" flag on the request.'), 26 + ), 27 + array( 28 + 'name' => 'secure', 29 + 'help' => pht('Set the "secure" flag on the request.'), 30 + ), 18 31 )); 19 32 } 20 33 ··· 39 52 $id)); 40 53 } 41 54 42 - $object = $hook; 55 + $object_name = $args->getArg('object'); 56 + if ($object_name === null) { 57 + $object = $hook; 58 + } else { 59 + $objects = id(new PhabricatorObjectQuery()) 60 + ->setViewer($viewer) 61 + ->withNames(array($object_name)) 62 + ->execute(); 63 + if (!$objects) { 64 + throw new PhutilArgumentUsageException( 65 + pht( 66 + 'Unable to load specified object ("%s").', 67 + $object_name)); 68 + } 69 + $object = head($objects); 70 + } 43 71 44 - $application_phid = id(new PhabricatorHeraldApplication())->getPHID(); 72 + $xaction_query = 73 + PhabricatorApplicationTransactionQuery::newQueryForObject($object); 74 + 75 + $xactions = $xaction_query 76 + ->withObjectPHIDs(array($object->getPHID())) 77 + ->setViewer($viewer) 78 + ->setLimit(10) 79 + ->execute(); 45 80 46 81 $request = HeraldWebhookRequest::initializeNewWebhookRequest($hook) 47 82 ->setObjectPHID($object->getPHID()) 83 + ->setIsTestAction(true) 84 + ->setIsSilentAction((bool)$args->getArg('silent')) 85 + ->setIsSecureAction((bool)$args->getArg('secure')) 86 + ->setTransactionPHIDs(mpull($xactions, 'getPHID')) 48 87 ->save(); 49 88 50 89 PhabricatorWorker::setRunAllTasksInProcess(true);
+9 -2
src/applications/herald/query/HeraldWebhookSearchEngine.php
··· 80 80 ->setViewer($viewer); 81 81 foreach ($hooks as $hook) { 82 82 $item = id(new PHUIObjectItemView()) 83 - ->setObjectName(pht('Hook %d', $hook->getID())) 83 + ->setObjectName(pht('Webhook %d', $hook->getID())) 84 84 ->setHeader($hook->getName()) 85 - ->setHref($hook->getURI()); 85 + ->setHref($hook->getURI()) 86 + ->addAttribute($hook->getWebhookURI()); 87 + 88 + $item->addIcon($hook->getStatusIcon(), $hook->getStatusDisplayName()); 89 + 90 + if ($hook->isDisabled()) { 91 + $item->setDisabled(true); 92 + } 86 93 87 94 $list->addItem($item); 88 95 }
+73 -6
src/applications/herald/storage/HeraldWebhook.php
··· 5 5 implements 6 6 PhabricatorPolicyInterface, 7 7 PhabricatorApplicationTransactionInterface, 8 - PhabricatorDestructibleInterface { 8 + PhabricatorDestructibleInterface, 9 + PhabricatorProjectInterface { 9 10 10 11 protected $name; 11 12 protected $webhookURI; ··· 44 45 ->setStatus(self::HOOKSTATUS_ENABLED) 45 46 ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) 46 47 ->setEditPolicy($viewer->getPHID()) 47 - ->setHmacKey(Filesystem::readRandomCharacters(32)); 48 + ->regenerateHMACKey(); 48 49 } 49 50 50 51 public function getURI() { ··· 56 57 } 57 58 58 59 public static function getStatusDisplayNameMap() { 60 + $specs = self::getStatusSpecifications(); 61 + return ipull($specs, 'name', 'key'); 62 + } 63 + 64 + private static function getStatusSpecifications() { 65 + $specs = array( 66 + array( 67 + 'key' => self::HOOKSTATUS_FIREHOSE, 68 + 'name' => pht('Firehose'), 69 + 'color' => 'orange', 70 + 'icon' => 'fa-star-o', 71 + ), 72 + array( 73 + 'key' => self::HOOKSTATUS_ENABLED, 74 + 'name' => pht('Enabled'), 75 + 'color' => 'bluegrey', 76 + 'icon' => 'fa-check', 77 + ), 78 + array( 79 + 'key' => self::HOOKSTATUS_DISABLED, 80 + 'name' => pht('Disabled'), 81 + 'color' => 'dark', 82 + 'icon' => 'fa-ban', 83 + ), 84 + ); 85 + 86 + return ipull($specs, null, 'key'); 87 + } 88 + 89 + 90 + private static function getSpecificationForStatus($status) { 91 + $specs = self::getStatusSpecifications(); 92 + 93 + if (isset($specs[$status])) { 94 + return $specs[$status]; 95 + } 96 + 59 97 return array( 60 - self::HOOKSTATUS_FIREHOSE => pht('Firehose'), 61 - self::HOOKSTATUS_ENABLED => pht('Enabled'), 62 - self::HOOKSTATUS_DISABLED => pht('Disabled'), 98 + 'key' => $status, 99 + 'name' => pht('Unknown ("%s")', $status), 100 + 'icon' => 'fa-question', 101 + 'color' => 'indigo', 63 102 ); 64 103 } 65 104 105 + public static function getDisplayNameForStatus($status) { 106 + $spec = self::getSpecificationForStatus($status); 107 + return $spec['name']; 108 + } 109 + 110 + public static function getIconForStatus($status) { 111 + $spec = self::getSpecificationForStatus($status); 112 + return $spec['icon']; 113 + } 114 + 115 + public static function getColorForStatus($status) { 116 + $spec = self::getSpecificationForStatus($status); 117 + return $spec['color']; 118 + } 119 + 66 120 public function getStatusDisplayName() { 67 121 $status = $this->getStatus(); 68 - return idx($this->getStatusDisplayNameMap(), $status); 122 + return self::getDisplayNameForStatus($status); 123 + } 124 + 125 + public function getStatusIcon() { 126 + $status = $this->getStatus(); 127 + return self::getIconForStatus($status); 128 + } 129 + 130 + public function getStatusColor() { 131 + $status = $this->getStatus(); 132 + return self::getColorForStatus($status); 69 133 } 70 134 71 135 public function getErrorBackoffWindow() { ··· 101 165 return false; 102 166 } 103 167 168 + public function regenerateHMACKey() { 169 + return $this->setHMACKey(Filesystem::readRandomCharacters(32)); 170 + } 104 171 105 172 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 106 173
+24
src/applications/herald/storage/HeraldWebhookRequest.php
··· 116 116 return $this->getProperty('transactionPHIDs', array()); 117 117 } 118 118 119 + public function setIsSilentAction($bool) { 120 + return $this->setProperty('silent', $bool); 121 + } 122 + 123 + public function getIsSilentAction() { 124 + return $this->getProperty('silent', false); 125 + } 126 + 127 + public function setIsTestAction($bool) { 128 + return $this->setProperty('test', $bool); 129 + } 130 + 131 + public function getIsTestAction() { 132 + return $this->getProperty('test', false); 133 + } 134 + 135 + public function setIsSecureAction($bool) { 136 + return $this->setProperty('secure', $bool); 137 + } 138 + 139 + public function getIsSecureAction() { 140 + return $this->getProperty('secure', false); 141 + } 142 + 119 143 public function queueCall() { 120 144 PhabricatorWorker::scheduleTask( 121 145 'HeraldWebhookWorker',
+10
src/applications/herald/view/HeraldWebhookRequestListView.php
··· 44 44 $rowc[] = null; 45 45 } 46 46 47 + $last_epoch = $request->getLastRequestEpoch(); 48 + if ($request->getLastRequestEpoch()) { 49 + $last_request = phabricator_datetime($last_epoch, $viewer); 50 + } else { 51 + $last_request = null; 52 + } 53 + 47 54 $rows[] = array( 48 55 $request->getID(), 49 56 $icon, 50 57 $handles[$request->getObjectPHID()]->renderLink(), 51 58 $request->getErrorType(), 52 59 $request->getErrorCode(), 60 + $last_request, 53 61 ); 54 62 } 55 63 ··· 62 70 pht('Object'), 63 71 pht('Type'), 64 72 pht('Code'), 73 + pht('Requested At'), 65 74 )) 66 75 ->setColumnClasses( 67 76 array( 68 77 'n', 69 78 '', 70 79 'wide', 80 + '', 71 81 '', 72 82 '', 73 83 ));
+6 -1
src/applications/herald/worker/HeraldWebhookWorker.php
··· 143 143 'object' => array( 144 144 'phid' => $object->getPHID(), 145 145 ), 146 + 'action' => array( 147 + 'test' => $request->getIsTestAction(), 148 + 'silent' => $request->getIsSilentAction(), 149 + 'secure' => $request->getIsSecureAction(), 150 + ), 146 151 'transactions' => $xaction_data, 147 152 ); 148 153 149 - $payload = phutil_json_encode($payload); 154 + $payload = id(new PhutilJSON())->encodeFormatted($payload); 150 155 $key = $hook->getHmacKey(); 151 156 $signature = PhabricatorHash::digestHMACSHA256($payload, $key); 152 157 $uri = $hook->getWebhookURI();
+16 -4
src/applications/herald/xaction/HeraldWebhookStatusTransaction.php
··· 14 14 } 15 15 16 16 public function getTitle() { 17 + $old_value = $this->getOldValue(); 18 + $new_value = $this->getNewValue(); 19 + 20 + $old_status = HeraldWebhook::getDisplayNameForStatus($old_value); 21 + $new_status = HeraldWebhook::getDisplayNameForStatus($new_value); 22 + 17 23 return pht( 18 24 '%s changed hook status from %s to %s.', 19 25 $this->renderAuthor(), 20 - $this->renderOldValue(), 21 - $this->renderNewValue()); 26 + $this->renderValue($old_status), 27 + $this->renderValue($new_status)); 22 28 } 23 29 24 30 public function getTitleForFeed() { 31 + $old_value = $this->getOldValue(); 32 + $new_value = $this->getNewValue(); 33 + 34 + $old_status = HeraldWebhook::getDisplayNameForStatus($old_value); 35 + $new_status = HeraldWebhook::getDisplayNameForStatus($new_value); 36 + 25 37 return pht( 26 38 '%s changed %s from %s to %s.', 27 39 $this->renderAuthor(), 28 40 $this->renderObject(), 29 - $this->renderOldValue(), 30 - $this->renderNewValue()); 41 + $this->renderValue($old_status), 42 + $this->renderValue($new_status)); 31 43 } 32 44 33 45 public function validateTransactions($object, array $xactions) {