Differential D11329 Diff 27604 src/applications/transactions/worker/PhabricatorApplicationTransactionNotificationWorker.php
Changeset View
Changeset View
Standalone View
Standalone View
src/applications/transactions/worker/PhabricatorApplicationTransactionNotificationWorker.php
- This file was added.
<?php | |||||
abstract class PhabricatorApplicationTransactionNotificationWorker | |||||
extends PhabricatorWorker { | |||||
private $shouldPublishFeedStory; | |||||
private $shouldSendMail; | |||||
private $actingAsPHID; | |||||
private $mailTo; | |||||
private $mailCC; | |||||
private $isNewObject; | |||||
private $heraldHeader; | |||||
private $excludeMailRecipientPHIDs; | |||||
private $parentMessageID; | |||||
private $users; | |||||
abstract protected function loadObject($phid); | |||||
abstract protected function loadXActions($xactions); | |||||
abstract protected function buildReplyHandler(PhabricatorLiskDAO $object); | |||||
public function getMaximumRetryCount() { | |||||
return 250; | |||||
} | |||||
public function getWaitBeforeRetry(PhabricatorWorkerTask $task) { | |||||
return ($task->getFailureCount() * 15); | |||||
} | |||||
protected function getActingAsPHID() { | |||||
return $this->actingAsPHID; | |||||
} | |||||
protected function getIsNewObject() { | |||||
return $this->isNewObject; | |||||
} | |||||
protected function expandPHIDs($phids) { | |||||
// Expand recipients (get projects members) | |||||
$map = id(new PhabricatorMetaMTAMemberQuery()) | |||||
->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
->withPHIDs($phids) | |||||
->execute(); | |||||
$results = array(); | |||||
foreach ($phids as $phid) { | |||||
if (isset($map[$phid])) { | |||||
foreach ($map[$phid] as $expanded_phid) { | |||||
$results[$expanded_phid] = $expanded_phid; | |||||
} | |||||
} else { | |||||
$results[$phid] = $phid; | |||||
} | |||||
} | |||||
return $results; | |||||
} | |||||
protected function loadUsers(array $phids) { | |||||
$users = id(new PhabricatorPeopleQuery()) | |||||
->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
->withPHIDs($phids) | |||||
->execute(); | |||||
$this->users = mpull($users, null, 'getPHID'); | |||||
} | |||||
protected function filterUsersWithoutPermission($object) { | |||||
foreach ($this->users as $key => $user) { | |||||
// Check if the receiving user has permissions to view this object | |||||
if ($object | |||||
&& $object instanceof PhabricatorPolicyInterface | |||||
&& $user | |||||
&& $user instanceof PhabricatorUser) { | |||||
if (!PhabricatorPolicyFilter::hasCapability( | |||||
$user, | |||||
$object, | |||||
PhabricatorPolicyCapability::CAN_VIEW)) { | |||||
// User has no permission to this object | |||||
// so remove them from all lists | |||||
unset($this->users[$key]); | |||||
$phid = array($user->getPHID()); | |||||
$this->mailTo = array_diff($this->mailTo, $phid); | |||||
$this->mailTo = array_diff($this->mailTo, $phid); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
protected function getUser($phid) { | |||||
return idx($this->users, $phid, null); | |||||
} | |||||
public function doWork() { | |||||
$task_data = $this->getTaskData(); | |||||
$this->shouldSendMail = | |||||
idx($task_data, 'shouldSendMail', false); | |||||
$this->shouldPublishFeedStory = | |||||
idx($task_data, 'shouldPublishFeedStory', false); | |||||
if (!idx($task_data, 'actingAsPHID')) { | |||||
throw new PhabricatorWorkerPermanentFailureException( | |||||
pht('Missing actingAsPHID in Tasks Data')); | |||||
} | |||||
$this->actingAsPHID = $task_data['actingAsPHID']; | |||||
if (!idx($task_data, 'mailTo')) { | |||||
throw new PhabricatorWorkerPermanentFailureException( | |||||
pht('Missing mailTo in Tasks Data')); | |||||
} | |||||
$this->mailTo = $task_data['mailTo']; | |||||
if (!idx($task_data, 'mailCC')) { | |||||
throw new PhabricatorWorkerPermanentFailureException( | |||||
pht('Missing mailCC in Tasks Data')); | |||||
} | |||||
$this->mailCC = $task_data['mailCC']; | |||||
$this->heraldHeader = | |||||
idx($task_data, 'heraldHeader', null); | |||||
$this->isNewObject = | |||||
idx($task_data, 'isNewObject', false); | |||||
$this->excludeMailRecipientPHIDs = | |||||
idx($task_data, 'excludeMailRecipientPHIDs'); | |||||
$this->parentMessageID = | |||||
idx($task_data, 'parentMessageID'); | |||||
// Load object and xactions | |||||
$object = $this->loadObject($task_data['object']); | |||||
if (!$object) { | |||||
throw new PhabricatorWorkerPermanentFailureException( | |||||
pht('Unable to load object!')); | |||||
} | |||||
$xactions = $this->loadXActions($task_data['xactions']); | |||||
assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); | |||||
// Filter non-visible xactions | |||||
$feed_xactions = mfilter($xactions, 'shouldHideForFeed', true); | |||||
$mail_xactions = array(); | |||||
foreach ($xactions as $xaction) { | |||||
if (!$xaction->shouldHideForMail($xactions)) { | |||||
$mail_xactions[] = $xaction; | |||||
} | |||||
} | |||||
// Expand TO & CC (Turn Project PHIDs into its Member PHIDs) | |||||
$this->mailTo = $this->expandPHIDs( | |||||
array_filter(array_unique($this->mailTo))); | |||||
$this->mailCC = $this->expandPHIDs( | |||||
array_filter(array_unique($this->mailCC))); | |||||
// Load corresponding User Objects | |||||
$this->loadUsers(array_merge($this->mailTo, $this->mailCC)); | |||||
// Filter Users without permission to the object notified about | |||||
$this->filterUsersWithoutPermission($object); | |||||
// Send Mail | |||||
$mailed_phids = array(); | |||||
if ($mail_xactions) { | |||||
$mailed_phids = $this->sendMail($object, $mail_xactions); | |||||
} | |||||
// Publish Feed Stories | |||||
if ($feed_xactions) { | |||||
$this->publishFeedStory($object, $feed_xactions, $mailed_phids); | |||||
} | |||||
return true; | |||||
} | |||||
protected function sendMail($object, $xactions) { | |||||
$email_to = $this->getMailTo($object); | |||||
$email_cc = $this->getMailCC($object); | |||||
$recipients = array_merge($email_to, $email_cc); | |||||
$handles = id(new PhabricatorHandleQuery()) | |||||
->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
->withPHIDs($recipients) | |||||
->execute(); | |||||
$to_handles = array_select_keys($handles, $email_to); | |||||
$cc_handles = array_select_keys($handles, $email_cc); | |||||
$emails = array(); | |||||
$send_as_one_email = false; | |||||
// Render the email for each user individually | |||||
// so correct permissions are preserved | |||||
foreach ($recipients as $recipient) { | |||||
$viewer = $this->getUser($recipient); | |||||
$template = $this->buildMailTemplate($object); | |||||
$template->addPHIDHeaders('X-Phabricator-To', array_keys($email_to)); | |||||
$template->addPHIDHeaders('X-Phabricator-Cc', array_keys($email_cc)); | |||||
if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) { | |||||
// If config is set to not multiplex we render as the triggering user | |||||
$viewer = $this->getUser($this->getActingAsPHID()); | |||||
$template->addTos($email_to); | |||||
$template->addCCs($email_cc); | |||||
$send_as_one_email = true; | |||||
} else { | |||||
$template->addTos(array($recipient)); | |||||
} | |||||
// This stuff doesen't change per user. We still handle | |||||
// it here to support sending in the users locale in the future. | |||||
$mail_tags = $this->getMailTags($object, $xactions); | |||||
$action = $this->getMailAction($object, $xactions); | |||||
$reply_handler = $this->buildReplyHandler($object); | |||||
$reply_section = $reply_handler->getReplyHandlerInstructions(); | |||||
// Render the actual mail body | |||||
$body = $this->buildMailBody($object, $xactions, $viewer); | |||||
if ($reply_section !== null) { | |||||
$body->addReplySection($reply_section); | |||||
} | |||||
$body->addEmailPreferenceSection(); | |||||
// FIXME?: This changes the emails a bit | |||||
// (adds a RECIPIENTS header at the bottom) | |||||
// Before this those were added by raw concatenation to the body | |||||
$body->addPlainTextSection('RECIPIENTS', | |||||
$reply_handler->getRecipientsSummary($to_handles, $cc_handles)); | |||||
$body->addHTMLSection('RECIPIENTS', | |||||
$reply_handler->getRecipientsSummaryHTML($to_handles, $cc_handles)); | |||||
// TODO: Reply-To handling is still missing | |||||
$template | |||||
->setFrom($this->getActingAsPHID()) | |||||
->setSubjectPrefix($this->getMailSubjectPrefix()) | |||||
->setVarySubjectPrefix('['.$action.']') | |||||
->setThreadID($this->getMailThreadID($object), $this->getIsNewObject()) | |||||
->setRelatedPHID($object->getPHID()) | |||||
->setExcludeMailRecipientPHIDs($this->excludeMailRecipientPHIDs) | |||||
->setMailTags($mail_tags) | |||||
->setIsBulk(true) | |||||
->setBody($body->render()) | |||||
->setHTMLBody($body->renderHTML()); | |||||
foreach ($body->getAttachments() as $attachment) { | |||||
$template->addAttachment($attachment); | |||||
} | |||||
if ($this->heraldHeader) { | |||||
$template->addHeader('X-Herald-Rules', $this->heraldHeader); | |||||
} | |||||
if ($object instanceof PhabricatorProjectInterface) { | |||||
$this->addMailProjectMetadata($object, $template, $viewer); | |||||
} | |||||
if ($this->parentMessageID) { | |||||
$template->setParentMessageID($this->parentMessageID); | |||||
} | |||||
$emails[] = $template; | |||||
if ($send_as_one_email) { | |||||
// We send all just a single email for all users | |||||
break; | |||||
} | |||||
} | |||||
$mailed_phids = array(); | |||||
foreach ($emails as $mail) { | |||||
$mail->saveAndSend(); | |||||
// The mailed_phids are collected here so we know we've | |||||
// sent them email and don't have to notify them in the Web UI | |||||
$mailed_phids += $mail->buildRecipientList(); | |||||
} | |||||
return $mailed_phids; | |||||
} | |||||
/* -( Sending Mail )---------------------------------------------------- */ | |||||
protected function buildMailTemplate(PhabricatorLiskDAO $object) { | |||||
throw new Exception('Capability not supported.'); | |||||
} | |||||
protected function getMailSubjectPrefix() { | |||||
throw new Exception('Capability not supported.'); | |||||
} | |||||
private function getMailTo(PhabricatorLiskDAO $object) { | |||||
return $this->mailTo; | |||||
} | |||||
private function getMailCC(PhabricatorLiskDAO $object) { | |||||
return $this->mailCC; | |||||
} | |||||
private function addMailProjectMetadata( | |||||
PhabricatorLiskDAO $object, | |||||
PhabricatorMetaMTAMail $template, | |||||
PhabricatorUser $viewer) { | |||||
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( | |||||
$object->getPHID(), | |||||
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); | |||||
if (!$project_phids) { | |||||
return; | |||||
} | |||||
$handles = id(new PhabricatorHandleQuery()) | |||||
->setViewer($viewer) | |||||
->withPHIDs($project_phids) | |||||
->execute(); | |||||
$project_tags = array(); | |||||
foreach ($handles as $handle) { | |||||
if (!$handle->isComplete()) { | |||||
continue; | |||||
} | |||||
$project_tags[] = '<'.$handle->getObjectName().'>'; | |||||
} | |||||
if (!$project_tags) { | |||||
return; | |||||
} | |||||
$project_tags = implode(', ', $project_tags); | |||||
$template->addHeader('X-Phabricator-Projects', $project_tags); | |||||
} | |||||
protected function getMailThreadID(PhabricatorLiskDAO $object) { | |||||
return $object->getPHID(); | |||||
} | |||||
protected function getMailTags( | |||||
PhabricatorLiskDAO $object, | |||||
array $xactions) { | |||||
$tags = array(); | |||||
foreach ($xactions as $xaction) { | |||||
$tags[] = $xaction->getMailTags(); | |||||
} | |||||
return array_mergev($tags); | |||||
} | |||||
public function getMailTagsMap() { | |||||
// TODO: We should move shared mail tags, like "comment", here. | |||||
return array(); | |||||
} | |||||
protected function getStrongestAction( | |||||
PhabricatorLiskDAO $object, | |||||
array $xactions) { | |||||
return last(msort($xactions, 'getActionStrength')); | |||||
} | |||||
protected function getMailAction( | |||||
PhabricatorLiskDAO $object, | |||||
array $xactions) { | |||||
return $this->getStrongestAction($object, $xactions)->getActionName(); | |||||
} | |||||
protected function buildMailBody( | |||||
PhabricatorLiskDAO $object, | |||||
array $xactions, | |||||
PhabricatorUser $viewer) { | |||||
$headers = array(); | |||||
$comments = array(); | |||||
foreach ($xactions as $xaction) { | |||||
if ($xaction->shouldHideForMail($xactions)) { | |||||
continue; | |||||
} | |||||
$header = $xaction->getTitleForMail(); | |||||
if ($header !== null) { | |||||
$headers[] = $header; | |||||
} | |||||
$comment = $xaction->getBodyForMail(); | |||||
if ($comment !== null) { | |||||
$comments[] = $comment; | |||||
} | |||||
} | |||||
$body = new PhabricatorMetaMTAMailBody(); | |||||
$body->setViewer($viewer); | |||||
$body->addRawSection(implode("\n", $headers)); | |||||
foreach ($comments as $comment) { | |||||
$body->addRemarkupSection($comment); | |||||
} | |||||
if ($object instanceof PhabricatorCustomFieldInterface) { | |||||
$field_list = PhabricatorCustomField::getObjectFields( | |||||
$object, | |||||
PhabricatorCustomField::ROLE_TRANSACTIONMAIL); | |||||
$field_list->setViewer($viewer); | |||||
$field_list->readFieldsFromStorage($object); | |||||
foreach ($field_list->getFields() as $field) { | |||||
$field->updateTransactionMailBody( | |||||
$body, | |||||
$this, | |||||
$xactions); | |||||
} | |||||
} | |||||
return $body; | |||||
} | |||||
/* -( Publishing Feed Stories )------------------------------------------ */ | |||||
/** | |||||
* @task feed | |||||
*/ | |||||
protected function getFeedStoryType() { | |||||
return 'PhabricatorApplicationTransactionFeedStory'; | |||||
} | |||||
/** | |||||
* @task feed | |||||
*/ | |||||
protected function getFeedRelatedPHIDs( | |||||
PhabricatorLiskDAO $object, | |||||
array $xactions) { | |||||
$phids = array( | |||||
$object->getPHID(), | |||||
$this->getActingAsPHID(), | |||||
); | |||||
if ($object instanceof PhabricatorProjectInterface) { | |||||
$project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( | |||||
$object->getPHID(), | |||||
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); | |||||
foreach ($project_phids as $project_phid) { | |||||
$phids[] = $project_phid; | |||||
} | |||||
} | |||||
return $phids; | |||||
} | |||||
/** | |||||
* @task feed | |||||
*/ | |||||
protected function getFeedNotifyPHIDs( | |||||
PhabricatorLiskDAO $object, | |||||
array $xactions) { | |||||
return array_unique(array_merge( | |||||
$this->getMailTo($object), | |||||
$this->getMailCC($object))); | |||||
} | |||||
/** | |||||
* @task feed | |||||
*/ | |||||
protected function getFeedStoryData( | |||||
PhabricatorLiskDAO $object, | |||||
array $xactions) { | |||||
$xactions = msort($xactions, 'getActionStrength'); | |||||
$xactions = array_reverse($xactions); | |||||
return array( | |||||
'objectPHID' => $object->getPHID(), | |||||
'transactionPHIDs' => mpull($xactions, 'getPHID'), | |||||
); | |||||
} | |||||
/** | |||||
* @task feed | |||||
*/ | |||||
protected function publishFeedStory( | |||||
PhabricatorLiskDAO $object, | |||||
array $xactions, | |||||
array $mailed_phids) { | |||||
$related_phids = $this->getFeedRelatedPHIDs($object, $xactions); | |||||
$subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions); | |||||
$story_type = $this->getFeedStoryType(); | |||||
$story_data = $this->getFeedStoryData($object, $xactions); | |||||
id(new PhabricatorFeedStoryPublisher()) | |||||
->setStoryType($story_type) | |||||
->setStoryData($story_data) | |||||
->setStoryTime(time()) | |||||
->setStoryAuthorPHID($this->getActingAsPHID()) | |||||
->setRelatedPHIDs($related_phids) | |||||
->setPrimaryObjectPHID($object->getPHID()) | |||||
->setSubscribedPHIDs($subscribed_phids) | |||||
->setMailRecipientPHIDs($mailed_phids) | |||||
->setMailTags($this->getMailTags($object, $xactions)) | |||||
->publish(); | |||||
} | |||||
} |