diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 35651c6a78..b0f5bad0e3 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1,496 +1,509 @@ auxiliaryFields = $fields; return $this; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function applyTransactions(ManiphestTask $task, array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $email_cc = $task->getCCPHIDs(); $email_to = array(); $email_to[] = $task->getOwnerPHID(); $pri_changed = $this->isCreate($transactions); $aux_writes = array(); foreach ($transactions as $key => $transaction) { $type = $transaction->getTransactionType(); $new = $transaction->getNewValue(); $email_to[] = $transaction->getAuthorPHID(); $value_is_phid_set = false; switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: $old = null; break; case ManiphestTransactionType::TYPE_STATUS: $old = $task->getStatus(); break; case ManiphestTransactionType::TYPE_OWNER: $old = $task->getOwnerPHID(); break; case ManiphestTransactionType::TYPE_CCS: $old = $task->getCCPHIDs(); $value_is_phid_set = true; break; case ManiphestTransactionType::TYPE_PRIORITY: $old = $task->getPriority(); break; case ManiphestTransactionType::TYPE_EDGE: $old = $transaction->getOldValue(); break; case ManiphestTransactionType::TYPE_ATTACH: $old = $task->getAttached(); break; case ManiphestTransactionType::TYPE_TITLE: $old = $task->getTitle(); break; case ManiphestTransactionType::TYPE_DESCRIPTION: $old = $task->getDescription(); break; case ManiphestTransactionType::TYPE_PROJECTS: $old = $task->getProjectPHIDs(); $value_is_phid_set = true; break; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $aux_key = $transaction->getMetadataValue('customfield:key'); if (!$aux_key) { throw new Exception( "Expected 'customfield:key' metadata on TYPE_CUSTOMFIELD ". "transaction."); } // This has already been populated. $old = $transaction->getOldValue(); break; default: throw new Exception('Unknown action type.'); } $old_cmp = $old; $new_cmp = $new; if ($value_is_phid_set) { // Normalize the old and new values if they are PHID sets so we don't // get any no-op transactions where the values differ only by keys, // order, duplicates, etc. if (is_array($old)) { $old = array_filter($old); $old = array_unique($old); sort($old); $old = array_values($old); $old_cmp = $old; } if (is_array($new)) { $new = array_filter($new); $new = array_unique($new); $transaction->setNewValue($new); $new_cmp = $new; sort($new_cmp); $new_cmp = array_values($new_cmp); } } if (($old !== null) && ($old_cmp == $new_cmp)) { if (count($transactions) > 1 && !$transaction->hasComments()) { // If we have at least one other transaction and this one isn't // doing anything and doesn't have any comments, just throw it // away. unset($transactions[$key]); continue; } else { $transaction->setOldValue(null); $transaction->setNewValue(null); $transaction->setTransactionType( PhabricatorTransactions::TYPE_COMMENT); } } else { switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: break; case ManiphestTransactionType::TYPE_STATUS: $task->setStatus($new); break; case ManiphestTransactionType::TYPE_OWNER: if ($new) { $handle = id(new PhabricatorHandleQuery()) ->setViewer($this->getActor()) ->withPHIDs(array($new)) ->executeOne(); $task->setOwnerOrdering($handle->getName()); } else { $task->setOwnerOrdering(null); } $task->setOwnerPHID($new); break; case ManiphestTransactionType::TYPE_CCS: $task->setCCPHIDs($new); break; case ManiphestTransactionType::TYPE_PRIORITY: $task->setPriority($new); $pri_changed = true; break; case ManiphestTransactionType::TYPE_ATTACH: $task->setAttached($new); break; case ManiphestTransactionType::TYPE_TITLE: $task->setTitle($new); break; case ManiphestTransactionType::TYPE_DESCRIPTION: $task->setDescription($new); break; case ManiphestTransactionType::TYPE_PROJECTS: $task->setProjectPHIDs($new); break; case PhabricatorTransactions::TYPE_CUSTOMFIELD: $aux_key = $transaction->getMetadataValue('customfield:key'); $aux_writes[$aux_key] = $new; break; case ManiphestTransactionType::TYPE_EDGE: // Edge edits are accomplished through PhabricatorEdgeEditor, which // has authority. break; default: throw new Exception('Unknown action type.'); } $transaction->setOldValue($old); $transaction->setNewValue($new); } } if ($pri_changed) { $subpriority = ManiphestTransactionEditor::getNextSubpriority( $task->getPriority(), null); $task->setSubpriority($subpriority); } $task->save(); if ($aux_writes) { ManiphestAuxiliaryFieldSpecification::writeLegacyAuxiliaryUpdates( $task, $aux_writes); } foreach ($transactions as $transaction) { $transaction->setTransactionTask($task); $transaction->save(); } $email_to[] = $task->getOwnerPHID(); $email_cc = array_merge( $email_cc, $task->getCCPHIDs()); $mail = $this->sendEmail($task, $transactions, $email_to, $email_cc); $this->publishFeedStory( $task, $transactions, $mail->buildRecipientList()); id(new PhabricatorSearchIndexer()) ->indexDocumentByPHID($task->getPHID()); $fields = PhabricatorCustomField::getObjectFields( $task, PhabricatorCustomField::ROLE_APPLICATIONSEARCH); $fields->readFieldsFromStorage($task); $fields->rebuildIndexes($task); } protected function getSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); } private function sendEmail($task, $transactions, $email_to, $email_cc) { $email_to = array_filter(array_unique($email_to)); $email_cc = array_filter(array_unique($email_cc)); $phids = array(); foreach ($transactions as $transaction) { foreach ($transaction->extractPHIDs() as $phid) { $phids[$phid] = true; } } foreach ($email_to as $phid) { $phids[$phid] = true; } foreach ($email_cc as $phid) { $phids[$phid] = true; } $phids = array_keys($phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getActor()) ->withPHIDs($phids) ->execute(); - $view = new ManiphestTransactionDetailView(); - $view->setTransactionGroup($transactions); - $view->setHandles($handles); - $view->setAuxiliaryFields($this->auxiliaryFields); - list($action, $main_body) = $view->renderForEmail($with_date = false); + $main_body = array(); + foreach ($transactions as $transaction) { + $main_body[] = id(clone $transaction->getModernTransaction()) + ->setHandles($handles) + ->setRenderingTarget('text') + ->getTitle(); + } + + foreach ($transactions as $transaction) { + if ($transaction->getComments()) { + $main_body[] = null; + $main_body[] = $transaction->getComments(); + } + } + + $main_body = implode("\n", $main_body); + + $action = head($transactions)->getModernTransaction()->getActionName(); $is_create = $this->isCreate($transactions); $task_uri = PhabricatorEnv::getProductionURI('/T'.$task->getID()); $reply_handler = $this->buildReplyHandler($task); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection($main_body); if ($is_create) { $body->addTextSection(pht('TASK DESCRIPTION'), $task->getDescription()); } $body->addTextSection(pht('TASK DETAIL'), $task_uri); $body->addReplySection($reply_handler->getReplyHandlerInstructions()); $thread_id = 'maniphest-task-'.$task->getPHID(); $task_id = $task->getID(); $title = $task->getTitle(); $mailtags = $this->getMailTags($transactions); $template = id(new PhabricatorMetaMTAMail()) ->setSubject("T{$task_id}: {$title}") ->setSubjectPrefix($this->getSubjectPrefix()) ->setVarySubjectPrefix("[{$action}]") ->setFrom($transaction->getAuthorPHID()) ->setParentMessageID($this->parentMessageID) ->addHeader('Thread-Topic', "T{$task_id}: ".$task->getOriginalTitle()) ->setThreadID($thread_id, $is_create) ->setRelatedPHID($task->getPHID()) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setIsBulk(true) ->setMailTags($mailtags) ->setBody($body->render()); $mails = $reply_handler->multiplexMail( $template, array_select_keys($handles, $email_to), array_select_keys($handles, $email_cc)); foreach ($mails as $mail) { $mail->saveAndSend(); } $template->addTos($email_to); $template->addCCs($email_cc); return $template; } public function buildReplyHandler(ManiphestTask $task) { $handler_object = PhabricatorEnv::newObjectFromConfig( 'metamta.maniphest.reply-handler'); $handler_object->setMailReceiver($task); return $handler_object; } private function publishFeedStory( ManiphestTask $task, array $transactions, array $mailed_phids) { assert_instances_of($transactions, 'ManiphestTransaction'); $actions = array(ManiphestAction::ACTION_UPDATE); $comments = null; foreach ($transactions as $transaction) { if ($transaction->hasComments()) { $comments = $transaction->getComments(); } $type = $transaction->getTransactionType(); switch ($type) { case ManiphestTransactionType::TYPE_OWNER: $actions[] = ManiphestAction::ACTION_ASSIGN; break; case ManiphestTransactionType::TYPE_STATUS: if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { $actions[] = ManiphestAction::ACTION_CLOSE; } else if ($this->isCreate($transactions)) { $actions[] = ManiphestAction::ACTION_CREATE; } else { $actions[] = ManiphestAction::ACTION_REOPEN; } break; default: $actions[] = $type; break; } } $action_type = ManiphestAction::selectStrongestAction($actions); $owner_phid = $task->getOwnerPHID(); $actor_phid = head($transactions)->getAuthorPHID(); $author_phid = $task->getAuthorPHID(); id(new PhabricatorFeedStoryPublisher()) ->setStoryType('PhabricatorFeedStoryManiphest') ->setStoryData(array( 'taskPHID' => $task->getPHID(), 'transactionIDs' => mpull($transactions, 'getID'), 'ownerPHID' => $owner_phid, 'action' => $action_type, 'comments' => $comments, 'description' => $task->getDescription(), )) ->setStoryTime(time()) ->setStoryAuthorPHID($actor_phid) ->setRelatedPHIDs( array_merge( array_filter( array( $task->getPHID(), $author_phid, $actor_phid, $owner_phid, )), $task->getProjectPHIDs())) ->setPrimaryObjectPHID($task->getPHID()) ->setSubscribedPHIDs( array_merge( array_filter( array( $author_phid, $owner_phid, $actor_phid)), $task->getCCPHIDs())) ->setMailRecipientPHIDs($mailed_phids) ->publish(); } private function isCreate(array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $is_create = false; foreach ($transactions as $transaction) { $type = $transaction->getTransactionType(); if (($type == ManiphestTransactionType::TYPE_STATUS) && ($transaction->getOldValue() === null) && ($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) { $is_create = true; } } return $is_create; } private function getMailTags(array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $tags = array(); foreach ($transactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransactionType::TYPE_STATUS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_STATUS; break; case ManiphestTransactionType::TYPE_OWNER: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OWNER; break; case ManiphestTransactionType::TYPE_CCS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC; break; case ManiphestTransactionType::TYPE_PROJECTS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS; break; case ManiphestTransactionType::TYPE_PRIORITY: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY; break; case PhabricatorTransactions::TYPE_COMMENT: // this is a comment which we will check separately below for // content break; default: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER; break; } if ($xaction->hasComments()) { $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT; } } return array_unique($tags); } public static function getNextSubpriority($pri, $sub) { if ($sub === null) { $next = id(new ManiphestTask())->loadOneWhere( 'priority = %d ORDER BY subpriority ASC LIMIT 1', $pri); if ($next) { return $next->getSubpriority() - ((double)(2 << 16)); } } else { $next = id(new ManiphestTask())->loadOneWhere( 'priority = %d AND subpriority > %s ORDER BY subpriority ASC LIMIT 1', $pri, $sub); if ($next) { return ($sub + $next->getSubpriority()) / 2; } } return (double)(2 << 32); } public static function addCC( ManiphestTask $task, PhabricatorUser $user) { $current_ccs = $task->getCCPHIDs(); $new_ccs = array_merge($current_ccs, array($user->getPHID())); $transaction = new ManiphestTransaction(); $transaction->setTransactionTask($task); $transaction->setAuthorPHID($user->getPHID()); $transaction->setTransactionType(ManiphestTransactionType::TYPE_CCS); $transaction->setNewValue(array_unique($new_ccs)); $transaction->setOldValue($current_ccs); id(new ManiphestTransactionEditor()) ->setActor($user) ->applyTransactions($task, array($transaction)); } public static function removeCC( ManiphestTask $task, PhabricatorUser $user) { $current_ccs = $task->getCCPHIDs(); $new_ccs = array_diff($current_ccs, array($user->getPHID())); $transaction = new ManiphestTransaction(); $transaction->setTransactionTask($task); $transaction->setAuthorPHID($user->getPHID()); $transaction->setTransactionType(ManiphestTransactionType::TYPE_CCS); $transaction->setNewValue(array_unique($new_ccs)); $transaction->setOldValue($current_ccs); id(new ManiphestTransactionEditor()) ->setActor($user) ->applyTransactions($task, array($transaction)); } } diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index 363b476b83..cb0aa64cd1 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -1,289 +1,291 @@ proxy = new ManiphestTransactionPro(); } public function __clone() { $this->proxy = clone $this->proxy; } public static function newFromModernTransaction( ManiphestTransactionPro $pro) { $obj = new ManiphestTransaction(); $obj->proxy = $pro; return $obj; } public function getModernTransaction() { return $this->proxy; } public function save() { $this->proxy->openTransaction(); $this->proxy ->setViewPolicy('public') ->setEditPolicy($this->getAuthorPHID()) ->save(); if ($this->pendingComment) { $comment = id(new ManiphestTransactionComment()) ->setTransactionPHID($this->proxy->getPHID()) ->setCommentVersion(1) ->setAuthorPHID($this->getAuthorPHID()) ->setViewPolicy('public') ->setEditPolicy($this->getAuthorPHID()) ->setContent($this->pendingComment) ->setContentSource($this->getContentSource()) ->setIsDeleted(0) ->save(); $this->proxy ->setCommentVersion(1) ->setCommentPHID($comment->getPHID()) ->save(); + $this->proxy->attachComment($comment); + $this->pendingComment = null; } $this->proxy->saveTransaction(); return $this; } public function setTransactionTask(ManiphestTask $task) { $this->proxy->setObjectPHID($task->getPHID()); return $this; } public function getTaskPHID() { return $this->proxy->getObjectPHID(); } public function getID() { return $this->proxy->getID(); } public function setTaskID() { throw new Exception("No longer supported!"); } public function getTaskID() { throw new Exception("No longer supported!"); } public function getAuthorPHID() { return $this->proxy->getAuthorPHID(); } public function setAuthorPHID($phid) { $this->proxy->setAuthorPHID($phid); return $this; } public function getOldValue() { return $this->proxy->getOldValue(); } public function setOldValue($value) { $this->proxy->setOldValue($value); return $this; } public function getNewValue() { return $this->proxy->getNewValue(); } public function setNewValue($value) { $this->proxy->setNewValue($value); return $this; } public function getTransactionType() { return $this->proxy->getTransactionType(); } public function setTransactionType($value) { $this->proxy->setTransactionType($value); return $this; } public function setContentSource(PhabricatorContentSource $content_source) { $this->proxy->setContentSource($content_source); return $this; } public function getContentSource() { return $this->proxy->getContentSource(); } public function getMetadataValue($key, $default = null) { return $this->proxy->getMetadataValue($key, $default); } public function setMetadataValue($key, $value) { $this->proxy->setMetadataValue($key, $value); return $this; } public function getComments() { if ($this->pendingComment) { return $this->pendingComment; } if ($this->proxy->getComment()) { return $this->proxy->getComment()->getContent(); } return null; } public function setComments($comment) { $this->pendingComment = $comment; return $this; } public function getDateCreated() { return $this->proxy->getDateCreated(); } public function getDateModified() { return $this->proxy->getDateModified(); } public function extractPHIDs() { $phids = array(); switch ($this->getTransactionType()) { case ManiphestTransactionType::TYPE_CCS: case ManiphestTransactionType::TYPE_PROJECTS: foreach ($this->getOldValue() as $phid) { $phids[] = $phid; } foreach ($this->getNewValue() as $phid) { $phids[] = $phid; } break; case ManiphestTransactionType::TYPE_OWNER: $phids[] = $this->getOldValue(); $phids[] = $this->getNewValue(); break; case ManiphestTransactionType::TYPE_EDGE: $phids = array_merge( $phids, array_keys($this->getOldValue() + $this->getNewValue())); break; case ManiphestTransactionType::TYPE_ATTACH: $old = $this->getOldValue(); $new = $this->getNewValue(); if (!is_array($old)) { $old = array(); } if (!is_array($new)) { $new = array(); } $val = array_merge(array_values($old), array_values($new)); foreach ($val as $stuff) { foreach ($stuff as $phid => $ignored) { $phids[] = $phid; } } break; } $phids[] = $this->getAuthorPHID(); return $phids; } public function canGroupWith($target) { if ($target->getAuthorPHID() != $this->getAuthorPHID()) { return false; } if ($target->hasComments() && $this->hasComments()) { return false; } $ttime = $target->getDateCreated(); $stime = $this->getDateCreated(); if (abs($stime - $ttime) > 60) { return false; } if ($target->getTransactionType() == $this->getTransactionType()) { $aux_type = PhabricatorTransactions::TYPE_CUSTOMFIELD; if ($this->getTransactionType() == $aux_type) { $that_key = $target->getMetadataValue('customfield:key'); $this_key = $this->getMetadataValue('customfield:key'); if ($that_key == $this_key) { return false; } } else { return false; } } return true; } public function hasComments() { return (bool)strlen(trim($this->getComments())); } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { if ($this->shouldUseMarkupCache($field)) { $id = $this->getID(); } else { $id = PhabricatorHash::digest($this->getMarkupText($field)); } return "maniphest:x:{$field}:{$id}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getComments(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newManiphestMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } }