Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/herald/controller/HeraldWebhookViewController.php b/src/applications/herald/controller/HeraldWebhookViewController.php
index 9b11f5d433..d8e5eb3c54 100644
--- a/src/applications/herald/controller/HeraldWebhookViewController.php
+++ b/src/applications/herald/controller/HeraldWebhookViewController.php
@@ -1,184 +1,197 @@
<?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();
+ $warnings = array();
+ if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
+ $message = pht(
+ 'Phabricator is currently configured in silent mode, so it will not '.
+ 'publish webhooks. To adjust this setting, see '.
+ '@{config:phabricator.silent} in Config.');
+
+ $warnings[] = id(new PHUIInfoView())
+ ->setTitle(pht('Silent Mode'))
+ ->setSeverity(PHUIInfoView::SEVERITY_WARNING)
+ ->appendChild(new PHUIRemarkupView($viewer, $message));
+ }
+
$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();
$status_icon = $hook->getStatusIcon();
$status_color = $hook->getStatusColor();
$status_name = $hook->getStatusDisplayName();
$header = id(new PHUIHeaderView())
->setHeader($title)
->setViewer($viewer)
->setPolicyObject($hook)
->setStatus($status_icon, $status_color, $status_name)
->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}/");
$key_view_uri = $this->getApplicationURI("webhook/key/view/{$id}/");
$key_cycle_uri = $this->getApplicationURI("webhook/key/cycle/{$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));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View HMAC Key'))
->setIcon('fa-key')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_view_uri));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Regenerate HMAC Key'))
->setIcon('fa-refresh')
->setDisabled(!$can_edit)
->setWorkflow(true)
->setHref($key_cycle_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);
}
}
diff --git a/src/applications/herald/storage/HeraldWebhookRequest.php b/src/applications/herald/storage/HeraldWebhookRequest.php
index 5db5b2916e..3381f6a99c 100644
--- a/src/applications/herald/storage/HeraldWebhookRequest.php
+++ b/src/applications/herald/storage/HeraldWebhookRequest.php
@@ -1,223 +1,276 @@
<?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';
+ const ERRORTYPE_HOOK = 'hook';
+ const ERRORTYPE_HTTP = 'http';
+ const ERRORTYPE_TIMEOUT = 'timeout';
+
+ const ERROR_SILENT = 'silent';
+ const ERROR_DISABLED = 'disabled';
+ const ERROR_URI = 'uri';
+ const ERROR_OBJECT = 'object';
+
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 getErrorTypeForDisplay() {
+ $map = array(
+ self::ERRORTYPE_HOOK => pht('Hook Error'),
+ self::ERRORTYPE_HTTP => pht('HTTP Error'),
+ self::ERRORTYPE_TIMEOUT => pht('Request Timeout'),
+ );
+
+ $type = $this->getErrorType();
+ return idx($map, $type, $type);
+ }
+
+ public function getErrorCodeForDisplay() {
+ $code = $this->getErrorCode();
+
+ if ($this->getErrorType() !== self::ERRORTYPE_HOOK) {
+ return $code;
+ }
+
+ $spec = $this->getHookErrorSpec($code);
+ return idx($spec, 'display', $code);
+ }
+
public function setTransactionPHIDs(array $phids) {
return $this->setProperty('transactionPHIDs', $phids);
}
public function getTransactionPHIDs() {
return $this->getProperty('transactionPHIDs', array());
}
public function setTriggerPHIDs(array $phids) {
return $this->setProperty('triggerPHIDs', $phids);
}
public function getTriggerPHIDs() {
return $this->getProperty('triggerPHIDs', array());
}
public function setIsSilentAction($bool) {
return $this->setProperty('silent', $bool);
}
public function getIsSilentAction() {
return $this->getProperty('silent', false);
}
public function setIsTestAction($bool) {
return $this->setProperty('test', $bool);
}
public function getIsTestAction() {
return $this->getProperty('test', false);
}
public function setIsSecureAction($bool) {
return $this->setProperty('secure', $bool);
}
public function getIsSecureAction() {
return $this->getProperty('secure', false);
}
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);
}
+ private function getHookErrorSpec($code) {
+ $map = $this->getHookErrorMap();
+ return idx($map, $code, array());
+ }
+
+ private function getHookErrorMap() {
+ return array(
+ self::ERROR_SILENT => array(
+ 'display' => pht('In Silent Mode'),
+ ),
+ self::ERROR_DISABLED => array(
+ 'display' => pht('Hook Disabled'),
+ ),
+ self::ERROR_URI => array(
+ 'display' => pht('Invalid URI'),
+ ),
+ self::ERROR_OBJECT => array(
+ 'display' => pht('Invalid Object'),
+ ),
+ );
+ }
+
/* -( 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),
);
}
}
diff --git a/src/applications/herald/view/HeraldWebhookRequestListView.php b/src/applications/herald/view/HeraldWebhookRequestListView.php
index 4e0f6510b9..082d320bba 100644
--- a/src/applications/herald/view/HeraldWebhookRequestListView.php
+++ b/src/applications/herald/view/HeraldWebhookRequestListView.php
@@ -1,88 +1,88 @@
<?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;
}
$last_epoch = $request->getLastRequestEpoch();
if ($request->getLastRequestEpoch()) {
$last_request = phabricator_datetime($last_epoch, $viewer);
} else {
$last_request = null;
}
$rows[] = array(
$request->getID(),
$icon,
$handles[$request->getObjectPHID()]->renderLink(),
- $request->getErrorType(),
- $request->getErrorCode(),
+ $request->getErrorTypeForDisplay(),
+ $request->getErrorCodeForDisplay(),
$last_request,
);
}
$table = id(new AphrontTableView($rows))
->setRowClasses($rowc)
->setHeaders(
array(
pht('ID'),
- '',
+ null,
pht('Object'),
pht('Type'),
pht('Code'),
pht('Requested At'),
))
->setColumnClasses(
array(
'n',
'',
'wide',
'',
'',
'',
));
return $table;
}
}
diff --git a/src/applications/herald/worker/HeraldWebhookWorker.php b/src/applications/herald/worker/HeraldWebhookWorker.php
index 150f98fd50..bc93f092d5 100644
--- a/src/applications/herald/worker/HeraldWebhookWorker.php
+++ b/src/applications/herald/worker/HeraldWebhookWorker.php
@@ -1,250 +1,263 @@
<?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));
}
// If we're in silent mode, permanently fail the webhook request and then
// return to complete this task.
if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
- $this->failRequest($request, 'hook', 'silent');
+ $this->failRequest(
+ $request,
+ HeraldWebhookRequest::ERRORTYPE_HOOK,
+ HeraldWebhookRequest::ERROR_SILENT);
return;
}
$hook = $request->getWebhook();
if ($hook->isDisabled()) {
- $this->failRequest($request, 'hook', 'disabled');
+ $this->failRequest(
+ $request,
+ HeraldWebhookRequest::ERRORTYPE_HOOK,
+ HeraldWebhookRequest::ERROR_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');
+ $this->failRequest(
+ $request,
+ HeraldWebhookRequest::ERRORTYPE_HOOK,
+ HeraldWebhookRequest::ERROR_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');
+ $this->failRequest(
+ $request,
+ HeraldWebhookRequest::ERRORTYPE_HOOK,
+ HeraldWebhookRequest::ERROR_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(),
);
}
$trigger_data = array();
foreach ($request->getTriggerPHIDs() as $trigger_phid) {
$trigger_data[] = array(
'phid' => $trigger_phid,
);
}
$payload = array(
'object' => array(
'type' => phid_get_type($object->getPHID()),
'phid' => $object->getPHID(),
),
'triggers' => $trigger_data,
'action' => array(
'test' => $request->getIsTestAction(),
'silent' => $request->getIsSilentAction(),
'secure' => $request->getIsSecureAction(),
'epoch' => (int)$request->getDateCreated(),
),
'transactions' => $xaction_data,
);
$payload = id(new PhutilJSON())->encodeFormatted($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';
+ $error_type = HeraldWebhookRequest::ERRORTYPE_TIMEOUT;
} else {
- $error_type = 'http';
+ $error_type = HeraldWebhookRequest::ERRORTYPE_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();
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Dec 2, 12:54 PM (18 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
431943
Default Alt Text
(23 KB)

Event Timeline