mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-18 19:40:55 +01:00
Add skeleton code for webhooks
Summary: Ref T11330. Adds general support for webhooks. This is still rough and missing a lot of pieces -- and not yet useful for anything -- but can make HTTP requests. Test Plan: Used `bin/webhook call ...` to complete requests to a test endpoint. Maniphest Tasks: T11330 Differential Revision: https://secure.phabricator.com/D19045
This commit is contained in:
parent
9386e436fe
commit
0470125d9e
34 changed files with 1896 additions and 14 deletions
1
bin/webhook
Symbolic link
1
bin/webhook
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../scripts/setup/manage_webhook.php
|
12
resources/sql/autopatches/20180209.hook.01.hook.sql
Normal file
12
resources/sql/autopatches/20180209.hook.01.hook.sql
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
CREATE TABLE {$NAMESPACE}_herald.herald_webhook (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
phid VARBINARY(64) NOT NULL,
|
||||||
|
name VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
webhookURI VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
viewPolicy VARBINARY(64) NOT NULL,
|
||||||
|
editPolicy VARBINARY(64) NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
hmacKey VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
dateCreated INT UNSIGNED NOT NULL,
|
||||||
|
dateModified INT UNSIGNED NOT NULL
|
||||||
|
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
19
resources/sql/autopatches/20180209.hook.02.hookxaction.sql
Normal file
19
resources/sql/autopatches/20180209.hook.02.hookxaction.sql
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
CREATE TABLE {$NAMESPACE}_herald.herald_webhooktransaction (
|
||||||
|
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};
|
12
resources/sql/autopatches/20180209.hook.03.hookrequest.sql
Normal file
12
resources/sql/autopatches/20180209.hook.03.hookrequest.sql
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
CREATE TABLE {$NAMESPACE}_herald.herald_webhookrequest (
|
||||||
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
phid VARBINARY(64) NOT NULL,
|
||||||
|
webhookPHID VARBINARY(64) NOT NULL,
|
||||||
|
objectPHID VARBINARY(64) NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
lastRequestResult VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT},
|
||||||
|
lastRequestEpoch INT UNSIGNED NOT NULL,
|
||||||
|
dateCreated INT UNSIGNED NOT NULL,
|
||||||
|
dateModified INT UNSIGNED NOT NULL
|
||||||
|
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
21
scripts/setup/manage_webhook.php
Executable file
21
scripts/setup/manage_webhook.php
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
$root = dirname(dirname(dirname(__FILE__)));
|
||||||
|
require_once $root.'/scripts/init/init-script.php';
|
||||||
|
|
||||||
|
$args = new PhutilArgumentParser($argv);
|
||||||
|
$args->setTagline(pht('manage webhooks'));
|
||||||
|
$args->setSynopsis(<<<EOSYNOPSIS
|
||||||
|
**webhook** __command__ [__options__]
|
||||||
|
Manage webhooks.
|
||||||
|
|
||||||
|
EOSYNOPSIS
|
||||||
|
);
|
||||||
|
$args->parseStandardArguments();
|
||||||
|
|
||||||
|
$workflows = id(new PhutilClassMapQuery())
|
||||||
|
->setAncestorClass('HeraldWebhookManagementWorkflow')
|
||||||
|
->execute();
|
||||||
|
$workflows[] = new PhutilHelpArgumentWorkflow();
|
||||||
|
$args->parseWorkflows($workflows);
|
|
@ -1364,6 +1364,7 @@ phutil_register_library_map(array(
|
||||||
'HeraldContentSourceField' => 'applications/herald/field/HeraldContentSourceField.php',
|
'HeraldContentSourceField' => 'applications/herald/field/HeraldContentSourceField.php',
|
||||||
'HeraldController' => 'applications/herald/controller/HeraldController.php',
|
'HeraldController' => 'applications/herald/controller/HeraldController.php',
|
||||||
'HeraldCoreStateReasons' => 'applications/herald/state/HeraldCoreStateReasons.php',
|
'HeraldCoreStateReasons' => 'applications/herald/state/HeraldCoreStateReasons.php',
|
||||||
|
'HeraldCreateWebhooksCapability' => 'applications/herald/capability/HeraldCreateWebhooksCapability.php',
|
||||||
'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php',
|
'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php',
|
||||||
'HeraldDeprecatedFieldGroup' => 'applications/herald/field/HeraldDeprecatedFieldGroup.php',
|
'HeraldDeprecatedFieldGroup' => 'applications/herald/field/HeraldDeprecatedFieldGroup.php',
|
||||||
'HeraldDifferentialAdapter' => 'applications/differential/herald/HeraldDifferentialAdapter.php',
|
'HeraldDifferentialAdapter' => 'applications/differential/herald/HeraldDifferentialAdapter.php',
|
||||||
|
@ -1440,6 +1441,30 @@ phutil_register_library_map(array(
|
||||||
'HeraldTranscriptSearchEngine' => 'applications/herald/query/HeraldTranscriptSearchEngine.php',
|
'HeraldTranscriptSearchEngine' => 'applications/herald/query/HeraldTranscriptSearchEngine.php',
|
||||||
'HeraldTranscriptTestCase' => 'applications/herald/storage/__tests__/HeraldTranscriptTestCase.php',
|
'HeraldTranscriptTestCase' => 'applications/herald/storage/__tests__/HeraldTranscriptTestCase.php',
|
||||||
'HeraldUtilityActionGroup' => 'applications/herald/action/HeraldUtilityActionGroup.php',
|
'HeraldUtilityActionGroup' => 'applications/herald/action/HeraldUtilityActionGroup.php',
|
||||||
|
'HeraldWebhook' => 'applications/herald/storage/HeraldWebhook.php',
|
||||||
|
'HeraldWebhookCallManagementWorkflow' => 'applications/herald/management/HeraldWebhookCallManagementWorkflow.php',
|
||||||
|
'HeraldWebhookController' => 'applications/herald/controller/HeraldWebhookController.php',
|
||||||
|
'HeraldWebhookEditController' => 'applications/herald/controller/HeraldWebhookEditController.php',
|
||||||
|
'HeraldWebhookEditEngine' => 'applications/herald/editor/HeraldWebhookEditEngine.php',
|
||||||
|
'HeraldWebhookEditor' => 'applications/herald/editor/HeraldWebhookEditor.php',
|
||||||
|
'HeraldWebhookListController' => 'applications/herald/controller/HeraldWebhookListController.php',
|
||||||
|
'HeraldWebhookManagementWorkflow' => 'applications/herald/management/HeraldWebhookManagementWorkflow.php',
|
||||||
|
'HeraldWebhookNameTransaction' => 'applications/herald/xaction/HeraldWebhookNameTransaction.php',
|
||||||
|
'HeraldWebhookPHIDType' => 'applications/herald/phid/HeraldWebhookPHIDType.php',
|
||||||
|
'HeraldWebhookQuery' => 'applications/herald/query/HeraldWebhookQuery.php',
|
||||||
|
'HeraldWebhookRequest' => 'applications/herald/storage/HeraldWebhookRequest.php',
|
||||||
|
'HeraldWebhookRequestListView' => 'applications/herald/view/HeraldWebhookRequestListView.php',
|
||||||
|
'HeraldWebhookRequestPHIDType' => 'applications/herald/phid/HeraldWebhookRequestPHIDType.php',
|
||||||
|
'HeraldWebhookRequestQuery' => 'applications/herald/query/HeraldWebhookRequestQuery.php',
|
||||||
|
'HeraldWebhookSearchEngine' => 'applications/herald/query/HeraldWebhookSearchEngine.php',
|
||||||
|
'HeraldWebhookStatusTransaction' => 'applications/herald/xaction/HeraldWebhookStatusTransaction.php',
|
||||||
|
'HeraldWebhookTestController' => 'applications/herald/controller/HeraldWebhookTestController.php',
|
||||||
|
'HeraldWebhookTransaction' => 'applications/herald/storage/HeraldWebhookTransaction.php',
|
||||||
|
'HeraldWebhookTransactionQuery' => 'applications/herald/query/HeraldWebhookTransactionQuery.php',
|
||||||
|
'HeraldWebhookTransactionType' => 'applications/herald/xaction/HeraldWebhookTransactionType.php',
|
||||||
|
'HeraldWebhookURITransaction' => 'applications/herald/xaction/HeraldWebhookURITransaction.php',
|
||||||
|
'HeraldWebhookViewController' => 'applications/herald/controller/HeraldWebhookViewController.php',
|
||||||
|
'HeraldWebhookWorker' => 'applications/herald/worker/HeraldWebhookWorker.php',
|
||||||
'Javelin' => 'infrastructure/javelin/Javelin.php',
|
'Javelin' => 'infrastructure/javelin/Javelin.php',
|
||||||
'LegalpadController' => 'applications/legalpad/controller/LegalpadController.php',
|
'LegalpadController' => 'applications/legalpad/controller/LegalpadController.php',
|
||||||
'LegalpadCreateDocumentsCapability' => 'applications/legalpad/capability/LegalpadCreateDocumentsCapability.php',
|
'LegalpadCreateDocumentsCapability' => 'applications/legalpad/capability/LegalpadCreateDocumentsCapability.php',
|
||||||
|
@ -6614,6 +6639,7 @@ phutil_register_library_map(array(
|
||||||
'HeraldContentSourceField' => 'HeraldField',
|
'HeraldContentSourceField' => 'HeraldField',
|
||||||
'HeraldController' => 'PhabricatorController',
|
'HeraldController' => 'PhabricatorController',
|
||||||
'HeraldCoreStateReasons' => 'HeraldStateReasons',
|
'HeraldCoreStateReasons' => 'HeraldStateReasons',
|
||||||
|
'HeraldCreateWebhooksCapability' => 'PhabricatorPolicyCapability',
|
||||||
'HeraldDAO' => 'PhabricatorLiskDAO',
|
'HeraldDAO' => 'PhabricatorLiskDAO',
|
||||||
'HeraldDeprecatedFieldGroup' => 'HeraldFieldGroup',
|
'HeraldDeprecatedFieldGroup' => 'HeraldFieldGroup',
|
||||||
'HeraldDifferentialAdapter' => 'HeraldAdapter',
|
'HeraldDifferentialAdapter' => 'HeraldAdapter',
|
||||||
|
@ -6704,6 +6730,39 @@ phutil_register_library_map(array(
|
||||||
'HeraldTranscriptSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
'HeraldTranscriptSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||||
'HeraldTranscriptTestCase' => 'PhabricatorTestCase',
|
'HeraldTranscriptTestCase' => 'PhabricatorTestCase',
|
||||||
'HeraldUtilityActionGroup' => 'HeraldActionGroup',
|
'HeraldUtilityActionGroup' => 'HeraldActionGroup',
|
||||||
|
'HeraldWebhook' => array(
|
||||||
|
'HeraldDAO',
|
||||||
|
'PhabricatorPolicyInterface',
|
||||||
|
'PhabricatorApplicationTransactionInterface',
|
||||||
|
'PhabricatorDestructibleInterface',
|
||||||
|
),
|
||||||
|
'HeraldWebhookCallManagementWorkflow' => 'HeraldWebhookManagementWorkflow',
|
||||||
|
'HeraldWebhookController' => 'HeraldController',
|
||||||
|
'HeraldWebhookEditController' => 'HeraldWebhookController',
|
||||||
|
'HeraldWebhookEditEngine' => 'PhabricatorEditEngine',
|
||||||
|
'HeraldWebhookEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||||
|
'HeraldWebhookListController' => 'HeraldWebhookController',
|
||||||
|
'HeraldWebhookManagementWorkflow' => 'PhabricatorManagementWorkflow',
|
||||||
|
'HeraldWebhookNameTransaction' => 'HeraldWebhookTransactionType',
|
||||||
|
'HeraldWebhookPHIDType' => 'PhabricatorPHIDType',
|
||||||
|
'HeraldWebhookQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
|
'HeraldWebhookRequest' => array(
|
||||||
|
'HeraldDAO',
|
||||||
|
'PhabricatorPolicyInterface',
|
||||||
|
'PhabricatorExtendedPolicyInterface',
|
||||||
|
),
|
||||||
|
'HeraldWebhookRequestListView' => 'AphrontView',
|
||||||
|
'HeraldWebhookRequestPHIDType' => 'PhabricatorPHIDType',
|
||||||
|
'HeraldWebhookRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
|
'HeraldWebhookSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||||
|
'HeraldWebhookStatusTransaction' => 'HeraldWebhookTransactionType',
|
||||||
|
'HeraldWebhookTestController' => 'HeraldWebhookController',
|
||||||
|
'HeraldWebhookTransaction' => 'PhabricatorModularTransaction',
|
||||||
|
'HeraldWebhookTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||||
|
'HeraldWebhookTransactionType' => 'PhabricatorModularTransactionType',
|
||||||
|
'HeraldWebhookURITransaction' => 'HeraldWebhookTransactionType',
|
||||||
|
'HeraldWebhookViewController' => 'HeraldWebhookController',
|
||||||
|
'HeraldWebhookWorker' => 'PhabricatorWorker',
|
||||||
'Javelin' => 'Phobject',
|
'Javelin' => 'Phobject',
|
||||||
'LegalpadController' => 'PhabricatorController',
|
'LegalpadController' => 'PhabricatorController',
|
||||||
'LegalpadCreateDocumentsCapability' => 'PhabricatorPolicyCapability',
|
'LegalpadCreateDocumentsCapability' => 'PhabricatorPolicyCapability',
|
||||||
|
|
|
@ -62,6 +62,17 @@ final class PhabricatorHeraldApplication extends PhabricatorApplication {
|
||||||
'(?P<id>[1-9]\d*)/'
|
'(?P<id>[1-9]\d*)/'
|
||||||
=> 'HeraldTranscriptController',
|
=> 'HeraldTranscriptController',
|
||||||
),
|
),
|
||||||
|
'webhook/' => array(
|
||||||
|
$this->getQueryRoutePattern() => 'HeraldWebhookListController',
|
||||||
|
'view/(?P<id>\d+)/(?:request/(?P<requestID>[^/]+)/)?' =>
|
||||||
|
'HeraldWebhookViewController',
|
||||||
|
$this->getEditRoutePattern('edit/') => 'HeraldWebhookEditController',
|
||||||
|
'test/(?P<id>\d+)/' => 'HeraldWebhookTestController',
|
||||||
|
'key/' => array(
|
||||||
|
'view/(?P<id>\d+)/' => 'HeraldWebhookViewKeyController',
|
||||||
|
'cycle/(?P<id>\d+)/' => 'HeraldWebhookCycleKeyController',
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -72,6 +83,9 @@ final class PhabricatorHeraldApplication extends PhabricatorApplication {
|
||||||
'caption' => pht('Global rules can bypass access controls.'),
|
'caption' => pht('Global rules can bypass access controls.'),
|
||||||
'default' => PhabricatorPolicies::POLICY_ADMIN,
|
'default' => PhabricatorPolicies::POLICY_ADMIN,
|
||||||
),
|
),
|
||||||
|
HeraldCreateWebhooksCapability::CAPABILITY => array(
|
||||||
|
'default' => PhabricatorPolicies::POLICY_ADMIN,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldCreateWebhooksCapability
|
||||||
|
extends PhabricatorPolicyCapability {
|
||||||
|
|
||||||
|
const CAPABILITY = 'herald.webhooks';
|
||||||
|
|
||||||
|
public function getCapabilityName() {
|
||||||
|
return pht('Can Create Webhooks');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function describeCapabilityRejection() {
|
||||||
|
return pht('You do not have permission to create webhooks.');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -6,18 +6,6 @@ abstract class HeraldController extends PhabricatorController {
|
||||||
return $this->buildSideNavView()->getMenu();
|
return $this->buildSideNavView()->getMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function buildApplicationCrumbs() {
|
|
||||||
$crumbs = parent::buildApplicationCrumbs();
|
|
||||||
|
|
||||||
$crumbs->addAction(
|
|
||||||
id(new PHUIListItemView())
|
|
||||||
->setName(pht('Create Herald Rule'))
|
|
||||||
->setHref($this->getApplicationURI('create/'))
|
|
||||||
->setIcon('fa-plus-square'));
|
|
||||||
|
|
||||||
return $crumbs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildSideNavView() {
|
public function buildSideNavView() {
|
||||||
$viewer = $this->getViewer();
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
|
@ -32,6 +20,9 @@ abstract class HeraldController extends PhabricatorController {
|
||||||
->addFilter('test', pht('Test Console'))
|
->addFilter('test', pht('Test Console'))
|
||||||
->addFilter('transcript', pht('Transcripts'));
|
->addFilter('transcript', pht('Transcripts'));
|
||||||
|
|
||||||
|
$nav->addLabel(pht('Webhooks'))
|
||||||
|
->addFilter('webhook', pht('Webhooks'));
|
||||||
|
|
||||||
$nav->selectFilter(null);
|
$nav->selectFilter(null);
|
||||||
|
|
||||||
return $nav;
|
return $nav;
|
||||||
|
|
|
@ -17,5 +17,16 @@ final class HeraldRuleListController extends HeraldController {
|
||||||
return $this->delegateToController($controller);
|
return $this->delegateToController($controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function buildApplicationCrumbs() {
|
||||||
|
$crumbs = parent::buildApplicationCrumbs();
|
||||||
|
|
||||||
|
$crumbs->addAction(
|
||||||
|
id(new PHUIListItemView())
|
||||||
|
->setName(pht('Create Herald Rule'))
|
||||||
|
->setHref($this->getApplicationURI('create/'))
|
||||||
|
->setIcon('fa-plus-square'));
|
||||||
|
|
||||||
|
return $crumbs;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class HeraldWebhookController extends HeraldController {
|
||||||
|
|
||||||
|
protected function buildApplicationCrumbs() {
|
||||||
|
$crumbs = parent::buildApplicationCrumbs();
|
||||||
|
|
||||||
|
$crumbs->addTextCrumb(
|
||||||
|
pht('Webhooks'),
|
||||||
|
$this->getApplicationURI('webhook/'));
|
||||||
|
|
||||||
|
return $crumbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookEditController
|
||||||
|
extends HeraldWebhookController {
|
||||||
|
|
||||||
|
public function handleRequest(AphrontRequest $request) {
|
||||||
|
return id(new HeraldWebhookEditEngine())
|
||||||
|
->setController($this)
|
||||||
|
->buildResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookListController
|
||||||
|
extends HeraldWebhookController {
|
||||||
|
|
||||||
|
public function shouldAllowPublic() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleRequest(AphrontRequest $request) {
|
||||||
|
return id(new HeraldWebhookSearchEngine())
|
||||||
|
->setController($this)
|
||||||
|
->buildResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildApplicationCrumbs() {
|
||||||
|
$crumbs = parent::buildApplicationCrumbs();
|
||||||
|
|
||||||
|
id(new HeraldWebhookEditEngine())
|
||||||
|
->setViewer($this->getViewer())
|
||||||
|
->addActionToCrumbs($crumbs);
|
||||||
|
|
||||||
|
return $crumbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookTestController
|
||||||
|
extends HeraldWebhookController {
|
||||||
|
|
||||||
|
public function handleRequest(AphrontRequest $request) {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
|
$hook = id(new HeraldWebhookQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withIDs(array($request->getURIData('id')))
|
||||||
|
->requireCapabilities(
|
||||||
|
array(
|
||||||
|
PhabricatorPolicyCapability::CAN_VIEW,
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT,
|
||||||
|
))
|
||||||
|
->executeOne();
|
||||||
|
if (!$hook) {
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->isFormPost()) {
|
||||||
|
$object = $hook;
|
||||||
|
|
||||||
|
$request = HeraldWebhookRequest::initializeNewWebhookRequest($hook)
|
||||||
|
->setObjectPHID($object->getPHID())
|
||||||
|
->save();
|
||||||
|
|
||||||
|
$request->queueCall();
|
||||||
|
|
||||||
|
$next_uri = $hook->getURI().'request/'.$request->getID().'/';
|
||||||
|
|
||||||
|
return id(new AphrontRedirectResponse())->setURI($next_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->newDialog()
|
||||||
|
->setTitle(pht('New Test Request'))
|
||||||
|
->appendParagraph(
|
||||||
|
pht('This will make a new test request to the configured URI.'))
|
||||||
|
->addCancelButton($hook->getURI())
|
||||||
|
->addSubmitButton(pht('Make Request'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookViewController
|
||||||
|
extends HeraldWebhookController {
|
||||||
|
|
||||||
|
public function shouldAllowPublic() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleRequest(AphrontRequest $request) {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
|
$hook = id(new HeraldWebhookQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withIDs(array($request->getURIData('id')))
|
||||||
|
->executeOne();
|
||||||
|
if (!$hook) {
|
||||||
|
return new Aphront404Response();
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = $this->buildHeaderView($hook);
|
||||||
|
|
||||||
|
$warnings = null;
|
||||||
|
if ($hook->isInErrorBackoff($viewer)) {
|
||||||
|
$message = pht(
|
||||||
|
'Many requests to this webhook have failed recently (at least %s '.
|
||||||
|
'errors in the last %s seconds). New requests are temporarily paused.',
|
||||||
|
$hook->getErrorBackoffThreshold(),
|
||||||
|
$hook->getErrorBackoffWindow());
|
||||||
|
|
||||||
|
$warnings = id(new PHUIInfoView())
|
||||||
|
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
|
||||||
|
->setErrors(
|
||||||
|
array(
|
||||||
|
$message,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$curtain = $this->buildCurtain($hook);
|
||||||
|
$properties_view = $this->buildPropertiesView($hook);
|
||||||
|
|
||||||
|
$timeline = $this->buildTransactionTimeline(
|
||||||
|
$hook,
|
||||||
|
new HeraldWebhookTransactionQuery());
|
||||||
|
$timeline->setShouldTerminate(true);
|
||||||
|
|
||||||
|
$requests = id(new HeraldWebhookRequestQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withWebhookPHIDs(array($hook->getPHID()))
|
||||||
|
->setLimit(20)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$requests_table = id(new HeraldWebhookRequestListView())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->setRequests($requests)
|
||||||
|
->setHighlightID($request->getURIData('requestID'));
|
||||||
|
|
||||||
|
$requests_view = id(new PHUIObjectBoxView())
|
||||||
|
->setHeaderText(pht('Recent Requests'))
|
||||||
|
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
|
||||||
|
->setTable($requests_table);
|
||||||
|
|
||||||
|
$hook_view = id(new PHUITwoColumnView())
|
||||||
|
->setHeader($header)
|
||||||
|
->setMainColumn(
|
||||||
|
array(
|
||||||
|
$warnings,
|
||||||
|
$properties_view,
|
||||||
|
$requests_view,
|
||||||
|
$timeline,
|
||||||
|
))
|
||||||
|
->setCurtain($curtain);
|
||||||
|
|
||||||
|
$crumbs = $this->buildApplicationCrumbs()
|
||||||
|
->addTextCrumb(pht('Webhook %d', $hook->getID()))
|
||||||
|
->setBorder(true);
|
||||||
|
|
||||||
|
return $this->newPage()
|
||||||
|
->setTitle(
|
||||||
|
array(
|
||||||
|
pht('Webhook %d', $hook->getID()),
|
||||||
|
$hook->getName(),
|
||||||
|
))
|
||||||
|
->setCrumbs($crumbs)
|
||||||
|
->setPageObjectPHIDs(
|
||||||
|
array(
|
||||||
|
$hook->getPHID(),
|
||||||
|
))
|
||||||
|
->appendChild($hook_view);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildHeaderView(HeraldWebhook $hook) {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
|
$title = $hook->getName();
|
||||||
|
|
||||||
|
$header = id(new PHUIHeaderView())
|
||||||
|
->setHeader($title)
|
||||||
|
->setViewer($viewer)
|
||||||
|
->setPolicyObject($hook)
|
||||||
|
->setHeaderIcon('fa-cloud-upload');
|
||||||
|
|
||||||
|
return $header;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function buildCurtain(HeraldWebhook $hook) {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
$curtain = $this->newCurtainView($hook);
|
||||||
|
|
||||||
|
$can_edit = PhabricatorPolicyFilter::hasCapability(
|
||||||
|
$viewer,
|
||||||
|
$hook,
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT);
|
||||||
|
|
||||||
|
$id = $hook->getID();
|
||||||
|
$edit_uri = $this->getApplicationURI("webhook/edit/{$id}/");
|
||||||
|
$test_uri = $this->getApplicationURI("webhook/test/{$id}/");
|
||||||
|
|
||||||
|
$curtain->addAction(
|
||||||
|
id(new PhabricatorActionView())
|
||||||
|
->setName(pht('Edit Webhook'))
|
||||||
|
->setIcon('fa-pencil')
|
||||||
|
->setDisabled(!$can_edit)
|
||||||
|
->setWorkflow(!$can_edit)
|
||||||
|
->setHref($edit_uri));
|
||||||
|
|
||||||
|
$curtain->addAction(
|
||||||
|
id(new PhabricatorActionView())
|
||||||
|
->setName(pht('New Test Request'))
|
||||||
|
->setIcon('fa-cloud-upload')
|
||||||
|
->setDisabled(!$can_edit)
|
||||||
|
->setWorkflow(true)
|
||||||
|
->setHref($test_uri));
|
||||||
|
|
||||||
|
return $curtain;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function buildPropertiesView(HeraldWebhook $hook) {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
|
$properties = id(new PHUIPropertyListView())
|
||||||
|
->setViewer($viewer);
|
||||||
|
|
||||||
|
$properties->addProperty(
|
||||||
|
pht('URI'),
|
||||||
|
$hook->getWebhookURI());
|
||||||
|
|
||||||
|
$properties->addProperty(
|
||||||
|
pht('Status'),
|
||||||
|
$hook->getStatusDisplayName());
|
||||||
|
|
||||||
|
return id(new PHUIObjectBoxView())
|
||||||
|
->setHeaderText(pht('Details'))
|
||||||
|
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
|
||||||
|
->appendChild($properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
105
src/applications/herald/editor/HeraldWebhookEditEngine.php
Normal file
105
src/applications/herald/editor/HeraldWebhookEditEngine.php
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookEditEngine
|
||||||
|
extends PhabricatorEditEngine {
|
||||||
|
|
||||||
|
const ENGINECONST = 'herald.webhook';
|
||||||
|
|
||||||
|
public function isEngineConfigurable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEngineName() {
|
||||||
|
return pht('Webhooks');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSummaryHeader() {
|
||||||
|
return pht('Edit Webhook Configurations');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSummaryText() {
|
||||||
|
return pht('This engine is used to edit webhooks.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEngineApplicationClass() {
|
||||||
|
return 'PhabricatorHeraldApplication';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function newEditableObject() {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
return HeraldWebhook::initializeNewWebhook($viewer);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function newObjectQuery() {
|
||||||
|
return new HeraldWebhookQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectCreateTitleText($object) {
|
||||||
|
return pht('Create Webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectCreateButtonText($object) {
|
||||||
|
return pht('Create Webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectEditTitleText($object) {
|
||||||
|
return pht('Edit Webhook: %s', $object->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectEditShortText($object) {
|
||||||
|
return pht('Edit Webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectCreateShortText() {
|
||||||
|
return pht('Create Webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectName() {
|
||||||
|
return pht('Webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getEditorURI() {
|
||||||
|
return '/herald/webhook/edit/';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectCreateCancelURI($object) {
|
||||||
|
return '/herald/webhook/';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getObjectViewURI($object) {
|
||||||
|
return $object->getURI();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getCreateNewObjectPolicy() {
|
||||||
|
return $this->getApplication()->getPolicy(
|
||||||
|
HeraldCreateWebhooksCapability::CAPABILITY);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildCustomEditFields($object) {
|
||||||
|
return array(
|
||||||
|
id(new PhabricatorTextEditField())
|
||||||
|
->setKey('name')
|
||||||
|
->setLabel(pht('Name'))
|
||||||
|
->setDescription(pht('Name of the webhook.'))
|
||||||
|
->setTransactionType(HeraldWebhookNameTransaction::TRANSACTIONTYPE)
|
||||||
|
->setIsRequired(true)
|
||||||
|
->setValue($object->getName()),
|
||||||
|
id(new PhabricatorTextEditField())
|
||||||
|
->setKey('uri')
|
||||||
|
->setLabel(pht('URI'))
|
||||||
|
->setDescription(pht('URI for the webhook.'))
|
||||||
|
->setTransactionType(HeraldWebhookURITransaction::TRANSACTIONTYPE)
|
||||||
|
->setIsRequired(true)
|
||||||
|
->setValue($object->getWebhookURI()),
|
||||||
|
id(new PhabricatorSelectEditField())
|
||||||
|
->setKey('status')
|
||||||
|
->setLabel(pht('Status'))
|
||||||
|
->setDescription(pht('Status mode for the webhook.'))
|
||||||
|
->setTransactionType(HeraldWebhookStatusTransaction::TRANSACTIONTYPE)
|
||||||
|
->setOptions(HeraldWebhook::getStatusDisplayNameMap())
|
||||||
|
->setValue($object->getStatus()),
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
22
src/applications/herald/editor/HeraldWebhookEditor.php
Normal file
22
src/applications/herald/editor/HeraldWebhookEditor.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookEditor
|
||||||
|
extends PhabricatorApplicationTransactionEditor {
|
||||||
|
|
||||||
|
public function getEditorApplicationClass() {
|
||||||
|
return 'PhabricatorHeraldApplication';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEditorObjectsDescription() {
|
||||||
|
return pht('Webhooks');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreateObjectTitle($author, $object) {
|
||||||
|
return pht('%s created this webhook.', $author);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCreateObjectTitleForFeed($author, $object) {
|
||||||
|
return pht('%s created %s.', $author, $object);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookCallManagementWorkflow
|
||||||
|
extends HeraldWebhookManagementWorkflow {
|
||||||
|
|
||||||
|
protected function didConstruct() {
|
||||||
|
$this
|
||||||
|
->setName('call')
|
||||||
|
->setExamples('**call** --id __id__')
|
||||||
|
->setSynopsis(pht('Call a webhook.'))
|
||||||
|
->setArguments(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'name' => 'id',
|
||||||
|
'param' => 'id',
|
||||||
|
'help' => pht('Webhook ID to call'),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(PhutilArgumentParser $args) {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
|
||||||
|
$id = $args->getArg('id');
|
||||||
|
if (!$id) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
pht(
|
||||||
|
'Specify a webhook to call with "--id".'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$hook = id(new HeraldWebhookQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withIDs(array($id))
|
||||||
|
->executeOne();
|
||||||
|
if (!$hook) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
pht(
|
||||||
|
'Unable to load specified webhook ("%s").',
|
||||||
|
$id));
|
||||||
|
}
|
||||||
|
|
||||||
|
$object = $hook;
|
||||||
|
|
||||||
|
$application_phid = id(new PhabricatorHeraldApplication())->getPHID();
|
||||||
|
|
||||||
|
$request = HeraldWebhookRequest::initializeNewWebhookRequest($hook)
|
||||||
|
->setObjectPHID($object->getPHID())
|
||||||
|
->save();
|
||||||
|
|
||||||
|
PhabricatorWorker::setRunAllTasksInProcess(true);
|
||||||
|
$request->queueCall();
|
||||||
|
|
||||||
|
$request->reload();
|
||||||
|
|
||||||
|
echo tsprintf(
|
||||||
|
"%s\n",
|
||||||
|
pht(
|
||||||
|
'Success, got HTTP %s from webhook.',
|
||||||
|
$request->getErrorCode()));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class HeraldWebhookManagementWorkflow
|
||||||
|
extends PhabricatorManagementWorkflow {}
|
49
src/applications/herald/phid/HeraldWebhookPHIDType.php
Normal file
49
src/applications/herald/phid/HeraldWebhookPHIDType.php
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookPHIDType extends PhabricatorPHIDType {
|
||||||
|
|
||||||
|
const TYPECONST = 'HWBH';
|
||||||
|
|
||||||
|
public function getTypeName() {
|
||||||
|
return pht('Webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newObject() {
|
||||||
|
return new HeraldWebhook();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPHIDTypeApplicationClass() {
|
||||||
|
return 'PhabricatorHeraldApplication';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildQueryForObjects(
|
||||||
|
PhabricatorObjectQuery $query,
|
||||||
|
array $phids) {
|
||||||
|
|
||||||
|
return id(new HeraldWebhookQuery())
|
||||||
|
->withPHIDs($phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadHandles(
|
||||||
|
PhabricatorHandleQuery $query,
|
||||||
|
array $handles,
|
||||||
|
array $objects) {
|
||||||
|
|
||||||
|
foreach ($handles as $phid => $handle) {
|
||||||
|
$hook = $objects[$phid];
|
||||||
|
|
||||||
|
$name = $hook->getName();
|
||||||
|
$id = $hook->getID();
|
||||||
|
|
||||||
|
$handle
|
||||||
|
->setName($name)
|
||||||
|
->setURI($hook->getURI())
|
||||||
|
->setFullName(pht('Webhook %d %s', $id, $name));
|
||||||
|
|
||||||
|
if ($hook->isDisabled()) {
|
||||||
|
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookRequestPHIDType extends PhabricatorPHIDType {
|
||||||
|
|
||||||
|
const TYPECONST = 'HWBR';
|
||||||
|
|
||||||
|
public function getTypeName() {
|
||||||
|
return pht('Webhook Request');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newObject() {
|
||||||
|
return new HeraldWebhook();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPHIDTypeApplicationClass() {
|
||||||
|
return 'PhabricatorHeraldApplication';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildQueryForObjects(
|
||||||
|
PhabricatorObjectQuery $query,
|
||||||
|
array $phids) {
|
||||||
|
|
||||||
|
return id(new HeraldWebhookRequestQuery())
|
||||||
|
->withPHIDs($phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadHandles(
|
||||||
|
PhabricatorHandleQuery $query,
|
||||||
|
array $handles,
|
||||||
|
array $objects) {
|
||||||
|
|
||||||
|
foreach ($handles as $phid => $handle) {
|
||||||
|
$request = $objects[$phid];
|
||||||
|
|
||||||
|
// TODO: Fill this in.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
64
src/applications/herald/query/HeraldWebhookQuery.php
Normal file
64
src/applications/herald/query/HeraldWebhookQuery.php
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookQuery
|
||||||
|
extends PhabricatorCursorPagedPolicyAwareQuery {
|
||||||
|
|
||||||
|
private $ids;
|
||||||
|
private $phids;
|
||||||
|
private $statuses;
|
||||||
|
|
||||||
|
public function withIDs(array $ids) {
|
||||||
|
$this->ids = $ids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPHIDs(array $phids) {
|
||||||
|
$this->phids = $phids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withStatuses(array $statuses) {
|
||||||
|
$this->statuses = $statuses;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newResultObject() {
|
||||||
|
return new HeraldWebhook();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadPage() {
|
||||||
|
return $this->loadStandardPage($this->newResultObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
|
||||||
|
$where = parent::buildWhereClauseParts($conn);
|
||||||
|
|
||||||
|
if ($this->ids !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'id IN (%Ld)',
|
||||||
|
$this->ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->phids !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'phid IN (%Ls)',
|
||||||
|
$this->phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->statuses !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'status IN (%Ls)',
|
||||||
|
$this->statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $where;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryApplicationClass() {
|
||||||
|
return 'PhabricatorHeraldApplication';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
126
src/applications/herald/query/HeraldWebhookRequestQuery.php
Normal file
126
src/applications/herald/query/HeraldWebhookRequestQuery.php
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookRequestQuery
|
||||||
|
extends PhabricatorCursorPagedPolicyAwareQuery {
|
||||||
|
|
||||||
|
private $ids;
|
||||||
|
private $phids;
|
||||||
|
private $webhookPHIDs;
|
||||||
|
private $lastRequestEpochMin;
|
||||||
|
private $lastRequestEpochMax;
|
||||||
|
private $lastRequestResults;
|
||||||
|
|
||||||
|
public function withIDs(array $ids) {
|
||||||
|
$this->ids = $ids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withPHIDs(array $phids) {
|
||||||
|
$this->phids = $phids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withWebhookPHIDs(array $phids) {
|
||||||
|
$this->webhookPHIDs = $phids;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newResultObject() {
|
||||||
|
return new HeraldWebhookRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadPage() {
|
||||||
|
return $this->loadStandardPage($this->newResultObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withLastRequestEpochBetween($epoch_min, $epoch_max) {
|
||||||
|
$this->lastRequestEpochMin = $epoch_min;
|
||||||
|
$this->lastRequestEpochMax = $epoch_max;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withLastRequestResults(array $results) {
|
||||||
|
$this->lastRequestResults = $results;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
|
||||||
|
$where = parent::buildWhereClauseParts($conn);
|
||||||
|
|
||||||
|
if ($this->ids !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'id IN (%Ld)',
|
||||||
|
$this->ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->phids !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'phid IN (%Ls)',
|
||||||
|
$this->phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->webhookPHIDs !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'webhookPHID IN (%Ls)',
|
||||||
|
$this->webhookPHIDs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->lastRequestEpochMin !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'lastRequestEpoch >= %d',
|
||||||
|
$this->lastRequestEpochMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->lastRequestEpochMax !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'lastRequestEpoch <= %d',
|
||||||
|
$this->lastRequestEpochMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->lastRequestResults !== null) {
|
||||||
|
$where[] = qsprintf(
|
||||||
|
$conn,
|
||||||
|
'lastRequestResult IN (%Ls)',
|
||||||
|
$this->lastRequestResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $where;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function willFilterPage(array $requests) {
|
||||||
|
$hook_phids = mpull($requests, 'getWebhookPHID');
|
||||||
|
|
||||||
|
$hooks = id(new HeraldWebhookQuery())
|
||||||
|
->setViewer($this->getViewer())
|
||||||
|
->setParentQuery($this)
|
||||||
|
->withPHIDs($hook_phids)
|
||||||
|
->execute();
|
||||||
|
$hooks = mpull($hooks, null, 'getPHID');
|
||||||
|
|
||||||
|
foreach ($requests as $key => $request) {
|
||||||
|
$hook_phid = $request->getWebhookPHID();
|
||||||
|
$hook = idx($hooks, $hook_phid);
|
||||||
|
|
||||||
|
if (!$hook) {
|
||||||
|
unset($requests[$key]);
|
||||||
|
$this->didRejectResult($request);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->attachWebhook($hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function getQueryApplicationClass() {
|
||||||
|
return 'PhabricatorHeraldApplication';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
95
src/applications/herald/query/HeraldWebhookSearchEngine.php
Normal file
95
src/applications/herald/query/HeraldWebhookSearchEngine.php
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookSearchEngine
|
||||||
|
extends PhabricatorApplicationSearchEngine {
|
||||||
|
|
||||||
|
public function getResultTypeDescription() {
|
||||||
|
return pht('Webhooks');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getApplicationClassName() {
|
||||||
|
return 'PhabricatorHeraldApplication';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newQuery() {
|
||||||
|
return new HeraldWebhookQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildQueryFromParameters(array $map) {
|
||||||
|
$query = $this->newQuery();
|
||||||
|
|
||||||
|
if ($map['statuses']) {
|
||||||
|
$query->withStatuses($map['statuses']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildCustomSearchFields() {
|
||||||
|
return array(
|
||||||
|
id(new PhabricatorSearchCheckboxesField())
|
||||||
|
->setKey('statuses')
|
||||||
|
->setLabel(pht('Status'))
|
||||||
|
->setDescription(
|
||||||
|
pht('Search for archived or active pastes.'))
|
||||||
|
->setOptions(HeraldWebhook::getStatusDisplayNameMap()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getURI($path) {
|
||||||
|
return '/herald/webhook/'.$path;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getBuiltinQueryNames() {
|
||||||
|
$names = array();
|
||||||
|
|
||||||
|
$names['active'] = pht('Active');
|
||||||
|
$names['all'] = pht('All');
|
||||||
|
|
||||||
|
return $names;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildSavedQueryFromBuiltin($query_key) {
|
||||||
|
$query = $this->newSavedQuery();
|
||||||
|
$query->setQueryKey($query_key);
|
||||||
|
|
||||||
|
switch ($query_key) {
|
||||||
|
case 'all':
|
||||||
|
return $query;
|
||||||
|
case 'active':
|
||||||
|
return $query->setParameter(
|
||||||
|
'statuses',
|
||||||
|
array(
|
||||||
|
HeraldWebhook::HOOKSTATUS_FIREHOSE,
|
||||||
|
HeraldWebhook::HOOKSTATUS_ENABLED,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::buildSavedQueryFromBuiltin($query_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function renderResultList(
|
||||||
|
array $hooks,
|
||||||
|
PhabricatorSavedQuery $query,
|
||||||
|
array $handles) {
|
||||||
|
assert_instances_of($hooks, 'HeraldWebhook');
|
||||||
|
|
||||||
|
$viewer = $this->requireViewer();
|
||||||
|
|
||||||
|
$list = id(new PHUIObjectItemListView())
|
||||||
|
->setViewer($viewer);
|
||||||
|
foreach ($hooks as $hook) {
|
||||||
|
$item = id(new PHUIObjectItemView())
|
||||||
|
->setObjectName(pht('Hook %d', $hook->getID()))
|
||||||
|
->setHeader($hook->getName())
|
||||||
|
->setHref($hook->getURI());
|
||||||
|
|
||||||
|
$list->addItem($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new PhabricatorApplicationSearchResultView())
|
||||||
|
->setObjectList($list)
|
||||||
|
->setNoDataString(pht('No webhooks found.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookTransactionQuery
|
||||||
|
extends PhabricatorApplicationTransactionQuery {
|
||||||
|
|
||||||
|
public function getTemplateApplicationTransaction() {
|
||||||
|
return new HeraldWebhookTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
177
src/applications/herald/storage/HeraldWebhook.php
Normal file
177
src/applications/herald/storage/HeraldWebhook.php
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhook
|
||||||
|
extends HeraldDAO
|
||||||
|
implements
|
||||||
|
PhabricatorPolicyInterface,
|
||||||
|
PhabricatorApplicationTransactionInterface,
|
||||||
|
PhabricatorDestructibleInterface {
|
||||||
|
|
||||||
|
protected $name;
|
||||||
|
protected $webhookURI;
|
||||||
|
protected $viewPolicy;
|
||||||
|
protected $editPolicy;
|
||||||
|
protected $status;
|
||||||
|
protected $hmacKey;
|
||||||
|
|
||||||
|
const HOOKSTATUS_FIREHOSE = 'firehose';
|
||||||
|
const HOOKSTATUS_ENABLED = 'enabled';
|
||||||
|
const HOOKSTATUS_DISABLED = 'disabled';
|
||||||
|
|
||||||
|
protected function getConfiguration() {
|
||||||
|
return array(
|
||||||
|
self::CONFIG_AUX_PHID => true,
|
||||||
|
self::CONFIG_COLUMN_SCHEMA => array(
|
||||||
|
'name' => 'text128',
|
||||||
|
'webhookURI' => 'text255',
|
||||||
|
'status' => 'text32',
|
||||||
|
'hmacKey' => 'text32',
|
||||||
|
),
|
||||||
|
self::CONFIG_KEY_SCHEMA => array(
|
||||||
|
'key_status' => array(
|
||||||
|
'columns' => array('status'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) + parent::getConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPHIDType() {
|
||||||
|
return HeraldWebhookPHIDType::TYPECONST;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function initializeNewWebhook(PhabricatorUser $viewer) {
|
||||||
|
return id(new self())
|
||||||
|
->setStatus(self::HOOKSTATUS_ENABLED)
|
||||||
|
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
|
||||||
|
->setEditPolicy($viewer->getPHID())
|
||||||
|
->setHmacKey(Filesystem::readRandomCharacters(32));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getURI() {
|
||||||
|
return '/herald/webhook/view/'.$this->getID().'/';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDisabled() {
|
||||||
|
return ($this->getStatus() === self::HOOKSTATUS_DISABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getStatusDisplayNameMap() {
|
||||||
|
return array(
|
||||||
|
self::HOOKSTATUS_FIREHOSE => pht('Firehose'),
|
||||||
|
self::HOOKSTATUS_ENABLED => pht('Enabled'),
|
||||||
|
self::HOOKSTATUS_DISABLED => pht('Disabled'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusDisplayName() {
|
||||||
|
$status = $this->getStatus();
|
||||||
|
return idx($this->getStatusDisplayNameMap(), $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getErrorBackoffWindow() {
|
||||||
|
return phutil_units('5 minutes in seconds');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getErrorBackoffThreshold() {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isInErrorBackoff(PhabricatorUser $viewer) {
|
||||||
|
$backoff_window = $this->getErrorBackoffWindow();
|
||||||
|
$backoff_threshold = $this->getErrorBackoffThreshold();
|
||||||
|
|
||||||
|
$now = PhabricatorTime::getNow();
|
||||||
|
|
||||||
|
$window_start = ($now - $backoff_window);
|
||||||
|
|
||||||
|
$requests = id(new HeraldWebhookRequestQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withWebhookPHIDs(array($this->getPHID()))
|
||||||
|
->withLastRequestEpochBetween($window_start, null)
|
||||||
|
->withLastRequestResults(
|
||||||
|
array(
|
||||||
|
HeraldWebhookRequest::RESULT_FAIL,
|
||||||
|
))
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
if (count($requests) >= $backoff_threshold) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
public function getApplicationTransactionEditor() {
|
||||||
|
return new HeraldWebhookEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getApplicationTransactionObject() {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getApplicationTransactionTemplate() {
|
||||||
|
return new HeraldWebhookTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function willRenderTimeline(
|
||||||
|
PhabricatorApplicationTransactionView $timeline,
|
||||||
|
AphrontRequest $request) {
|
||||||
|
return $timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( PhabricatorDestructibleInterface )----------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
public function destroyObjectPermanently(
|
||||||
|
PhabricatorDestructionEngine $engine) {
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$requests = id(new HeraldWebhookRequestQuery())
|
||||||
|
->setViewer($engine->getViewer())
|
||||||
|
->withWebhookPHIDs(array($this->getPHID()))
|
||||||
|
->setLimit(100)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
if (!$requests) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($requests as $request) {
|
||||||
|
$request->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
191
src/applications/herald/storage/HeraldWebhookRequest.php
Normal file
191
src/applications/herald/storage/HeraldWebhookRequest.php
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookRequest
|
||||||
|
extends HeraldDAO
|
||||||
|
implements
|
||||||
|
PhabricatorPolicyInterface,
|
||||||
|
PhabricatorExtendedPolicyInterface {
|
||||||
|
|
||||||
|
protected $webhookPHID;
|
||||||
|
protected $objectPHID;
|
||||||
|
protected $status;
|
||||||
|
protected $properties = array();
|
||||||
|
protected $lastRequestResult;
|
||||||
|
protected $lastRequestEpoch;
|
||||||
|
|
||||||
|
private $webhook = self::ATTACHABLE;
|
||||||
|
|
||||||
|
const RETRY_NEVER = 'never';
|
||||||
|
const RETRY_FOREVER = 'forever';
|
||||||
|
|
||||||
|
const STATUS_QUEUED = 'queued';
|
||||||
|
const STATUS_FAILED = 'failed';
|
||||||
|
const STATUS_SENT = 'sent';
|
||||||
|
|
||||||
|
const RESULT_NONE = 'none';
|
||||||
|
const RESULT_OKAY = 'okay';
|
||||||
|
const RESULT_FAIL = 'fail';
|
||||||
|
|
||||||
|
protected function getConfiguration() {
|
||||||
|
return array(
|
||||||
|
self::CONFIG_AUX_PHID => true,
|
||||||
|
self::CONFIG_SERIALIZATION => array(
|
||||||
|
'properties' => self::SERIALIZATION_JSON,
|
||||||
|
),
|
||||||
|
self::CONFIG_COLUMN_SCHEMA => array(
|
||||||
|
'status' => 'text32',
|
||||||
|
'lastRequestResult' => 'text32',
|
||||||
|
'lastRequestEpoch' => 'epoch',
|
||||||
|
),
|
||||||
|
self::CONFIG_KEY_SCHEMA => array(
|
||||||
|
'key_ratelimit' => array(
|
||||||
|
'columns' => array(
|
||||||
|
'webhookPHID',
|
||||||
|
'lastRequestResult',
|
||||||
|
'lastRequestEpoch',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'key_collect' => array(
|
||||||
|
'columns' => array('dateCreated'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) + parent::getConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPHIDType() {
|
||||||
|
return HeraldWebhookRequestPHIDType::TYPECONST;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function initializeNewWebhookRequest(HeraldWebhook $hook) {
|
||||||
|
return id(new self())
|
||||||
|
->setWebhookPHID($hook->getPHID())
|
||||||
|
->attachWebhook($hook)
|
||||||
|
->setStatus(self::STATUS_QUEUED)
|
||||||
|
->setRetryMode(self::RETRY_NEVER)
|
||||||
|
->setLastRequestResult(self::RESULT_NONE)
|
||||||
|
->setLastRequestEpoch(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWebhook() {
|
||||||
|
return $this->assertAttached($this->webhook);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attachWebhook(HeraldWebhook $hook) {
|
||||||
|
$this->webhook = $hook;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setProperty($key, $value) {
|
||||||
|
$this->properties[$key] = $value;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getProperty($key, $default = null) {
|
||||||
|
return idx($this->properties, $key, $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRetryMode($mode) {
|
||||||
|
return $this->setProperty('retry', $mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRetryMode() {
|
||||||
|
return $this->getProperty('retry');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setErrorType($error_type) {
|
||||||
|
return $this->setProperty('errorType', $error_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getErrorType() {
|
||||||
|
return $this->getProperty('errorType');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setErrorCode($error_code) {
|
||||||
|
return $this->setProperty('errorCode', $error_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getErrorCode() {
|
||||||
|
return $this->getProperty('errorCode');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTransactionPHIDs(array $phids) {
|
||||||
|
return $this->setProperty('transactionPHIDs', $phids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTransactionPHIDs() {
|
||||||
|
return $this->getProperty('transactionPHIDs', array());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function queueCall() {
|
||||||
|
PhabricatorWorker::scheduleTask(
|
||||||
|
'HeraldWebhookWorker',
|
||||||
|
array(
|
||||||
|
'webhookRequestPHID' => $this->getPHID(),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'objectPHID' => $this->getPHID(),
|
||||||
|
));
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newStatusIcon() {
|
||||||
|
switch ($this->getStatus()) {
|
||||||
|
case self::STATUS_QUEUED:
|
||||||
|
$icon = 'fa-refresh';
|
||||||
|
$color = 'blue';
|
||||||
|
$tooltip = pht('Queued');
|
||||||
|
break;
|
||||||
|
case self::STATUS_SENT:
|
||||||
|
$icon = 'fa-check';
|
||||||
|
$color = 'green';
|
||||||
|
$tooltip = pht('Sent');
|
||||||
|
break;
|
||||||
|
case self::STATUS_FAILED:
|
||||||
|
default:
|
||||||
|
$icon = 'fa-times';
|
||||||
|
$color = 'red';
|
||||||
|
$tooltip = pht('Failed');
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return id(new PHUIIconView())
|
||||||
|
->setIcon($icon, $color)
|
||||||
|
->setTooltip($tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
public function getCapabilities() {
|
||||||
|
return array(
|
||||||
|
PhabricatorPolicyCapability::CAN_VIEW,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPolicy($capability) {
|
||||||
|
switch ($capability) {
|
||||||
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
||||||
|
return PhabricatorPolicies::getMostOpenPolicy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
public function getExtendedPolicy($capability, PhabricatorUser $viewer) {
|
||||||
|
return array(
|
||||||
|
array($this->getWebhook(), PhabricatorPolicyCapability::CAN_VIEW),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
22
src/applications/herald/storage/HeraldWebhookTransaction.php
Normal file
22
src/applications/herald/storage/HeraldWebhookTransaction.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookTransaction
|
||||||
|
extends PhabricatorModularTransaction {
|
||||||
|
|
||||||
|
public function getApplicationName() {
|
||||||
|
return 'herald';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getApplicationTransactionType() {
|
||||||
|
return HeraldWebhookPHIDType::TYPECONST;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getApplicationTransactionCommentObject() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBaseTransactionClass() {
|
||||||
|
return 'HeraldWebhookTransactionType';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookRequestListView
|
||||||
|
extends AphrontView {
|
||||||
|
|
||||||
|
private $requests;
|
||||||
|
private $highlightID;
|
||||||
|
|
||||||
|
public function setRequests(array $requests) {
|
||||||
|
assert_instances_of($requests, 'HeraldWebhookRequest');
|
||||||
|
$this->requests = $requests;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setHighlightID($highlight_id) {
|
||||||
|
$this->highlightID = $highlight_id;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHighlightID() {
|
||||||
|
return $this->highlightID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render() {
|
||||||
|
$viewer = $this->getViewer();
|
||||||
|
$requests = $this->requests;
|
||||||
|
|
||||||
|
$handle_phids = array();
|
||||||
|
foreach ($requests as $request) {
|
||||||
|
$handle_phids[] = $request->getObjectPHID();
|
||||||
|
}
|
||||||
|
$handles = $viewer->loadHandles($handle_phids);
|
||||||
|
|
||||||
|
$highlight_id = $this->getHighlightID();
|
||||||
|
|
||||||
|
$rows = array();
|
||||||
|
$rowc = array();
|
||||||
|
foreach ($requests as $request) {
|
||||||
|
$icon = $request->newStatusIcon();
|
||||||
|
|
||||||
|
if ($highlight_id == $request->getID()) {
|
||||||
|
$rowc[] = 'highlighted';
|
||||||
|
} else {
|
||||||
|
$rowc[] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows[] = array(
|
||||||
|
$request->getID(),
|
||||||
|
$icon,
|
||||||
|
$handles[$request->getObjectPHID()]->renderLink(),
|
||||||
|
$request->getErrorType(),
|
||||||
|
$request->getErrorCode(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = id(new AphrontTableView($rows))
|
||||||
|
->setRowClasses($rowc)
|
||||||
|
->setHeaders(
|
||||||
|
array(
|
||||||
|
pht('ID'),
|
||||||
|
'',
|
||||||
|
pht('Object'),
|
||||||
|
pht('Type'),
|
||||||
|
pht('Code'),
|
||||||
|
))
|
||||||
|
->setColumnClasses(
|
||||||
|
array(
|
||||||
|
'n',
|
||||||
|
'',
|
||||||
|
'wide',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
));
|
||||||
|
|
||||||
|
return $table;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
229
src/applications/herald/worker/HeraldWebhookWorker.php
Normal file
229
src/applications/herald/worker/HeraldWebhookWorker.php
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookWorker
|
||||||
|
extends PhabricatorWorker {
|
||||||
|
|
||||||
|
protected function doWork() {
|
||||||
|
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||||
|
|
||||||
|
$data = $this->getTaskData();
|
||||||
|
$request_phid = idx($data, 'webhookRequestPHID');
|
||||||
|
|
||||||
|
$request = id(new HeraldWebhookRequestQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs(array($request_phid))
|
||||||
|
->executeOne();
|
||||||
|
if (!$request) {
|
||||||
|
throw new PhabricatorWorkerPermanentFailureException(
|
||||||
|
pht(
|
||||||
|
'Unable to load webhook request ("%s"). It may have been '.
|
||||||
|
'garbage collected.',
|
||||||
|
$request_phid));
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = $request->getStatus();
|
||||||
|
if ($status !== HeraldWebhookRequest::STATUS_QUEUED) {
|
||||||
|
throw new PhabricatorWorkerPermanentFailureException(
|
||||||
|
pht(
|
||||||
|
'Webhook request ("%s") is not in "%s" status (actual '.
|
||||||
|
'status is "%s"). Declining call to hook.',
|
||||||
|
$request_phid,
|
||||||
|
HeraldWebhookRequest::STATUS_QUEUED,
|
||||||
|
$status));
|
||||||
|
}
|
||||||
|
|
||||||
|
$hook = $request->getWebhook();
|
||||||
|
|
||||||
|
if ($hook->isDisabled()) {
|
||||||
|
$this->failRequest($request, 'hook', 'disabled');
|
||||||
|
throw new PhabricatorWorkerPermanentFailureException(
|
||||||
|
pht(
|
||||||
|
'Associated hook ("%s") for webhook request ("%s") is disabled.',
|
||||||
|
$hook->getPHID(),
|
||||||
|
$request_phid));
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = $hook->getWebhookURI();
|
||||||
|
try {
|
||||||
|
PhabricatorEnv::requireValidRemoteURIForFetch(
|
||||||
|
$uri,
|
||||||
|
array(
|
||||||
|
'http',
|
||||||
|
'https',
|
||||||
|
));
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$this->failRequest($request, 'hook', 'uri');
|
||||||
|
throw new PhabricatorWorkerPermanentFailureException(
|
||||||
|
pht(
|
||||||
|
'Associated hook ("%s") for webhook request ("%s") has invalid '.
|
||||||
|
'fetch URI: %s',
|
||||||
|
$hook->getPHID(),
|
||||||
|
$request_phid,
|
||||||
|
$ex->getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$object_phid = $request->getObjectPHID();
|
||||||
|
|
||||||
|
$object = id(new PhabricatorObjectQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withPHIDs(array($object_phid))
|
||||||
|
->executeOne();
|
||||||
|
if (!$object) {
|
||||||
|
$this->failRequest($request, 'hook', 'object');
|
||||||
|
throw new PhabricatorWorkerPermanentFailureException(
|
||||||
|
pht(
|
||||||
|
'Unable to load object ("%s") for webhook request ("%s").',
|
||||||
|
$object_phid,
|
||||||
|
$request_phid));
|
||||||
|
}
|
||||||
|
|
||||||
|
$xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(
|
||||||
|
$object);
|
||||||
|
$xaction_phids = $request->getTransactionPHIDs();
|
||||||
|
if ($xaction_phids) {
|
||||||
|
$xactions = $xaction_query
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withObjectPHIDs(array($object_phid))
|
||||||
|
->withPHIDs($xaction_phids)
|
||||||
|
->execute();
|
||||||
|
$xactions = mpull($xactions, null, 'getPHID');
|
||||||
|
} else {
|
||||||
|
$xactions = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// To prevent thundering herd issues for high volume webhooks (where
|
||||||
|
// a large number of workers might try to work through a request backlog
|
||||||
|
// simultaneously, before the error backoff can catch up), we never
|
||||||
|
// parallelize requests to a particular webhook.
|
||||||
|
|
||||||
|
$lock_key = 'webhook('.$hook->getPHID().')';
|
||||||
|
$lock = PhabricatorGlobalLock::newLock($lock_key);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lock->lock();
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
phlog($ex);
|
||||||
|
throw new PhabricatorWorkerYieldException(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
$caught = null;
|
||||||
|
try {
|
||||||
|
$this->callWebhookWithLock($hook, $request, $object, $xactions);
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$caught = $ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lock->unlock();
|
||||||
|
|
||||||
|
if ($caught) {
|
||||||
|
throw $caught;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function callWebhookWithLock(
|
||||||
|
HeraldWebhook $hook,
|
||||||
|
HeraldWebhookRequest $request,
|
||||||
|
$object,
|
||||||
|
array $xactions) {
|
||||||
|
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||||
|
|
||||||
|
if ($hook->isInErrorBackoff($viewer)) {
|
||||||
|
throw new PhabricatorWorkerYieldException($hook->getErrorBackoffWindow());
|
||||||
|
}
|
||||||
|
|
||||||
|
$xaction_data = array();
|
||||||
|
foreach ($xactions as $xaction) {
|
||||||
|
$xaction_data[] = array(
|
||||||
|
'phid' => $xaction->getPHID(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = array(
|
||||||
|
'triggers' => array(),
|
||||||
|
'object' => array(
|
||||||
|
'phid' => $object->getPHID(),
|
||||||
|
),
|
||||||
|
'transactions' => $xaction_data,
|
||||||
|
);
|
||||||
|
|
||||||
|
$payload = phutil_json_encode($payload);
|
||||||
|
$key = $hook->getHmacKey();
|
||||||
|
$signature = PhabricatorHash::digestHMACSHA256($payload, $key);
|
||||||
|
$uri = $hook->getWebhookURI();
|
||||||
|
|
||||||
|
$future = id(new HTTPSFuture($uri))
|
||||||
|
->setMethod('POST')
|
||||||
|
->addHeader('Content-Type', 'application/json')
|
||||||
|
->addHeader('X-Phabricator-Webhook-Signature', $signature)
|
||||||
|
->setTimeout(15)
|
||||||
|
->setData($payload);
|
||||||
|
|
||||||
|
list($status) = $future->resolve();
|
||||||
|
|
||||||
|
if ($status->isTimeout()) {
|
||||||
|
$error_type = 'timeout';
|
||||||
|
} else {
|
||||||
|
$error_type = 'http';
|
||||||
|
}
|
||||||
|
$error_code = $status->getStatusCode();
|
||||||
|
|
||||||
|
$request
|
||||||
|
->setErrorType($error_type)
|
||||||
|
->setErrorCode($error_code)
|
||||||
|
->setLastRequestEpoch(PhabricatorTime::getNow());
|
||||||
|
|
||||||
|
$retry_forever = HeraldWebhookRequest::RETRY_FOREVER;
|
||||||
|
if ($status->isTimeout() || $status->isError()) {
|
||||||
|
$should_retry = ($request->getRetryMode() === $retry_forever);
|
||||||
|
|
||||||
|
$request
|
||||||
|
->setLastRequestResult(HeraldWebhookRequest::RESULT_FAIL);
|
||||||
|
|
||||||
|
if ($should_retry) {
|
||||||
|
$request->save();
|
||||||
|
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
|
||||||
|
'will be retried.',
|
||||||
|
$request->getPHID(),
|
||||||
|
$uri,
|
||||||
|
$error_type,
|
||||||
|
$error_code));
|
||||||
|
} else {
|
||||||
|
$request
|
||||||
|
->setStatus(HeraldWebhookRequest::STATUS_FAILED)
|
||||||
|
->save();
|
||||||
|
|
||||||
|
throw new PhabricatorWorkerPermanentFailureException(
|
||||||
|
pht(
|
||||||
|
'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
|
||||||
|
'will not be retried.',
|
||||||
|
$request->getPHID(),
|
||||||
|
$uri,
|
||||||
|
$error_type,
|
||||||
|
$error_code));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$request
|
||||||
|
->setLastRequestResult(HeraldWebhookRequest::RESULT_OKAY)
|
||||||
|
->setStatus(HeraldWebhookRequest::STATUS_SENT)
|
||||||
|
->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function failRequest(
|
||||||
|
HeraldWebhookRequest $request,
|
||||||
|
$error_type,
|
||||||
|
$error_code) {
|
||||||
|
|
||||||
|
$request
|
||||||
|
->setStatus(HeraldWebhookRequest::STATUS_FAILED)
|
||||||
|
->setErrorType($error_type)
|
||||||
|
->setErrorCode($error_code)
|
||||||
|
->setLastRequestResult(HeraldWebhookRequest::RESULT_NONE)
|
||||||
|
->setLastRequestEpoch(0)
|
||||||
|
->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookNameTransaction
|
||||||
|
extends HeraldWebhookTransactionType {
|
||||||
|
|
||||||
|
const TRANSACTIONTYPE = 'name';
|
||||||
|
|
||||||
|
public function generateOldValue($object) {
|
||||||
|
return $object->getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyInternalEffects($object, $value) {
|
||||||
|
$object->setName($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle() {
|
||||||
|
return pht(
|
||||||
|
'%s renamed this webhook from %s to %s.',
|
||||||
|
$this->renderAuthor(),
|
||||||
|
$this->renderOldValue(),
|
||||||
|
$this->renderNewValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitleForFeed() {
|
||||||
|
return pht(
|
||||||
|
'%s renamed %s from %s to %s.',
|
||||||
|
$this->renderAuthor(),
|
||||||
|
$this->renderObject(),
|
||||||
|
$this->renderOldValue(),
|
||||||
|
$this->renderNewValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateTransactions($object, array $xactions) {
|
||||||
|
$errors = array();
|
||||||
|
$viewer = $this->getActor();
|
||||||
|
|
||||||
|
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
|
||||||
|
$errors[] = $this->newRequiredError(
|
||||||
|
pht('Webhooks must have a name.'));
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$max_length = $object->getColumnMaximumByteLength('name');
|
||||||
|
foreach ($xactions as $xaction) {
|
||||||
|
$old_value = $this->generateOldValue($object);
|
||||||
|
$new_value = $xaction->getNewValue();
|
||||||
|
|
||||||
|
$new_length = strlen($new_value);
|
||||||
|
if ($new_length > $max_length) {
|
||||||
|
$errors[] = $this->newInvalidError(
|
||||||
|
pht(
|
||||||
|
'Webhook names can be no longer than %s characters.',
|
||||||
|
new PhutilNumber($max_length)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookStatusTransaction
|
||||||
|
extends HeraldWebhookTransactionType {
|
||||||
|
|
||||||
|
const TRANSACTIONTYPE = 'status';
|
||||||
|
|
||||||
|
public function generateOldValue($object) {
|
||||||
|
return $object->getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyInternalEffects($object, $value) {
|
||||||
|
$object->setStatus($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle() {
|
||||||
|
return pht(
|
||||||
|
'%s changed hook status from %s to %s.',
|
||||||
|
$this->renderAuthor(),
|
||||||
|
$this->renderOldValue(),
|
||||||
|
$this->renderNewValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitleForFeed() {
|
||||||
|
return pht(
|
||||||
|
'%s changed %s from %s to %s.',
|
||||||
|
$this->renderAuthor(),
|
||||||
|
$this->renderObject(),
|
||||||
|
$this->renderOldValue(),
|
||||||
|
$this->renderNewValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateTransactions($object, array $xactions) {
|
||||||
|
$errors = array();
|
||||||
|
$viewer = $this->getActor();
|
||||||
|
|
||||||
|
$options = HeraldWebhook::getStatusDisplayNameMap();
|
||||||
|
|
||||||
|
foreach ($xactions as $xaction) {
|
||||||
|
$new_value = $xaction->getNewValue();
|
||||||
|
|
||||||
|
if (!isset($options[$new_value])) {
|
||||||
|
$errors[] = $this->newInvalidError(
|
||||||
|
pht(
|
||||||
|
'Webhook status "%s" is not valid. Valid statuses are: %s.',
|
||||||
|
$new_value,
|
||||||
|
implode(', ', array_keys($options))),
|
||||||
|
$xaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class HeraldWebhookTransactionType
|
||||||
|
extends PhabricatorModularTransactionType {}
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class HeraldWebhookURITransaction
|
||||||
|
extends HeraldWebhookTransactionType {
|
||||||
|
|
||||||
|
const TRANSACTIONTYPE = 'uri';
|
||||||
|
|
||||||
|
public function generateOldValue($object) {
|
||||||
|
return $object->getWebhookURI();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyInternalEffects($object, $value) {
|
||||||
|
$object->setWebhookURI($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle() {
|
||||||
|
return pht(
|
||||||
|
'%s changed the URI for this webhook from %s to %s.',
|
||||||
|
$this->renderAuthor(),
|
||||||
|
$this->renderOldValue(),
|
||||||
|
$this->renderNewValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitleForFeed() {
|
||||||
|
return pht(
|
||||||
|
'%s changed the URI for %s from %s to %s.',
|
||||||
|
$this->renderAuthor(),
|
||||||
|
$this->renderObject(),
|
||||||
|
$this->renderOldValue(),
|
||||||
|
$this->renderNewValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateTransactions($object, array $xactions) {
|
||||||
|
$errors = array();
|
||||||
|
$viewer = $this->getActor();
|
||||||
|
|
||||||
|
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
|
||||||
|
$errors[] = $this->newRequiredError(
|
||||||
|
pht('Webhooks must have a URI.'));
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
$max_length = $object->getColumnMaximumByteLength('webhookURI');
|
||||||
|
foreach ($xactions as $xaction) {
|
||||||
|
$old_value = $this->generateOldValue($object);
|
||||||
|
$new_value = $xaction->getNewValue();
|
||||||
|
|
||||||
|
$new_length = strlen($new_value);
|
||||||
|
if ($new_length > $max_length) {
|
||||||
|
$errors[] = $this->newInvalidError(
|
||||||
|
pht(
|
||||||
|
'Webhook URIs can be no longer than %s characters.',
|
||||||
|
new PhutilNumber($max_length)),
|
||||||
|
$xaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PhabricatorEnv::requireValidRemoteURIForFetch(
|
||||||
|
$new_value,
|
||||||
|
array(
|
||||||
|
'http',
|
||||||
|
'https',
|
||||||
|
));
|
||||||
|
} catch (Exception $ex) {
|
||||||
|
$errors[] = $this->newInvalidError(
|
||||||
|
$ex->getMessage(),
|
||||||
|
$xaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue