Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / herald / worker / HeraldWebhookWorker.php
blobbc93f092d56971875d25e96a377459c2d6a6e66e
1 <?php
3 final class HeraldWebhookWorker
4 extends PhabricatorWorker {
6 protected function doWork() {
7 $viewer = PhabricatorUser::getOmnipotentUser();
9 $data = $this->getTaskData();
10 $request_phid = idx($data, 'webhookRequestPHID');
12 $request = id(new HeraldWebhookRequestQuery())
13 ->setViewer($viewer)
14 ->withPHIDs(array($request_phid))
15 ->executeOne();
16 if (!$request) {
17 throw new PhabricatorWorkerPermanentFailureException(
18 pht(
19 'Unable to load webhook request ("%s"). It may have been '.
20 'garbage collected.',
21 $request_phid));
24 $status = $request->getStatus();
25 if ($status !== HeraldWebhookRequest::STATUS_QUEUED) {
26 throw new PhabricatorWorkerPermanentFailureException(
27 pht(
28 'Webhook request ("%s") is not in "%s" status (actual '.
29 'status is "%s"). Declining call to hook.',
30 $request_phid,
31 HeraldWebhookRequest::STATUS_QUEUED,
32 $status));
35 // If we're in silent mode, permanently fail the webhook request and then
36 // return to complete this task.
37 if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
38 $this->failRequest(
39 $request,
40 HeraldWebhookRequest::ERRORTYPE_HOOK,
41 HeraldWebhookRequest::ERROR_SILENT);
42 return;
45 $hook = $request->getWebhook();
47 if ($hook->isDisabled()) {
48 $this->failRequest(
49 $request,
50 HeraldWebhookRequest::ERRORTYPE_HOOK,
51 HeraldWebhookRequest::ERROR_DISABLED);
52 throw new PhabricatorWorkerPermanentFailureException(
53 pht(
54 'Associated hook ("%s") for webhook request ("%s") is disabled.',
55 $hook->getPHID(),
56 $request_phid));
59 $uri = $hook->getWebhookURI();
60 try {
61 PhabricatorEnv::requireValidRemoteURIForFetch(
62 $uri,
63 array(
64 'http',
65 'https',
66 ));
67 } catch (Exception $ex) {
68 $this->failRequest(
69 $request,
70 HeraldWebhookRequest::ERRORTYPE_HOOK,
71 HeraldWebhookRequest::ERROR_URI);
72 throw new PhabricatorWorkerPermanentFailureException(
73 pht(
74 'Associated hook ("%s") for webhook request ("%s") has invalid '.
75 'fetch URI: %s',
76 $hook->getPHID(),
77 $request_phid,
78 $ex->getMessage()));
81 $object_phid = $request->getObjectPHID();
83 $object = id(new PhabricatorObjectQuery())
84 ->setViewer($viewer)
85 ->withPHIDs(array($object_phid))
86 ->executeOne();
87 if (!$object) {
88 $this->failRequest(
89 $request,
90 HeraldWebhookRequest::ERRORTYPE_HOOK,
91 HeraldWebhookRequest::ERROR_OBJECT);
93 throw new PhabricatorWorkerPermanentFailureException(
94 pht(
95 'Unable to load object ("%s") for webhook request ("%s").',
96 $object_phid,
97 $request_phid));
100 $xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(
101 $object);
102 $xaction_phids = $request->getTransactionPHIDs();
103 if ($xaction_phids) {
104 $xactions = $xaction_query
105 ->setViewer($viewer)
106 ->withObjectPHIDs(array($object_phid))
107 ->withPHIDs($xaction_phids)
108 ->execute();
109 $xactions = mpull($xactions, null, 'getPHID');
110 } else {
111 $xactions = array();
114 // To prevent thundering herd issues for high volume webhooks (where
115 // a large number of workers might try to work through a request backlog
116 // simultaneously, before the error backoff can catch up), we never
117 // parallelize requests to a particular webhook.
119 $lock_key = 'webhook('.$hook->getPHID().')';
120 $lock = PhabricatorGlobalLock::newLock($lock_key);
122 try {
123 $lock->lock();
124 } catch (Exception $ex) {
125 phlog($ex);
126 throw new PhabricatorWorkerYieldException(15);
129 $caught = null;
130 try {
131 $this->callWebhookWithLock($hook, $request, $object, $xactions);
132 } catch (Exception $ex) {
133 $caught = $ex;
136 $lock->unlock();
138 if ($caught) {
139 throw $caught;
143 private function callWebhookWithLock(
144 HeraldWebhook $hook,
145 HeraldWebhookRequest $request,
146 $object,
147 array $xactions) {
148 $viewer = PhabricatorUser::getOmnipotentUser();
150 if ($hook->isInErrorBackoff($viewer)) {
151 throw new PhabricatorWorkerYieldException($hook->getErrorBackoffWindow());
154 $xaction_data = array();
155 foreach ($xactions as $xaction) {
156 $xaction_data[] = array(
157 'phid' => $xaction->getPHID(),
161 $trigger_data = array();
162 foreach ($request->getTriggerPHIDs() as $trigger_phid) {
163 $trigger_data[] = array(
164 'phid' => $trigger_phid,
168 $payload = array(
169 'object' => array(
170 'type' => phid_get_type($object->getPHID()),
171 'phid' => $object->getPHID(),
173 'triggers' => $trigger_data,
174 'action' => array(
175 'test' => $request->getIsTestAction(),
176 'silent' => $request->getIsSilentAction(),
177 'secure' => $request->getIsSecureAction(),
178 'epoch' => (int)$request->getDateCreated(),
180 'transactions' => $xaction_data,
183 $payload = id(new PhutilJSON())->encodeFormatted($payload);
184 $key = $hook->getHmacKey();
185 $signature = PhabricatorHash::digestHMACSHA256($payload, $key);
186 $uri = $hook->getWebhookURI();
188 $future = id(new HTTPSFuture($uri))
189 ->setMethod('POST')
190 ->addHeader('Content-Type', 'application/json')
191 ->addHeader('X-Phabricator-Webhook-Signature', $signature)
192 ->setTimeout(15)
193 ->setData($payload);
195 list($status) = $future->resolve();
197 if ($status->isTimeout()) {
198 $error_type = HeraldWebhookRequest::ERRORTYPE_TIMEOUT;
199 } else {
200 $error_type = HeraldWebhookRequest::ERRORTYPE_HTTP;
202 $error_code = $status->getStatusCode();
204 $request
205 ->setErrorType($error_type)
206 ->setErrorCode($error_code)
207 ->setLastRequestEpoch(PhabricatorTime::getNow());
209 $retry_forever = HeraldWebhookRequest::RETRY_FOREVER;
210 if ($status->isTimeout() || $status->isError()) {
211 $should_retry = ($request->getRetryMode() === $retry_forever);
213 $request
214 ->setLastRequestResult(HeraldWebhookRequest::RESULT_FAIL);
216 if ($should_retry) {
217 $request->save();
219 throw new Exception(
220 pht(
221 'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
222 'will be retried.',
223 $request->getPHID(),
224 $uri,
225 $error_type,
226 $error_code));
227 } else {
228 $request
229 ->setStatus(HeraldWebhookRequest::STATUS_FAILED)
230 ->save();
232 throw new PhabricatorWorkerPermanentFailureException(
233 pht(
234 'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
235 'will not be retried.',
236 $request->getPHID(),
237 $uri,
238 $error_type,
239 $error_code));
241 } else {
242 $request
243 ->setLastRequestResult(HeraldWebhookRequest::RESULT_OKAY)
244 ->setStatus(HeraldWebhookRequest::STATUS_SENT)
245 ->save();
249 private function failRequest(
250 HeraldWebhookRequest $request,
251 $error_type,
252 $error_code) {
254 $request
255 ->setStatus(HeraldWebhookRequest::STATUS_FAILED)
256 ->setErrorType($error_type)
257 ->setErrorCode($error_code)
258 ->setLastRequestResult(HeraldWebhookRequest::RESULT_NONE)
259 ->setLastRequestEpoch(0)
260 ->save();