diff --git a/src/applications/differential/editor/DifferentialCommentEditor.php b/src/applications/differential/editor/DifferentialCommentEditor.php index 6c9f75f845..8dac959187 100644 --- a/src/applications/differential/editor/DifferentialCommentEditor.php +++ b/src/applications/differential/editor/DifferentialCommentEditor.php @@ -1,771 +1,795 @@ revision = $revision; $this->action = $action; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function setMessage($message) { $this->message = $message; return $this; } public function setAttachInlineComments($attach) { $this->attachInlineComments = $attach; return $this; } public function setChangedByCommit($changed_by_commit) { $this->changedByCommit = $changed_by_commit; return $this; } public function getChangedByCommit() { return $this->changedByCommit; } public function setAddedReviewers(array $added_reviewers) { $this->addedReviewers = $added_reviewers; return $this; } public function getAddedReviewers() { return $this->addedReviewers; } public function setRemovedReviewers(array $removeded_reviewers) { $this->removedReviewers = $removeded_reviewers; return $this; } public function getRemovedReviewers() { return $this->removedReviewers; } public function setAddedCCs($added_ccs) { $this->addedCCs = $added_ccs; return $this; } public function getAddedCCs() { return $this->addedCCs; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function setIsDaemonWorkflow($is_daemon) { $this->isDaemonWorkflow = $is_daemon; return $this; } public function setNoEmail($no_email) { $this->noEmail = $no_email; return $this; } public function save() { $actor = $this->requireActor(); // Reload the revision to pick up reviewer status, until we can lift this // out of here. $this->revision = id(new DifferentialRevisionQuery()) ->setViewer($actor) ->withIDs(array($this->revision->getID())) ->needRelationships(true) ->needReviewerStatus(true) ->needReviewerAuthority(true) ->executeOne(); $revision = $this->revision; $action = $this->action; $actor_phid = $actor->getPHID(); $actor_is_author = ($actor_phid == $revision->getAuthorPHID()); $allow_self_accept = PhabricatorEnv::getEnvConfig( 'differential.allow-self-accept'); $always_allow_close = PhabricatorEnv::getEnvConfig( 'differential.always-allow-close'); $allow_reopen = PhabricatorEnv::getEnvConfig( 'differential.allow-reopen'); $revision_status = $revision->getStatus(); $update_accepted_status = false; $reviewer_phids = $revision->getReviewers(); if ($reviewer_phids) { $reviewer_phids = array_fuse($reviewer_phids); } $metadata = array(); $inline_comments = array(); if ($this->attachInlineComments) { $inline_comments = id(new DifferentialInlineCommentQuery()) ->withDraftComments($actor_phid, $revision->getID()) ->execute(); } 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."); } // If the actor is a reviewer, and their status is "added" (that is, // they haven't accepted or requested changes to the revision), // upgrade their status to "commented". If they have a stronger status // already, don't overwrite it. if (isset($reviewer_phids[$actor_phid])) { $status_added = DifferentialReviewerStatus::STATUS_ADDED; $reviewer_status = $revision->getReviewerStatus(); foreach ($reviewer_status as $reviewer) { if ($reviewer->getReviewerPHID() == $actor_phid) { if ($reviewer->getStatus() == $status_added) { DifferentialRevisionEditor::updateReviewerStatus( $revision, $actor, $actor_phid, DifferentialReviewerStatus::STATUS_COMMENTED); } } } } 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."); } list($added_reviewers, $ignored) = $this->alterReviewers(); if ($added_reviewers) { $key = DifferentialComment::METADATA_ADDED_REVIEWERS; $metadata[$key] = $added_reviewers; } DifferentialRevisionEditor::updateReviewers( $revision, $actor, array(), array($actor_phid)); // If you are a blocking reviewer, your presence as a reviewer may be // the only thing keeping a revision from transitioning to "accepted". // Recalculate state after removing the resigning reviewer. switch ($revision_status) { case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: $update_accepted_status = true; break; } 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.'); } switch ($revision_status) { 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."); case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::ACCEPTED: // We expect "Accept" from these states. break; default: throw new Exception( "Unexpected revision state '{$revision_status}'!"); } $was_reviewer_already = false; foreach ($revision->getReviewerStatus() as $reviewer) { if ($reviewer->hasAuthority($actor)) { DifferentialRevisionEditor::updateReviewerStatus( $revision, $actor, $reviewer->getReviewerPHID(), DifferentialReviewerStatus::STATUS_ACCEPTED); if ($reviewer->getReviewerPHID() == $actor_phid) { $was_reviewer_already = true; } } } if (!$was_reviewer_already) { DifferentialRevisionEditor::updateReviewerStatus( $revision, $actor, $actor_phid, DifferentialReviewerStatus::STATUS_ACCEPTED); } $update_accepted_status = true; 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: // We expect rejects from these states. 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}'!"); } DifferentialRevisionEditor::updateReviewerStatus( $revision, $actor, $actor_phid, DifferentialReviewerStatus::STATUS_REJECTED); $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: // We expect accepts from these states. 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); $update_accepted_status = true; 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 && !$always_allow_close) { 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_REOPEN: if (!$allow_reopen) { throw new Exception( "You cannot reopen a revision when this action is disabled."); } if ($revision_status != ArcanistDifferentialRevisionStatus::CLOSED) { throw new Exception( "You cannot reopen a revision that is not currently closed."); } $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); 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, - $actor_phid); - } - $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 ($update_accepted_status) { $revision = DifferentialRevisionEditor::updateAcceptedStatus( $actor, $revision); } if ($action != DifferentialAction::ACTION_RESIGN) { DifferentialRevisionEditor::addCC( $revision, $actor_phid, $actor_phid); } $is_new = !$revision->getID(); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_DIFFERENTIAL_WILLEDITREVISION, array( 'revision' => $revision, 'new' => $is_new, )); $event->setUser($actor); PhutilEventEngine::dispatchEvent($event); - $comment = id(new DifferentialComment()) + $template = id(new DifferentialComment()) ->setAuthorPHID($actor_phid) - ->setRevision($revision) - ->setAction($action) - ->setContent((string)$this->message) - ->setMetadata($metadata); + ->setRevision($revision); if ($this->contentSource) { - $comment->setContentSource($this->contentSource); + $template->setContentSource($this->contentSource); } - $comment->save(); + $comments = array(); - $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(); - } + // If this edit performs a meaningful action, save a transaction for the + // action. Do this first, since the mail currently assumes the first + // transaction is the strongest. + if ($action != DifferentialAction::ACTION_COMMENT && + $action != DifferentialAction::ACTION_ADDREVIEWERS && + $action != DifferentialAction::ACTION_ADDCCS) { + $comments[] = id(clone $template) + ->setAction($action); } - // Find any "@mentions" in the comment blocks. - $content_blocks = array($comment->getContent()); + // If this edit adds reviewers, save a transaction for those changes. + $rev_add = DifferentialComment::METADATA_ADDED_REVIEWERS; + $rev_rem = DifferentialComment::METADATA_REMOVED_REVIEWERS; + if (idx($metadata, $rev_add) || idx($metadata, $rev_rem)) { + $reviewer_meta = array_select_keys($metadata, array($rev_add, $rev_rem)); + + $comments[] = id(clone $template) + ->setAction(DifferentialAction::ACTION_ADDREVIEWERS) + ->setMetadata($reviewer_meta); + } + + // If this edit adds CCs, save a transaction for the new CCs. We build this + // for either explicit CCs, or for @mentions. First, find any "@mentions" in + // the comment blocks. + $content_blocks = array($this->message); 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) { + + // Now, build a comment if we have explicit action CCs or mention CCs. + $cc_add = DifferentialComment::METADATA_ADDED_CCS; + if (idx($metadata, $cc_add) || $mention_ccs) { + $all_adds = array_merge( + idx($metadata, $cc_add, array()), + $mention_ccs); + $all_adds = $this->filterAddedCCs($all_adds); + if ($all_adds) { + $cc_meta = array( + DifferentialComment::METADATA_ADDED_CCS => $all_adds, + ); + + foreach ($all_adds as $cc_phid) { DifferentialRevisionEditor::addCC( $revision, $cc_phid, $actor_phid); - $metacc[] = $cc_phid; } - $metadata[DifferentialComment::METADATA_ADDED_CCS] = $metacc; - $comment->setMetadata($metadata); - $comment->save(); + $comments[] = id(clone $template) + ->setAction(DifferentialAction::ACTION_ADDCCS) + ->setMetadata($cc_meta); + } + } + + // If this edit has comments or inline comments, save a transaction for + // the comment content. + if (strlen($this->message) || $inline_comments) { + $comments[] = id(clone $template) + ->setAction(DifferentialAction::ACTION_COMMENT) + ->setContent((string)$this->message); + } + + foreach ($comments as $comment) { + $comment->save(); + } + + $last_comment = last($comments); - $event = new PhabricatorEvent( - PhabricatorEventType::TYPE_DIFFERENTIAL_DIDEDITREVISION, - array( - 'revision' => $revision, - 'new' => $is_new, - )); - $event->setUser($actor); - PhutilEventEngine::dispatchEvent($event); + $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) { + // For now, attach inlines to the last comment. We'll eventually give + // them their own transactions, but this would be fairly gross during + // the storage transition and we'll have to do special thing with these + // during migration anyway. + $inline->setCommentID($last_comment->getID()); + $inline->save(); } } $revision->saveTransaction(); $phids = array($actor_phid); $handles = id(new PhabricatorHandleQuery()) ->setViewer($actor) ->withPHIDs($phids) ->execute(); $actor_handle = $handles[$actor_phid]; $xherald_header = HeraldTranscript::loadXHeraldRulesHeader( $revision->getPHID()); $mailed_phids = array(); if (!$this->noEmail) { $mail = id(new DifferentialCommentMail( $revision, $actor_handle, - array($comment), + $comments, $changesets, $inline_comments)) ->setActor($actor) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setToPHIDs( array_merge( $revision->getReviewers(), array($revision->getAuthorPHID()))) ->setCCPHIDs($revision->getCCPHIDs()) ->setChangedByCommit($this->getChangedByCommit()) ->setXHeraldRulesHeader($xherald_header) ->setParentMessageID($this->parentMessageID) ->send(); $mailed_phids = $mail->getRawMail()->buildRecipientList(); } + $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(), + 'action' => head($comments)->getAction(), + 'feedback_content' => $this->message, 'actor_phid' => $actor_phid, // NOTE: Don't use this, it will be removed after ApplicationTransactions. // For now, it powers inline comment rendering over the Asana brdige. - 'temporaryCommentID' => $comment->getID(), + 'temporaryCommentID' => $last_comment->getID(), ); id(new PhabricatorFeedStoryPublisher()) ->setStoryType('PhabricatorFeedStoryDifferential') ->setStoryData($event_data) ->setStoryTime(time()) ->setStoryAuthorPHID($actor_phid) ->setRelatedPHIDs( array( $revision->getPHID(), $actor_phid, $revision->getAuthorPHID(), )) ->setPrimaryObjectPHID($revision->getPHID()) ->setSubscribedPHIDs( array_merge( array($revision->getAuthorPHID()), $revision->getReviewers(), $revision->getCCPHIDs())) ->setMailRecipientPHIDs($mailed_phids) ->publish(); id(new PhabricatorSearchIndexer()) ->queueDocumentForIndexing($revision->getPHID()); - - return $comment; } private function filterAddedCCs(array $ccs) { $revision = $this->revision; $current_ccs = $revision->getCCPHIDs(); $current_ccs = array_fill_keys($current_ccs, true); $reviewer_phids = $revision->getReviewers(); $reviewer_phids = array_fill_keys($reviewer_phids, true); foreach ($ccs as $key => $cc) { if (isset($current_ccs[$cc])) { unset($ccs[$key]); } if (isset($reviewer_phids[$cc])) { unset($ccs[$key]); } if ($cc == $revision->getAuthorPHID()) { unset($ccs[$key]); } } return $ccs; } private function alterReviewers() { $actor_phid = $this->getActor()->getPHID(); $revision = $this->revision; $added_reviewers = $this->getAddedReviewers(); $removed_reviewers = $this->getRemovedReviewers(); $reviewer_phids = $revision->getReviewers(); $allow_self_accept = PhabricatorEnv::getEnvConfig( 'differential.allow-self-accept'); $reviewer_phids_map = array_fill_keys($reviewer_phids, true); foreach ($added_reviewers as $k => $user_phid) { if (!$allow_self_accept && $user_phid == $revision->getAuthorPHID()) { unset($added_reviewers[$k]); } if (isset($reviewer_phids_map[$user_phid])) { unset($added_reviewers[$k]); } } foreach ($removed_reviewers as $k => $user_phid) { if (!isset($reviewer_phids_map[$user_phid])) { unset($removed_reviewers[$k]); } } $added_reviewers = array_unique($added_reviewers); $removed_reviewers = array_unique($removed_reviewers); if ($added_reviewers) { DifferentialRevisionEditor::updateReviewers( $revision, $this->getActor(), $added_reviewers, $removed_reviewers); } return array($added_reviewers, $removed_reviewers); } } diff --git a/src/applications/differential/mail/DifferentialReplyHandler.php b/src/applications/differential/mail/DifferentialReplyHandler.php index aa8d09e5e6..037a7da19a 100644 --- a/src/applications/differential/mail/DifferentialReplyHandler.php +++ b/src/applications/differential/mail/DifferentialReplyHandler.php @@ -1,180 +1,177 @@ getDefaultPrivateReplyHandlerEmailAddress($handle, 'D'); } public function getPublicReplyHandlerEmailAddress() { return $this->getDefaultPublicReplyHandlerEmailAddress('D'); } public function getReplyHandlerDomain() { return PhabricatorEnv::getEnvConfig( 'metamta.differential.reply-handler-domain'); } /* * Generate text like the following from the supported commands. * " * * ACTIONS * Reply to comment, or !accept, !reject, !abandon, !resign, !reclaim. * * " */ public function getReplyHandlerInstructions() { if (!$this->supportsReplies()) { return null; } $supported_commands = $this->getSupportedCommands(); $text = ''; if (empty($supported_commands)) { return $text; } $comment_command_printed = false; if (in_array(DifferentialAction::ACTION_COMMENT, $supported_commands)) { $text .= pht('Reply to comment'); $comment_command_printed = true; $supported_commands = array_diff( $supported_commands, array(DifferentialAction::ACTION_COMMENT)); } if (!empty($supported_commands)) { if ($comment_command_printed) { $text .= ', or '; } $modified_commands = array(); foreach ($supported_commands as $command) { $modified_commands[] = '!'.$command; } $text .= implode(', ', $modified_commands); } $text .= "."; return $text; } public function getSupportedCommands() { $actions = array( DifferentialAction::ACTION_COMMENT, DifferentialAction::ACTION_REJECT, DifferentialAction::ACTION_ABANDON, DifferentialAction::ACTION_RECLAIM, DifferentialAction::ACTION_RESIGN, DifferentialAction::ACTION_RETHINK, 'unsubscribe', ); if (PhabricatorEnv::getEnvConfig('differential.enable-email-accept')) { $actions[] = DifferentialAction::ACTION_ACCEPT; } return $actions; } protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { $this->receivedMail = $mail; $this->handleAction($mail->getCleanTextBody(), $mail->getAttachments()); } public function handleAction($body, array $attachments) { // all commands start with a bang and separated from the body by a newline // to make sure that actual feedback text couldn't trigger an action. // unrecognized commands will be parsed as part of the comment. $command = DifferentialAction::ACTION_COMMENT; $supported_commands = $this->getSupportedCommands(); $regex = "/\A\n*!(" . implode('|', $supported_commands) . ")\n*/"; $matches = array(); if (preg_match($regex, $body, $matches)) { $command = $matches[1]; $body = trim(str_replace('!' . $command, '', $body)); } $actor = $this->getActor(); if (!$actor) { throw new Exception('No actor is set for the reply action.'); } switch ($command) { case 'unsubscribe': $this->unsubscribeUser($this->getMailReceiver(), $actor); // TODO: Send the user a confirmation email? return null; } $body = $this->enhanceBodyWithAttachments($body, $attachments); try { $editor = new DifferentialCommentEditor( $this->getMailReceiver(), $command); $editor->setActor($actor); $editor->setExcludeMailRecipientPHIDs( $this->getExcludeMailRecipientPHIDs()); // NOTE: We have to be careful about this because Facebook's // implementation jumps straight into handleAction() and will not have // a PhabricatorMetaMTAReceivedMail object. if ($this->receivedMail) { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_EMAIL, array( 'id' => $this->receivedMail->getID(), )); $editor->setContentSource($content_source); $editor->setParentMessageID($this->receivedMail->getMessageID()); } $editor->setMessage($body); - $comment = $editor->save(); - - return $comment->getID(); - + $editor->save(); } catch (Exception $ex) { if ($this->receivedMail) { $error_body = $this->receivedMail->getRawTextBody(); } else { $error_body = $body; } $exception_mail = new DifferentialExceptionMail( $this->getMailReceiver(), $ex, $error_body); $exception_mail->setActor($this->getActor()); $exception_mail->setToPHIDs(array($this->getActor()->getPHID())); $exception_mail->send(); throw $ex; } } private function unsubscribeUser( DifferentialRevision $revision, PhabricatorUser $user) { $revision->loadRelationships(); DifferentialRevisionEditor::removeCCAndUpdateRevision( $revision, $user->getPHID(), $user); } } diff --git a/src/applications/differential/storage/DifferentialComment.php b/src/applications/differential/storage/DifferentialComment.php index 1bc0dec2b0..66c22cd0fe 100644 --- a/src/applications/differential/storage/DifferentialComment.php +++ b/src/applications/differential/storage/DifferentialComment.php @@ -1,156 +1,156 @@ getProxyComment()->getContent(); } public function setContent($content) { // NOTE: We no longer read this field, but there's no cost to continuing // to write it in case something goes horribly wrong, since it makes it // far easier to back out of this. $this->content = $content; $this->getProxyComment()->setContent($content); return $this; } private function getProxyComment() { if (!$this->proxyComment) { $this->proxyComment = new DifferentialTransactionComment(); } return $this->proxyComment; } public function setProxyComment(DifferentialTransactionComment $proxy) { if ($this->proxyComment) { throw new Exception(pht('You can not overwrite a proxy comment.')); } $this->proxyComment = $proxy; return $this; } public function setRevision(DifferentialRevision $revision) { $this->getProxyComment()->setRevisionPHID($revision->getPHID()); return $this->setRevisionID($revision->getID()); } public function giveFacebookSomeArbitraryDiff(DifferentialDiff $diff) { $this->arbitraryDiffForFacebook = $diff; return $this; } public function getRequiredHandlePHIDs() { $phids = array(); $metadata = $this->getMetadata(); $added_reviewers = idx( $metadata, self::METADATA_ADDED_REVIEWERS); if ($added_reviewers) { foreach ($added_reviewers as $phid) { $phids[] = $phid; } } $added_ccs = idx( $metadata, self::METADATA_ADDED_CCS); if ($added_ccs) { foreach ($added_ccs as $phid) { $phids[] = $phid; } } return $phids; } public function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function getMarkupFieldKey($field) { return 'DC:'.$this->getID(); } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newDifferentialMarkupEngine( array( 'differential.diff' => $this->arbitraryDiffForFacebook, )); } public function getMarkupText($field) { return $this->getContent(); } public function didMarkupText($field, $output, PhutilMarkupEngine $engine) { return $output; } public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } public function save() { $this->openTransaction(); $result = parent::save(); - if (strlen($this->getContent())) { + if ($this->getContent() !== null) { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_LEGACY, array()); $xaction_phid = PhabricatorPHID::generateNewPHID( PhabricatorApplicationTransactionPHIDTypeTransaction::TYPECONST, DifferentialPHIDTypeRevision::TYPECONST); $proxy = $this->getProxyComment(); $proxy ->setAuthorPHID($this->getAuthorPHID()) ->setViewPolicy('public') ->setEditPolicy($this->getAuthorPHID()) ->setContentSource($content_source) ->setCommentVersion(1) ->setLegacyCommentID($this->getID()) ->setTransactionPHID($xaction_phid) ->save(); } $this->saveTransaction(); return $result; } }