public function save()
 {
     if (!$this->viewer) {
         throw new Exception("Must set user before saving question");
     }
     if (!$this->question) {
         throw new Exception("Must set question before saving it");
     }
     $viewer = $this->viewer;
     $question = $this->question;
     $question->save();
     // search index
     $question->attachRelated();
     PhabricatorSearchPonderIndexer::indexQuestion($question);
     // subscribe author and @mentions
     $subeditor = id(new PhabricatorSubscriptionsEditor())->setObject($question)->setUser($viewer);
     $subeditor->subscribeExplicit(array($question->getAuthorPHID()));
     $content = $question->getContent();
     $at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(array($content));
     $subeditor->subscribeImplicit($at_mention_phids);
     $subeditor->save();
     if ($this->shouldEmail && $at_mention_phids) {
         id(new PonderMentionMail($question, $question, $viewer))->setToPHIDs($at_mention_phids)->send();
     }
 }
 public function save()
 {
     if (!$this->comment) {
         throw new Exception("Must set comment before saving it");
     }
     if (!$this->question) {
         throw new Exception("Must set question before saving comment");
     }
     if (!$this->targetPHID) {
         throw new Exception("Must set target before saving comment");
     }
     if (!$this->viewer) {
         throw new Exception("Must set viewer before saving comment");
     }
     $comment = $this->comment;
     $question = $this->question;
     $target = $this->targetPHID;
     $viewer = $this->viewer;
     $comment->save();
     $question->attachRelated();
     PhabricatorSearchPonderIndexer::indexQuestion($question);
     // subscribe author and @mentions
     $subeditor = id(new PhabricatorSubscriptionsEditor())->setObject($question)->setUser($viewer);
     $subeditor->subscribeExplicit(array($comment->getAuthorPHID()));
     $content = $comment->getContent();
     $at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(array($content));
     $subeditor->subscribeImplicit($at_mention_phids);
     $subeditor->save();
     if ($this->shouldEmail) {
         // now load subscribers, including implicitly-added @mention victims
         $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID($question->getPHID());
         // @mention emails (but not for anyone who has explicitly unsubscribed)
         if (array_intersect($at_mention_phids, $subscribers)) {
             id(new PonderMentionMail($question, $comment, $viewer))->setToPHIDs($at_mention_phids)->send();
         }
         if ($target === $question->getPHID()) {
             $target = $question;
         } else {
             $answers_by_phid = mgroup($question->getAnswers(), 'getPHID');
             $target = head($answers_by_phid[$target]);
         }
         // only send emails to others in the same thread
         $thread = mpull($target->getComments(), 'getAuthorPHID');
         $thread[] = $target->getAuthorPHID();
         $thread[] = $question->getAuthorPHID();
         $other_subs = array_diff(array_intersect($thread, $subscribers), $at_mention_phids);
         // 'Comment' emails for subscribers who are in the same comment thread,
         // including the author of the parent question and/or answer, excluding
         // @mentions (and excluding the author, depending on their MetaMTA
         // settings).
         if ($other_subs) {
             id(new PonderCommentMail($question, $comment, $viewer))->setToPHIDs($other_subs)->send();
         }
     }
 }
示例#3
0
 public function saveAnswer()
 {
     if (!$this->viewer) {
         throw new Exception("Must set user before saving question");
     }
     if (!$this->question) {
         throw new Exception("Must set question before saving answer");
     }
     if (!$this->answer) {
         throw new Exception("Must set answer before saving it");
     }
     $question = $this->question;
     $answer = $this->answer;
     $viewer = $this->viewer;
     $conn = $answer->establishConnection('w');
     $trans = $conn->openTransaction();
     $trans->beginReadLocking();
     $question->reload();
     queryfx($conn, 'UPDATE %T as t
     SET t.`answerCount` = t.`answerCount` + 1
     WHERE t.`PHID` = %s', $question->getTableName(), $question->getPHID());
     $answer->setQuestionID($question->getID());
     $answer->save();
     $trans->endReadLocking();
     $trans->saveTransaction();
     $question->attachRelated();
     PhabricatorSearchPonderIndexer::indexQuestion($question);
     // subscribe author and @mentions
     $subeditor = id(new PhabricatorSubscriptionsEditor())->setObject($question)->setUser($viewer);
     $subeditor->subscribeExplicit(array($answer->getAuthorPHID()));
     $content = $answer->getContent();
     $at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions(array($content));
     $subeditor->subscribeImplicit($at_mention_phids);
     $subeditor->save();
     if ($this->shouldEmail) {
         // now load subscribers, including implicitly-added @mention victims
         $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID($question->getPHID());
         // @mention emails (but not for anyone who has explicitly unsubscribed)
         if (array_intersect($at_mention_phids, $subscribers)) {
             id(new PonderMentionMail($question, $answer, $viewer))->setToPHIDs($at_mention_phids)->send();
         }
         $other_subs = array_diff($subscribers, $at_mention_phids);
         // 'Answered' emails for subscribers who are not @mentiond (and excluding
         // author depending on their MetaMTA settings).
         if ($other_subs) {
             id(new PonderAnsweredMail($question, $answer, $viewer))->setToPHIDs($other_subs)->send();
         }
     }
 }
 private function buildMentionTransaction(PhabricatorLiskDAO $object, array $xactions, array $blocks)
 {
     if (!$object instanceof PhabricatorSubscribableInterface) {
         return null;
     }
     $texts = array_mergev($blocks);
     $phids = PhabricatorMarkupEngine::extractPHIDsFromMentions($this->getActor(), $texts);
     $this->mentionedPHIDs = $phids;
     if ($object->getPHID()) {
         // Don't try to subscribe already-subscribed mentions: we want to generate
         // a dialog about an action having no effect if the user explicitly adds
         // existing CCs, but not if they merely mention existing subscribers.
         $phids = array_diff($phids, $this->subscribers);
     }
     foreach ($phids as $key => $phid) {
         if ($object->isAutomaticallySubscribed($phid)) {
             unset($phids[$key]);
         }
     }
     $phids = array_values($phids);
     if (!$phids) {
         return null;
     }
     $xaction = newv(get_class(head($xactions)), array());
     $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
     $xaction->setNewValue(array('+' => $phids));
     return $xaction;
 }
 public function save()
 {
     $revision = $this->revision;
     $action = $this->action;
     $actor_phid = $this->actorPHID;
     $actor = id(new PhabricatorUser())->loadOneWhere('PHID = %s', $actor_phid);
     $actor_is_author = $actor_phid == $revision->getAuthorPHID();
     $allow_self_accept = PhabricatorEnv::getEnvConfig('differential.allow-self-accept', false);
     $revision_status = $revision->getStatus();
     $revision->loadRelationships();
     $reviewer_phids = $revision->getReviewers();
     if ($reviewer_phids) {
         $reviewer_phids = array_combine($reviewer_phids, $reviewer_phids);
     }
     $metadata = array();
     $inline_comments = array();
     if ($this->attachInlineComments) {
         $inline_comments = id(new DifferentialInlineComment())->loadAllWhere('authorPHID = %s AND revisionID = %d AND commentID IS NULL', $this->actorPHID, $revision->getID());
     }
     switch ($action) {
         case DifferentialAction::ACTION_COMMENT:
             if (!$this->message && !$inline_comments) {
                 throw new DifferentialActionHasNoEffectException("You are submitting an empty comment with no action: " . "you must act on the revision or post a comment.");
             }
             break;
         case DifferentialAction::ACTION_RESIGN:
             if ($actor_is_author) {
                 throw new Exception('You can not resign from your own revision!');
             }
             if (empty($reviewer_phids[$actor_phid])) {
                 throw new DifferentialActionHasNoEffectException("You can not resign from this revision because you are not " . "a reviewer.");
             }
             DifferentialRevisionEditor::alterReviewers($revision, $reviewer_phids, $rem = array($actor_phid), $add = array(), $actor_phid);
             break;
         case DifferentialAction::ACTION_ABANDON:
             if (!$actor_is_author) {
                 throw new Exception('You can only abandon your own revisions.');
             }
             if ($revision_status == ArcanistDifferentialRevisionStatus::CLOSED) {
                 throw new DifferentialActionHasNoEffectException("You can not abandon this revision because it has already " . "been closed.");
             }
             if ($revision_status == ArcanistDifferentialRevisionStatus::ABANDONED) {
                 throw new DifferentialActionHasNoEffectException("You can not abandon this revision because it has already " . "been abandoned.");
             }
             $revision->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED);
             break;
         case DifferentialAction::ACTION_ACCEPT:
             if ($actor_is_author && !$allow_self_accept) {
                 throw new Exception('You can not accept your own revision.');
             }
             if ($revision_status != ArcanistDifferentialRevisionStatus::NEEDS_REVIEW && $revision_status != ArcanistDifferentialRevisionStatus::NEEDS_REVISION) {
                 switch ($revision_status) {
                     case ArcanistDifferentialRevisionStatus::ACCEPTED:
                         throw new DifferentialActionHasNoEffectException("You can not accept this revision because someone else " . "already accepted it.");
                     case ArcanistDifferentialRevisionStatus::ABANDONED:
                         throw new DifferentialActionHasNoEffectException("You can not accept this revision because it has been " . "abandoned.");
                     case ArcanistDifferentialRevisionStatus::CLOSED:
                         throw new DifferentialActionHasNoEffectException("You can not accept this revision because it has already " . "been closed.");
                     default:
                         throw new Exception("Unexpected revision state '{$revision_status}'!");
                 }
             }
             $revision->setStatus(ArcanistDifferentialRevisionStatus::ACCEPTED);
             if (!isset($reviewer_phids[$actor_phid])) {
                 DifferentialRevisionEditor::alterReviewers($revision, $reviewer_phids, $rem = array(), $add = array($actor_phid), $actor_phid);
             }
             break;
         case DifferentialAction::ACTION_REQUEST:
             if (!$actor_is_author) {
                 throw new Exception('You must own a revision to request review.');
             }
             switch ($revision_status) {
                 case ArcanistDifferentialRevisionStatus::ACCEPTED:
                 case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
                     $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
                     break;
                 case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
                     throw new DifferentialActionHasNoEffectException("You can not request review of this revision because it has " . "been abandoned.");
                 case ArcanistDifferentialRevisionStatus::ABANDONED:
                     throw new DifferentialActionHasNoEffectException("You can not request review of this revision because it has " . "been abandoned.");
                 case ArcanistDifferentialRevisionStatus::CLOSED:
                     throw new DifferentialActionHasNoEffectException("You can not request review of this revision because it has " . "already been closed.");
                 default:
                     throw new Exception("Unexpected revision state '{$revision_status}'!");
             }
             list($added_reviewers, $ignored) = $this->alterReviewers();
             if ($added_reviewers) {
                 $key = DifferentialComment::METADATA_ADDED_REVIEWERS;
                 $metadata[$key] = $added_reviewers;
             }
             break;
         case DifferentialAction::ACTION_REJECT:
             if ($actor_is_author) {
                 throw new Exception('You can not request changes to your own revision.');
             }
             switch ($revision_status) {
                 case ArcanistDifferentialRevisionStatus::ACCEPTED:
                 case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
                 case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
                     // NOTE: We allow you to reject an already-rejected revision
                     // because it doesn't create any ambiguity and avoids a rather
                     // needless dialog.
                     break;
                 case ArcanistDifferentialRevisionStatus::ABANDONED:
                     throw new DifferentialActionHasNoEffectException("You can not request changes to this revision because it has " . "been abandoned.");
                 case ArcanistDifferentialRevisionStatus::CLOSED:
                     throw new DifferentialActionHasNoEffectException("You can not request changes to this revision because it has " . "already been closed.");
                 default:
                     throw new Exception("Unexpected revision state '{$revision_status}'!");
             }
             if (!isset($reviewer_phids[$actor_phid])) {
                 DifferentialRevisionEditor::alterReviewers($revision, $reviewer_phids, $rem = array(), $add = array($actor_phid), $actor_phid);
             }
             $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVISION);
             break;
         case DifferentialAction::ACTION_RETHINK:
             if (!$actor_is_author) {
                 throw new Exception("You can not plan changes to somebody else's revision");
             }
             switch ($revision_status) {
                 case ArcanistDifferentialRevisionStatus::ACCEPTED:
                 case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
                 case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
                     break;
                 case ArcanistDifferentialRevisionStatus::ABANDONED:
                     throw new DifferentialActionHasNoEffectException("You can not plan changes to this revision because it has " . "been abandoned.");
                 case ArcanistDifferentialRevisionStatus::CLOSED:
                     throw new DifferentialActionHasNoEffectException("You can not plan changes to this revision because it has " . "already been closed.");
                 default:
                     throw new Exception("Unexpected revision state '{$revision_status}'!");
             }
             $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVISION);
             break;
         case DifferentialAction::ACTION_RECLAIM:
             if (!$actor_is_author) {
                 throw new Exception('You can not reclaim a revision you do not own.');
             }
             if ($revision_status != ArcanistDifferentialRevisionStatus::ABANDONED) {
                 throw new DifferentialActionHasNoEffectException("You can not reclaim this revision because it is not abandoned.");
             }
             $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
             break;
         case DifferentialAction::ACTION_CLOSE:
             // NOTE: The daemons can mark things closed from any state. We treat
             // them as completely authoritative.
             if (!$this->isDaemonWorkflow) {
                 if (!$actor_is_author) {
                     throw new Exception("You can not mark a revision you don't own as closed.");
                 }
                 $status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
                 $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED;
                 if ($revision_status == $status_closed) {
                     throw new DifferentialActionHasNoEffectException("You can not mark this revision as closed because it has " . "already been marked as closed.");
                 }
                 if ($revision_status != $status_accepted) {
                     throw new DifferentialActionHasNoEffectException("You can not mark this revision as closed because it is " . "has not been accepted.");
                 }
             }
             if (!$revision->getDateCommitted()) {
                 $revision->setDateCommitted(time());
             }
             $revision->setStatus(ArcanistDifferentialRevisionStatus::CLOSED);
             break;
         case DifferentialAction::ACTION_ADDREVIEWERS:
             list($added_reviewers, $ignored) = $this->alterReviewers();
             if ($added_reviewers) {
                 $key = DifferentialComment::METADATA_ADDED_REVIEWERS;
                 $metadata[$key] = $added_reviewers;
             } else {
                 $user_tried_to_add = count($this->getAddedReviewers());
                 if ($user_tried_to_add == 0) {
                     throw new DifferentialActionHasNoEffectException("You can not add reviewers, because you did not specify any " . "reviewers.");
                 } else {
                     if ($user_tried_to_add == 1) {
                         throw new DifferentialActionHasNoEffectException("You can not add that reviewer, because they are already an " . "author or reviewer.");
                     } else {
                         throw new DifferentialActionHasNoEffectException("You can not add those reviewers, because they are all already " . "authors or reviewers.");
                     }
                 }
             }
             break;
         case DifferentialAction::ACTION_ADDCCS:
             $added_ccs = $this->getAddedCCs();
             $user_tried_to_add = count($added_ccs);
             $added_ccs = $this->filterAddedCCs($added_ccs);
             if ($added_ccs) {
                 foreach ($added_ccs as $cc) {
                     DifferentialRevisionEditor::addCC($revision, $cc, $this->actorPHID);
                 }
                 $key = DifferentialComment::METADATA_ADDED_CCS;
                 $metadata[$key] = $added_ccs;
             } else {
                 if ($user_tried_to_add == 0) {
                     throw new DifferentialActionHasNoEffectException("You can not add CCs, because you did not specify any " . "CCs.");
                 } else {
                     if ($user_tried_to_add == 1) {
                         throw new DifferentialActionHasNoEffectException("You can not add that CC, because they are already an " . "author, reviewer or CC.");
                     } else {
                         throw new DifferentialActionHasNoEffectException("You can not add those CCs, because they are all already " . "authors, reviewers or CCs.");
                     }
                 }
             }
             break;
         case DifferentialAction::ACTION_CLAIM:
             if ($actor_is_author) {
                 throw new Exception("You can not commandeer your own revision.");
             }
             switch ($revision_status) {
                 case ArcanistDifferentialRevisionStatus::CLOSED:
                     throw new DifferentialActionHasNoEffectException("You can not commandeer this revision because it has " . "already been closed.");
                     break;
             }
             $this->setAddedReviewers(array($revision->getAuthorPHID()));
             $this->setRemovedReviewers(array($actor_phid));
             // NOTE: Set the new author PHID before calling addReviewers(), since it
             // doesn't permit the author to become a reviewer.
             $revision->setAuthorPHID($actor_phid);
             list($added_reviewers, $removed_reviewers) = $this->alterReviewers();
             if ($added_reviewers) {
                 $key = DifferentialComment::METADATA_ADDED_REVIEWERS;
                 $metadata[$key] = $added_reviewers;
             }
             if ($removed_reviewers) {
                 $key = DifferentialComment::METADATA_REMOVED_REVIEWERS;
                 $metadata[$key] = $removed_reviewers;
             }
             break;
         default:
             throw new Exception('Unsupported action.');
     }
     // Update information about reviewer in charge.
     if ($action == DifferentialAction::ACTION_ACCEPT || $action == DifferentialAction::ACTION_REJECT) {
         $revision->setLastReviewerPHID($actor_phid);
     }
     // TODO: Call beginReadLocking() prior to loading the revision.
     $revision->openTransaction();
     // Always save the revision (even if we didn't actually change any of its
     // properties) so that it jumps to the top of the revision list when sorted
     // by "updated". Notably, this allows "ping" comments to push it to the
     // top of the action list.
     $revision->save();
     if ($action != DifferentialAction::ACTION_RESIGN) {
         DifferentialRevisionEditor::addCC($revision, $this->actorPHID, $this->actorPHID);
     }
     $comment = id(new DifferentialComment())->setAuthorPHID($this->actorPHID)->setRevisionID($revision->getID())->setAction($action)->setContent((string) $this->message)->setMetadata($metadata);
     if ($this->contentSource) {
         $comment->setContentSource($this->contentSource);
     }
     $comment->save();
     $changesets = array();
     if ($inline_comments) {
         $load_ids = mpull($inline_comments, 'getChangesetID');
         if ($load_ids) {
             $load_ids = array_unique($load_ids);
             $changesets = id(new DifferentialChangeset())->loadAllWhere('id in (%Ld)', $load_ids);
         }
         foreach ($inline_comments as $inline) {
             $inline->setCommentID($comment->getID());
             $inline->save();
         }
     }
     // Find any "@mentions" in the comment blocks.
     $content_blocks = array($comment->getContent());
     foreach ($inline_comments as $inline) {
         $content_blocks[] = $inline->getContent();
     }
     $mention_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions($content_blocks);
     if ($mention_ccs) {
         $mention_ccs = $this->filterAddedCCs($mention_ccs);
         if ($mention_ccs) {
             $metadata = $comment->getMetadata();
             $metacc = idx($metadata, DifferentialComment::METADATA_ADDED_CCS, array());
             foreach ($mention_ccs as $cc_phid) {
                 DifferentialRevisionEditor::addCC($revision, $cc_phid, $this->actorPHID);
                 $metacc[] = $cc_phid;
             }
             $metadata[DifferentialComment::METADATA_ADDED_CCS] = $metacc;
             $comment->setMetadata($metadata);
             $comment->save();
         }
     }
     $revision->saveTransaction();
     $phids = array($this->actorPHID);
     $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     $actor_handle = $handles[$this->actorPHID];
     $xherald_header = HeraldTranscript::loadXHeraldRulesHeader($revision->getPHID());
     id(new DifferentialCommentMail($revision, $actor_handle, $comment, $changesets, $inline_comments))->setToPHIDs(array_merge($revision->getReviewers(), array($revision->getAuthorPHID())))->setCCPHIDs($revision->getCCPHIDs())->setChangedByCommit($this->getChangedByCommit())->setXHeraldRulesHeader($xherald_header)->setParentMessageID($this->parentMessageID)->send();
     $event_data = array('revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $comment->getAction(), 'feedback_content' => $comment->getContent(), 'actor_phid' => $this->actorPHID);
     id(new PhabricatorTimelineEvent('difx', $event_data))->recordEvent();
     // TODO: Move to a daemon?
     id(new PhabricatorFeedStoryPublisher())->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL)->setStoryData($event_data)->setStoryTime(time())->setStoryAuthorPHID($this->actorPHID)->setRelatedPHIDs(array($revision->getPHID(), $this->actorPHID, $revision->getAuthorPHID()))->setPrimaryObjectPHID($revision->getPHID())->setSubscribedPHIDs(array_merge(array($revision->getAuthorPHID()), $revision->getReviewers(), $revision->getCCPHIDs()))->publish();
     // TODO: Move to a daemon?
     PhabricatorSearchDifferentialIndexer::indexRevision($revision);
     return $comment;
 }
 private function buildSubscribeTransaction(PhabricatorLiskDAO $object, array $xactions, array $changes)
 {
     if (!$object instanceof PhabricatorSubscribableInterface) {
         return null;
     }
     if ($this->shouldEnableMentions($object, $xactions)) {
         // Identify newly mentioned users. We ignore users who were previously
         // mentioned so that we don't re-subscribe users after an edit of text
         // which mentions them.
         $old_texts = mpull($changes, 'getOldValue');
         $new_texts = mpull($changes, 'getNewValue');
         $old_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions($this->getActor(), $old_texts);
         $new_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions($this->getActor(), $new_texts);
         $phids = array_diff($new_phids, $old_phids);
     } else {
         $phids = array();
     }
     $this->mentionedPHIDs = $phids;
     if ($object->getPHID()) {
         // Don't try to subscribe already-subscribed mentions: we want to generate
         // a dialog about an action having no effect if the user explicitly adds
         // existing CCs, but not if they merely mention existing subscribers.
         $phids = array_diff($phids, $this->subscribers);
     }
     if ($phids) {
         $users = id(new PhabricatorPeopleQuery())->setViewer($this->getActor())->withPHIDs($phids)->execute();
         $users = mpull($users, null, 'getPHID');
         foreach ($phids as $key => $phid) {
             // Do not subscribe mentioned users
             // who do not have VIEW Permissions
             if ($object instanceof PhabricatorPolicyInterface && !PhabricatorPolicyFilter::hasCapability($users[$phid], $object, PhabricatorPolicyCapability::CAN_VIEW)) {
                 unset($phids[$key]);
             } else {
                 if ($object->isAutomaticallySubscribed($phid)) {
                     unset($phids[$key]);
                 }
             }
         }
         $phids = array_values($phids);
     }
     // No else here to properly return null should we unset all subscriber
     if (!$phids) {
         return null;
     }
     $xaction = newv(get_class(head($xactions)), array());
     $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
     $xaction->setNewValue(array('+' => $phids));
     return $xaction;
 }
 public function save()
 {
     $revision = $this->revision;
     $action = $this->action;
     $actor_phid = $this->actorPHID;
     $actor_is_author = $actor_phid == $revision->getAuthorPHID();
     $revision_status = $revision->getStatus();
     $revision->loadRelationships();
     $reviewer_phids = $revision->getReviewers();
     if ($reviewer_phids) {
         $reviewer_phids = array_combine($reviewer_phids, $reviewer_phids);
     }
     $metadata = array();
     switch ($action) {
         case DifferentialAction::ACTION_COMMENT:
             break;
         case DifferentialAction::ACTION_RESIGN:
             if ($actor_is_author) {
                 throw new Exception('You can not resign from your own revision!');
             }
             if (isset($reviewer_phids[$actor_phid])) {
                 DifferentialRevisionEditor::alterReviewers($revision, $reviewer_phids, $rem = array($actor_phid), $add = array(), $actor_phid);
             }
             break;
         case DifferentialAction::ACTION_ABANDON:
             if (!$actor_is_author) {
                 throw new Exception('You can only abandon your revisions.');
             }
             if ($revision_status == DifferentialRevisionStatus::COMMITTED) {
                 throw new Exception('You can not abandon a committed revision.');
             }
             if ($revision_status == DifferentialRevisionStatus::ABANDONED) {
                 $action = DifferentialAction::ACTION_COMMENT;
                 break;
             }
             $revision->setStatus(DifferentialRevisionStatus::ABANDONED)->save();
             break;
         case DifferentialAction::ACTION_ACCEPT:
             if ($actor_is_author) {
                 throw new Exception('You can not accept your own revision.');
             }
             if ($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW && $revision_status != DifferentialRevisionStatus::NEEDS_REVISION) {
                 $action = DifferentialAction::ACTION_COMMENT;
                 break;
             }
             $revision->setStatus(DifferentialRevisionStatus::ACCEPTED)->save();
             if (!isset($reviewer_phids[$actor_phid])) {
                 DifferentialRevisionEditor::alterReviewers($revision, $reviewer_phids, $rem = array(), $add = array($actor_phid), $actor_phid);
             }
             break;
         case DifferentialAction::ACTION_REQUEST:
             if (!$actor_is_author) {
                 throw new Exception('You must own a revision to request review.');
             }
             if ($revision_status != DifferentialRevisionStatus::NEEDS_REVISION && $revision_status != DifferentialRevisionStatus::ACCEPTED) {
                 $action = DifferentialAction::ACTION_COMMENT;
                 break;
             }
             $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW)->save();
             break;
         case DifferentialAction::ACTION_REJECT:
             if ($actor_is_author) {
                 throw new Exception('You can not request changes to your own revision.');
             }
             if ($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW && $revision_status != DifferentialRevisionStatus::ACCEPTED) {
                 $action = DifferentialAction::ACTION_COMMENT;
                 break;
             }
             if (!isset($reviewer_phids[$actor_phid])) {
                 DifferentialRevisionEditor::alterReviewers($revision, $reviewer_phids, $rem = array(), $add = array($actor_phid), $actor_phid);
             }
             $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVISION)->save();
             break;
         case DifferentialAction::ACTION_RETHINK:
             if (!$actor_is_author) {
                 throw new Exception("You can not plan changes to somebody else's revision");
             }
             if ($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW && $revision_status != DifferentialRevisionStatus::ACCEPTED) {
                 $action = DifferentialAction::ACTION_COMMENT;
                 break;
             }
             $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVISION)->save();
             break;
         case DifferentialAction::ACTION_RECLAIM:
             if (!$actor_is_author) {
                 throw new Exception('You can not reclaim a revision you do not own.');
             }
             if ($revision_status != DifferentialRevisionStatus::ABANDONED) {
                 $action = DifferentialAction::ACTION_COMMENT;
                 break;
             }
             $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW)->save();
             break;
         case DifferentialAction::ACTION_COMMIT:
             if (!$actor_is_author) {
                 throw new Exception('You can not commit a revision you do not own.');
             }
             $revision->setStatus(DifferentialRevisionStatus::COMMITTED)->save();
             break;
         case DifferentialAction::ACTION_ADDREVIEWERS:
             $added_reviewers = $this->getAddedReviewers();
             foreach ($added_reviewers as $k => $user_phid) {
                 if ($user_phid == $revision->getAuthorPHID()) {
                     unset($added_reviewers[$k]);
                 }
                 if (!empty($reviewer_phids[$user_phid])) {
                     unset($added_reviewers[$k]);
                 }
             }
             $added_reviewers = array_unique($added_reviewers);
             if ($added_reviewers) {
                 DifferentialRevisionEditor::alterReviewers($revision, $reviewer_phids, $rem = array(), $add = $added_reviewers, $actor_phid);
                 $key = DifferentialComment::METADATA_ADDED_REVIEWERS;
                 $metadata[$key] = $added_reviewers;
             } else {
                 $action = DifferentialAction::ACTION_COMMENT;
             }
             break;
         case DifferentialAction::ACTION_ADDCCS:
             $added_ccs = $this->getAddedCCs();
             $current_ccs = $revision->getCCPHIDs();
             if ($current_ccs) {
                 $current_ccs = array_fill_keys($current_ccs, true);
                 foreach ($added_ccs as $k => $cc) {
                     if (isset($current_ccs[$cc])) {
                         unset($added_ccs[$k]);
                     }
                 }
             }
             if ($added_ccs) {
                 foreach ($added_ccs as $cc) {
                     DifferentialRevisionEditor::addCC($revision, $cc, $this->actorPHID);
                 }
                 $key = DifferentialComment::METADATA_ADDED_CCS;
                 $metadata[$key] = $added_ccs;
             } else {
                 $action = DifferentialAction::ACTION_COMMENT;
             }
             break;
         default:
             throw new Exception('Unsupported action.');
     }
     if ($this->addCC) {
         DifferentialRevisionEditor::addCC($revision, $this->actorPHID, $this->actorPHID);
     }
     $inline_comments = array();
     if ($this->attachInlineComments) {
         $inline_comments = id(new DifferentialInlineComment())->loadAllWhere('authorPHID = %s AND revisionID = %d AND commentID IS NULL', $this->actorPHID, $revision->getID());
     }
     $comment = id(new DifferentialComment())->setAuthorPHID($this->actorPHID)->setRevisionID($revision->getID())->setAction($action)->setContent((string) $this->message)->setMetadata($metadata)->save();
     $changesets = array();
     if ($inline_comments) {
         $load_ids = mpull($inline_comments, 'getChangesetID');
         if ($load_ids) {
             $load_ids = array_unique($load_ids);
             $changesets = id(new DifferentialChangeset())->loadAllWhere('id in (%Ld)', $load_ids);
         }
         foreach ($inline_comments as $inline) {
             $inline->setCommentID($comment->getID());
             $inline->save();
         }
     }
     // Find any "@mentions" in the comment blocks.
     $content_blocks = array($comment->getContent());
     foreach ($inline_comments as $inline) {
         $content_blocks[] = $inline->getContent();
     }
     $mention_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions($content_blocks);
     if ($mention_ccs) {
         $current_ccs = $revision->getCCPHIDs();
         if ($current_ccs) {
             $current_ccs = array_fill_keys($current_ccs, true);
             foreach ($mention_ccs as $key => $mention_cc) {
                 if (isset($current_ccs[$mention_cc])) {
                     unset($mention_ccs);
                 }
             }
         }
         if ($mention_ccs) {
             $metadata = $comment->getMetadata();
             $metacc = idx($metadata, DifferentialComment::METADATA_ADDED_CCS, array());
             foreach ($mention_ccs as $cc_phid) {
                 DifferentialRevisionEditor::addCC($revision, $cc_phid, $this->actorPHID);
                 $metacc[] = $cc_phid;
             }
             $metadata[DifferentialComment::METADATA_ADDED_CCS] = $metacc;
             $comment->setMetadata($metadata);
             $comment->save();
         }
     }
     $phids = array($this->actorPHID);
     $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     $actor_handle = $handles[$this->actorPHID];
     $xherald_header = HeraldTranscript::loadXHeraldRulesHeader($revision->getPHID());
     id(new DifferentialCommentMail($revision, $actor_handle, $comment, $changesets, $inline_comments))->setToPHIDs(array_merge($revision->getReviewers(), array($revision->getAuthorPHID())))->setCCPHIDs($revision->getCCPHIDs())->setChangedByCommit($this->getChangedByCommit())->setXHeraldRulesHeader($xherald_header)->setParentMessageID($this->parentMessageID)->send();
     $event_data = array('revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $comment->getAction(), 'feedback_content' => $comment->getContent(), 'actor_phid' => $this->actorPHID);
     id(new PhabricatorTimelineEvent('difx', $event_data))->recordEvent();
     // TODO: Move to a daemon?
     id(new PhabricatorFeedStoryPublisher())->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL)->setStoryData($event_data)->setStoryTime(time())->setStoryAuthorPHID($this->actorPHID)->setRelatedPHIDs(array($revision->getPHID(), $this->actorPHID, $revision->getAuthorPHID()))->publish();
     // TODO: Move to a daemon?
     PhabricatorSearchDifferentialIndexer::indexRevision($revision);
     return $comment;
 }
 public function addComment(PhabricatorAuditComment $comment)
 {
     $commit = $this->commit;
     $user = $this->user;
     $other_comments = id(new PhabricatorAuditComment())->loadAllWhere('targetPHID = %s', $commit->getPHID());
     $inline_comments = array();
     if ($this->attachInlineComments) {
         $inline_comments = id(new PhabricatorAuditInlineComment())->loadAllWhere('authorPHID = %s AND commitPHID = %s
       AND auditCommentID IS NULL', $user->getPHID(), $commit->getPHID());
     }
     $comment->setActorPHID($user->getPHID())->setTargetPHID($commit->getPHID())->save();
     $content_blocks = array($comment->getContent());
     if ($inline_comments) {
         foreach ($inline_comments as $inline) {
             $inline->setAuditCommentID($comment->getID());
             $inline->save();
             $content_blocks[] = $inline->getContent();
         }
     }
     $ccs = $this->ccs;
     $auditors = $this->auditors;
     $metadata = $comment->getMetadata();
     $metacc = array();
     // Find any "@mentions" in the content blocks.
     $mention_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions($content_blocks);
     if ($mention_ccs) {
         $metacc = idx($metadata, PhabricatorAuditComment::METADATA_ADDED_CCS, array());
         foreach ($mention_ccs as $cc_phid) {
             $metacc[] = $cc_phid;
         }
     }
     if ($metacc) {
         $ccs = array_merge($ccs, $metacc);
     }
     // When a user submits an audit comment, we update all the audit requests
     // they have authority over to reflect the most recent status. The general
     // idea here is that if audit has triggered for, e.g., several packages, but
     // a user owns all of them, they can clear the audit requirement in one go
     // without auditing the commit for each trigger.
     $audit_phids = self::loadAuditPHIDsForUser($this->user);
     $audit_phids = array_fill_keys($audit_phids, true);
     $requests = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere('commitPHID = %s', $commit->getPHID());
     $action = $comment->getAction();
     // TODO: We should validate the action, currently we allow anyone to, e.g.,
     // close an audit if they muck with form parameters. I'll followup with this
     // and handle the no-effect cases (e.g., closing and already-closed audit).
     $user_is_author = $user->getPHID() == $commit->getAuthorPHID();
     if ($action == PhabricatorAuditActionConstants::CLOSE) {
         // "Close" means wipe out all the concerns.
         $concerned_status = PhabricatorAuditStatusConstants::CONCERNED;
         foreach ($requests as $request) {
             if ($request->getAuditStatus() == $concerned_status) {
                 $request->setAuditStatus(PhabricatorAuditStatusConstants::CLOSED);
                 $request->save();
             }
         }
     } else {
         if ($action == PhabricatorAuditActionConstants::RESIGN) {
             // "Resign" has unusual rules for writing user rows, only affects the
             // user row (never package/project rows), and always affects the user
             // row (other actions don't, if they were able to affect a package/project
             // row).
             $user_request = null;
             foreach ($requests as $request) {
                 if ($request->getAuditorPHID() == $user->getPHID()) {
                     $user_request = $request;
                     break;
                 }
             }
             if (!$user_request) {
                 $user_request = id(new PhabricatorRepositoryAuditRequest())->setCommitPHID($commit->getPHID())->setAuditorPHID($user->getPHID())->setAuditReasons(array("Resigned"));
             }
             $user_request->setAuditStatus(PhabricatorAuditStatusConstants::RESIGNED)->save();
             $requests[] = $user_request;
         } else {
             $have_any_requests = false;
             foreach ($requests as $request) {
                 if (empty($audit_phids[$request->getAuditorPHID()])) {
                     continue;
                 }
                 $request_is_for_user = $request->getAuditorPHID() == $user->getPHID();
                 $have_any_requests = true;
                 $new_status = null;
                 switch ($action) {
                     case PhabricatorAuditActionConstants::COMMENT:
                     case PhabricatorAuditActionConstants::ADD_CCS:
                     case PhabricatorAuditActionConstants::ADD_AUDITORS:
                         // Commenting or adding cc's/auditors doesn't change status.
                         break;
                     case PhabricatorAuditActionConstants::ACCEPT:
                         if (!$user_is_author || $request_is_for_user) {
                             // When modifying your own commits, you act only on behalf of
                             // yourself, not your packages/projects -- the idea being that
                             // you can't accept your own commits.
                             $new_status = PhabricatorAuditStatusConstants::ACCEPTED;
                         }
                         break;
                     case PhabricatorAuditActionConstants::CONCERN:
                         if (!$user_is_author || $request_is_for_user) {
                             // See above.
                             $new_status = PhabricatorAuditStatusConstants::CONCERNED;
                         }
                         break;
                     default:
                         throw new Exception("Unknown action '{$action}'!");
                 }
                 if ($new_status !== null) {
                     $request->setAuditStatus($new_status);
                     $request->save();
                 }
             }
             // If the user has no current authority over any audit trigger, make a
             // new one to represent their audit state.
             if (!$have_any_requests) {
                 $new_status = null;
                 switch ($action) {
                     case PhabricatorAuditActionConstants::COMMENT:
                     case PhabricatorAuditActionConstants::ADD_CCS:
                     case PhabricatorAuditActionConstants::ADD_AUDITORS:
                         $new_status = PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED;
                         break;
                     case PhabricatorAuditActionConstants::ACCEPT:
                         $new_status = PhabricatorAuditStatusConstants::ACCEPTED;
                         break;
                     case PhabricatorAuditActionConstants::CONCERN:
                         $new_status = PhabricatorAuditStatusConstants::CONCERNED;
                         break;
                     case PhabricatorAuditActionConstants::CLOSE:
                         // Impossible to reach this block with 'close'.
                     // Impossible to reach this block with 'close'.
                     default:
                         throw new Exception("Unknown or invalid action '{$action}'!");
                 }
                 $request = id(new PhabricatorRepositoryAuditRequest())->setCommitPHID($commit->getPHID())->setAuditorPHID($user->getPHID())->setAuditStatus($new_status)->setAuditReasons(array("Voluntary Participant"))->save();
                 $requests[] = $request;
             }
         }
     }
     $requests_by_auditor = mpull($requests, null, 'getAuditorPHID');
     $requests_phids = array_keys($requests_by_auditor);
     $ccs = array_diff($ccs, $requests_phids);
     $auditors = array_diff($auditors, $requests_phids);
     if ($action == PhabricatorAuditActionConstants::ADD_CCS) {
         if ($ccs) {
             $metadata[PhabricatorAuditComment::METADATA_ADDED_CCS] = $ccs;
             $comment->setMetaData($metadata);
         } else {
             $comment->setAction(PhabricatorAuditActionConstants::COMMENT);
         }
     }
     if ($action == PhabricatorAuditActionConstants::ADD_AUDITORS) {
         if ($auditors) {
             $metadata[PhabricatorAuditComment::METADATA_ADDED_AUDITORS] = $auditors;
             $comment->setMetaData($metadata);
         } else {
             $comment->setAction(PhabricatorAuditActionConstants::COMMENT);
         }
     }
     $comment->save();
     if ($auditors) {
         foreach ($auditors as $auditor_phid) {
             $audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED;
             $requests[] = id(new PhabricatorRepositoryAuditRequest())->setCommitPHID($commit->getPHID())->setAuditorPHID($auditor_phid)->setAuditStatus($audit_requested)->setAuditReasons(array('Added by ' . $user->getUsername()))->save();
         }
     }
     if ($ccs) {
         foreach ($ccs as $cc_phid) {
             $audit_cc = PhabricatorAuditStatusConstants::CC;
             $requests[] = id(new PhabricatorRepositoryAuditRequest())->setCommitPHID($commit->getPHID())->setAuditorPHID($cc_phid)->setAuditStatus($audit_cc)->setAuditReasons(array('Added by ' . $user->getUsername()))->save();
         }
     }
     $commit->updateAuditStatus($requests);
     $commit->save();
     $this->publishFeedStory($comment, array_keys($audit_phids));
     PhabricatorSearchCommitIndexer::indexCommit($commit);
     $this->sendMail($comment, $other_comments, $inline_comments, $requests);
 }
 public function processRequest()
 {
     $request = $this->getRequest();
     $user = $request->getUser();
     $task = id(new ManiphestTaskQuery())->setViewer($user)->withIDs(array($request->getStr('taskID')))->executeOne();
     if (!$task) {
         return new Aphront404Response();
     }
     $task_uri = '/' . $task->getMonogram();
     $transactions = array();
     $action = $request->getStr('action');
     // Compute new CCs added by @mentions. Several things can cause CCs to
     // be added as side effects: mentions, explicit CCs, users who aren't
     // CC'd interacting with the task, and ownership changes. We build up a
     // list of all the CCs and then construct a transaction for them at the
     // end if necessary.
     $added_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions($user, array($request->getStr('comments')));
     $cc_transaction = new ManiphestTransaction();
     $cc_transaction->setTransactionType(ManiphestTransaction::TYPE_CCS);
     $transaction = new ManiphestTransaction();
     $transaction->setTransactionType($action);
     switch ($action) {
         case ManiphestTransaction::TYPE_STATUS:
             $transaction->setNewValue($request->getStr('resolution'));
             break;
         case ManiphestTransaction::TYPE_OWNER:
             $assign_to = $request->getArr('assign_to');
             $assign_to = reset($assign_to);
             $transaction->setNewValue($assign_to);
             break;
         case ManiphestTransaction::TYPE_PROJECTS:
             $projects = $request->getArr('projects');
             $projects = array_merge($projects, $task->getProjectPHIDs());
             $projects = array_filter($projects);
             $projects = array_unique($projects);
             // TODO: Bleh.
             $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
             $transaction->setTransactionType(PhabricatorTransactions::TYPE_EDGE)->setMetadataValue('edge:type', $project_type)->setNewValue(array('+' => array_fuse($projects)));
             break;
         case ManiphestTransaction::TYPE_CCS:
             // Accumulate the new explicit CCs into the array that we'll add in
             // the CC transaction later.
             $added_ccs = array_merge($added_ccs, $request->getArr('ccs'));
             // Throw away the primary transaction.
             $transaction = null;
             break;
         case ManiphestTransaction::TYPE_PRIORITY:
             $transaction->setNewValue($request->getInt('priority'));
             break;
         case PhabricatorTransactions::TYPE_COMMENT:
             // Nuke this, we're going to create it below.
             $transaction = null;
             break;
         default:
             throw new Exception('unknown action');
     }
     if ($transaction) {
         $transactions[] = $transaction;
     }
     // When you interact with a task, we add you to the CC list so you get
     // further updates, and possibly assign the task to you if you took an
     // ownership action (closing it) but it's currently unowned. We also move
     // previous owners to CC if ownership changes. Detect all these conditions
     // and create side-effect transactions for them.
     $implicitly_claimed = false;
     if ($action == ManiphestTransaction::TYPE_OWNER) {
         if ($task->getOwnerPHID() == $transaction->getNewValue()) {
             // If this is actually no-op, don't generate the side effect.
         } else {
             // Otherwise, when a task is reassigned, move the previous owner to CC.
             $added_ccs[] = $task->getOwnerPHID();
         }
     }
     if ($action == ManiphestTransaction::TYPE_STATUS) {
         $resolution = $request->getStr('resolution');
         if (!$task->getOwnerPHID() && ManiphestTaskStatus::isClosedStatus($resolution)) {
             // Closing an unassigned task. Assign the user as the owner of
             // this task.
             $assign = new ManiphestTransaction();
             $assign->setTransactionType(ManiphestTransaction::TYPE_OWNER);
             $assign->setNewValue($user->getPHID());
             $transactions[] = $assign;
             $implicitly_claimed = true;
         }
     }
     $user_owns_task = false;
     if ($implicitly_claimed) {
         $user_owns_task = true;
     } else {
         if ($action == ManiphestTransaction::TYPE_OWNER) {
             if ($transaction->getNewValue() == $user->getPHID()) {
                 $user_owns_task = true;
             }
         } else {
             if ($task->getOwnerPHID() == $user->getPHID()) {
                 $user_owns_task = true;
             }
         }
     }
     if (!$user_owns_task) {
         // If we aren't making the user the new task owner and they aren't the
         // existing task owner, add them to CC unless they're aleady CC'd.
         if (!in_array($user->getPHID(), $task->getCCPHIDs())) {
             $added_ccs[] = $user->getPHID();
         }
     }
     // Evade no-effect detection in the new editor stuff until we can switch
     // to subscriptions.
     $added_ccs = array_filter(array_diff($added_ccs, $task->getCCPHIDs()));
     if ($added_ccs) {
         // We've added CCs, so include a CC transaction.
         $all_ccs = array_merge($task->getCCPHIDs(), $added_ccs);
         $cc_transaction->setNewValue($all_ccs);
         $transactions[] = $cc_transaction;
     }
     $comments = $request->getStr('comments');
     if (strlen($comments) || !$transactions) {
         $transactions[] = id(new ManiphestTransaction())->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)->attachComment(id(new ManiphestTransactionComment())->setContent($comments));
     }
     $event = new PhabricatorEvent(PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array('task' => $task, 'new' => false, 'transactions' => $transactions));
     $event->setUser($user);
     $event->setAphrontRequest($request);
     PhutilEventEngine::dispatchEvent($event);
     $task = $event->getValue('task');
     $transactions = $event->getValue('transactions');
     $editor = id(new ManiphestTransactionEditor())->setActor($user)->setContentSourceFromRequest($request)->setContinueOnMissingFields(true)->setContinueOnNoEffect($request->isContinueRequest());
     try {
         $editor->applyTransactions($task, $transactions);
     } catch (PhabricatorApplicationTransactionNoEffectException $ex) {
         return id(new PhabricatorApplicationTransactionNoEffectResponse())->setCancelURI($task_uri)->setException($ex);
     }
     $draft = id(new PhabricatorDraft())->loadOneWhere('authorPHID = %s AND draftKey = %s', $user->getPHID(), $task->getPHID());
     if ($draft) {
         $draft->delete();
     }
     $event = new PhabricatorEvent(PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array('task' => $task, 'new' => false, 'transactions' => $transactions));
     $event->setUser($user);
     $event->setAphrontRequest($request);
     PhutilEventEngine::dispatchEvent($event);
     return id(new AphrontRedirectResponse())->setURI($task_uri);
 }
 public function processRequest()
 {
     $request = $this->getRequest();
     $user = $request->getUser();
     $task = id(new ManiphestTask())->load($request->getStr('taskID'));
     if (!$task) {
         return new Aphront404Response();
     }
     $transactions = array();
     $action = $request->getStr('action');
     // If we have drag-and-dropped files, attach them first in a separate
     // transaction. These can come in on any transaction type, which is why we
     // handle them separately.
     $files = array();
     // Look for drag-and-drop uploads first.
     $file_phids = $request->getArr('files');
     if ($file_phids) {
         $files = id(new PhabricatorFile())->loadAllWhere('phid in (%Ls)', $file_phids);
     }
     // This means "attach a file" even though we store other types of data
     // as 'attached'.
     if ($action == ManiphestTransactionType::TYPE_ATTACH) {
         if (!empty($_FILES['file'])) {
             $err = idx($_FILES['file'], 'error');
             if ($err != UPLOAD_ERR_NO_FILE) {
                 $file = PhabricatorFile::newFromPHPUpload($_FILES['file'], array('authorPHID' => $user->getPHID()));
                 $files[] = $file;
             }
         }
     }
     // If we had explicit or drag-and-drop files, create a transaction
     // for those before we deal with whatever else might have happened.
     $file_transaction = null;
     if ($files) {
         $files = mpull($files, 'getPHID', 'getPHID');
         $new = $task->getAttached();
         foreach ($files as $phid) {
             if (empty($new[PhabricatorPHIDConstants::PHID_TYPE_FILE])) {
                 $new[PhabricatorPHIDConstants::PHID_TYPE_FILE] = array();
             }
             $new[PhabricatorPHIDConstants::PHID_TYPE_FILE][$phid] = array();
         }
         $transaction = new ManiphestTransaction();
         $transaction->setAuthorPHID($user->getPHID())->setTransactionType(ManiphestTransactionType::TYPE_ATTACH);
         $transaction->setNewValue($new);
         $transactions[] = $transaction;
         $file_transaction = $transaction;
     }
     // Compute new CCs added by @mentions. Several things can cause CCs to
     // be added as side effects: mentions, explicit CCs, users who aren't
     // CC'd interacting with the task, and ownership changes. We build up a
     // list of all the CCs and then construct a transaction for them at the
     // end if necessary.
     $added_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions(array($request->getStr('comments')));
     $cc_transaction = new ManiphestTransaction();
     $cc_transaction->setAuthorPHID($user->getPHID())->setTransactionType(ManiphestTransactionType::TYPE_CCS);
     $force_cc_transaction = false;
     $transaction = new ManiphestTransaction();
     $transaction->setAuthorPHID($user->getPHID())->setComments($request->getStr('comments'))->setTransactionType($action);
     switch ($action) {
         case ManiphestTransactionType::TYPE_STATUS:
             $transaction->setNewValue($request->getStr('resolution'));
             break;
         case ManiphestTransactionType::TYPE_OWNER:
             $assign_to = $request->getArr('assign_to');
             $assign_to = reset($assign_to);
             $transaction->setNewValue($assign_to);
             break;
         case ManiphestTransactionType::TYPE_PROJECTS:
             $projects = $request->getArr('projects');
             $projects = array_merge($projects, $task->getProjectPHIDs());
             $projects = array_filter($projects);
             $projects = array_unique($projects);
             $transaction->setNewValue($projects);
             break;
         case ManiphestTransactionType::TYPE_CCS:
             // Accumulate the new explicit CCs into the array that we'll add in
             // the CC transaction later.
             $added_ccs = array_merge($added_ccs, $request->getArr('ccs'));
             // Transfer any comments over to the CC transaction.
             $cc_transaction->setComments($transaction->getComments());
             // Make sure we include this transaction, even if the user didn't
             // actually add any CC's, because we'll discard their comment otherwise.
             $force_cc_transaction = true;
             // Throw away the primary transaction.
             $transaction = null;
             break;
         case ManiphestTransactionType::TYPE_PRIORITY:
             $transaction->setNewValue($request->getInt('priority'));
             break;
         case ManiphestTransactionType::TYPE_NONE:
         case ManiphestTransactionType::TYPE_ATTACH:
             // If we have a file transaction, just get rid of this secondary
             // transaction and put the comments on it instead.
             if ($file_transaction) {
                 $file_transaction->setComments($transaction->getComments());
                 $transaction = null;
             }
             break;
         default:
             throw new Exception('unknown action');
     }
     if ($transaction) {
         $transactions[] = $transaction;
     }
     // When you interact with a task, we add you to the CC list so you get
     // further updates, and possibly assign the task to you if you took an
     // ownership action (closing it) but it's currently unowned. We also move
     // previous owners to CC if ownership changes. Detect all these conditions
     // and create side-effect transactions for them.
     $implicitly_claimed = false;
     switch ($action) {
         case ManiphestTransactionType::TYPE_OWNER:
             if ($task->getOwnerPHID() == $transaction->getNewValue()) {
                 // If this is actually no-op, don't generate the side effect.
                 break;
             }
             // Otherwise, when a task is reassigned, move the previous owner to CC.
             $added_ccs[] = $task->getOwnerPHID();
             break;
         case ManiphestTransactionType::TYPE_STATUS:
             if (!$task->getOwnerPHID() && $request->getStr('resolution') != ManiphestTaskStatus::STATUS_OPEN) {
                 // Closing an unassigned task. Assign the user as the owner of
                 // this task.
                 $assign = new ManiphestTransaction();
                 $assign->setAuthorPHID($user->getPHID());
                 $assign->setTransactionType(ManiphestTransactionType::TYPE_OWNER);
                 $assign->setNewValue($user->getPHID());
                 $transactions[] = $assign;
                 $implicitly_claimed = true;
             }
             break;
     }
     $user_owns_task = false;
     if ($implicitly_claimed) {
         $user_owns_task = true;
     } else {
         if ($action == ManiphestTransactionType::TYPE_OWNER) {
             if ($transaction->getNewValue() == $user->getPHID()) {
                 $user_owns_task = true;
             }
         } else {
             if ($task->getOwnerPHID() == $user->getPHID()) {
                 $user_owns_task = true;
             }
         }
     }
     if (!$user_owns_task) {
         // If we aren't making the user the new task owner and they aren't the
         // existing task owner, add them to CC.
         $added_ccs[] = $user->getPHID();
     }
     if ($added_ccs || $force_cc_transaction) {
         // We've added CCs, so include a CC transaction. It's safe to do this even
         // if we're just "adding" CCs which already exist, because the
         // ManiphestTransactionEditor is smart enough to ignore them.
         $all_ccs = array_merge($task->getCCPHIDs(), $added_ccs);
         $cc_transaction->setNewValue($all_ccs);
         $transactions[] = $cc_transaction;
     }
     $content_source = PhabricatorContentSource::newForSource(PhabricatorContentSource::SOURCE_WEB, array('ip' => $request->getRemoteAddr()));
     foreach ($transactions as $transaction) {
         $transaction->setContentSource($content_source);
     }
     $editor = new ManiphestTransactionEditor();
     $editor->applyTransactions($task, $transactions);
     $draft = id(new PhabricatorDraft())->loadOneWhere('authorPHID = %s AND draftKey = %s', $user->getPHID(), $task->getPHID());
     if ($draft) {
         $draft->delete();
     }
     return id(new AphrontRedirectResponse())->setURI('/T' . $task->getID());
 }