protected function collectGarbage()
 {
     $table = new HeraldTranscript();
     $conn_w = $table->establishConnection('w');
     queryfx($conn_w, 'UPDATE %T SET
       objectTranscript     = "",
       ruleTranscripts      = "",
       conditionTranscripts = "",
       applyTranscripts     = "",
       garbageCollected     = 1
     WHERE garbageCollected = 0 AND time < %d
     LIMIT 100', $table->getTableName(), $this->getGarbageEpoch());
     return $conn_w->getAffectedRows() == 100;
 }
 private function collectHeraldTranscripts()
 {
     $ttl = PhabricatorEnv::getEnvConfig('gcdaemon.ttl.herald-transcripts');
     if ($ttl <= 0) {
         return 0;
     }
     $table = new HeraldTranscript();
     $conn_w = $table->establishConnection('w');
     queryfx($conn_w, 'UPDATE %T SET
       objectTranscript     = "",
       ruleTranscripts      = "",
       conditionTranscripts = "",
       applyTranscripts     = ""
     WHERE `time` < %d AND objectTranscript != ""
     LIMIT 100', $table->getTableName(), time() - $ttl);
     return $conn_w->getAffectedRows();
 }
 public function processRequest()
 {
     $request = $this->getRequest();
     $user = $request->getUser();
     // Get one page of data together with the pager.
     // Pull these objects manually since the serialized fields are gigantic.
     $transcript = new HeraldTranscript();
     $conn_r = $transcript->establishConnection('r');
     $phid = $request->getStr('phid');
     $where_clause = '';
     if ($phid) {
         $where_clause = qsprintf($conn_r, 'WHERE objectPHID = %s', $phid);
     }
     $pager = new AphrontPagerView();
     $pager->setOffset($request->getInt('offset'));
     $pager->setURI($request->getRequestURI(), 'offset');
     $limit_clause = qsprintf($conn_r, 'LIMIT %d, %d', $pager->getOffset(), $pager->getPageSize() + 1);
     $data = queryfx_all($conn_r, 'SELECT id, objectPHID, time, duration, dryRun FROM %T
     %Q
     ORDER BY id DESC
     %Q', $transcript->getTableName(), $where_clause, $limit_clause);
     $data = $pager->sliceResults($data);
     // Render the table.
     $handles = array();
     if ($data) {
         $phids = ipull($data, 'objectPHID', 'objectPHID');
         $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     }
     $rows = array();
     foreach ($data as $xscript) {
         $rows[] = array(phabricator_date($xscript['time'], $user), phabricator_time($xscript['time'], $user), $handles[$xscript['objectPHID']]->renderLink(), $xscript['dryRun'] ? 'Yes' : '', number_format((int) (1000 * $xscript['duration'])) . ' ms', phutil_render_tag('a', array('href' => '/herald/transcript/' . $xscript['id'] . '/', 'class' => 'button small grey'), 'View Transcript'));
     }
     $table = new AphrontTableView($rows);
     $table->setHeaders(array('Date', 'Time', 'Object', 'Dry Run', 'Duration', 'View'));
     $table->setColumnClasses(array('', 'right', 'wide wrap', '', '', 'action'));
     // Render the whole page.
     $panel = new AphrontPanelView();
     $panel->setHeader('Herald Transcripts');
     $panel->appendChild($table);
     $panel->appendChild($pager);
     $nav = $this->renderNav();
     $nav->selectFilter('transcript');
     $nav->appendChild($panel);
     return $this->buildStandardPageResponse($nav, array('title' => 'Herald Transcripts', 'tab' => 'transcripts'));
 }
 public function collectGarbage()
 {
     $ttl = PhabricatorEnv::getEnvConfig('gcdaemon.ttl.herald-transcripts');
     if ($ttl <= 0) {
         return false;
     }
     $table = new HeraldTranscript();
     $conn_w = $table->establishConnection('w');
     queryfx($conn_w, 'UPDATE %T SET
       objectTranscript     = "",
       ruleTranscripts      = "",
       conditionTranscripts = "",
       applyTranscripts     = "",
       garbageCollected     = 1
     WHERE garbageCollected = 0 AND time < %d
     LIMIT 100', $table->getTableName(), time() - $ttl);
     return $conn_w->getAffectedRows() == 100;
 }
 protected function loadPage()
 {
     $transcript = new HeraldTranscript();
     $conn_r = $transcript->establishConnection('r');
     // NOTE: Transcripts include a potentially enormous amount of serialized
     // data, so we're loading only some of the fields here if the caller asked
     // for partial records.
     if ($this->needPartialRecords) {
         $fields = implode(', ', array('id', 'phid', 'objectPHID', 'time', 'duration', 'dryRun', 'host'));
     } else {
         $fields = '*';
     }
     $rows = queryfx_all($conn_r, 'SELECT %Q FROM %T t %Q %Q %Q', $fields, $transcript->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r));
     $transcripts = $transcript->loadAllFromArray($rows);
     if ($this->needPartialRecords) {
         // Make sure nothing tries to write these; they aren't complete.
         foreach ($transcripts as $transcript) {
             $transcript->makeEphemeral();
         }
     }
     return $transcripts;
 }
 /**
  * @task mail
  */
 protected function sendMail(PhabricatorLiskDAO $object, array $xactions)
 {
     // Check if any of the transactions are visible. If we don't have any
     // visible transactions, don't send the mail.
     $any_visible = false;
     foreach ($xactions as $xaction) {
         if (!$xaction->shouldHideForMail($xactions)) {
             $any_visible = true;
             break;
         }
     }
     if (!$any_visible) {
         return;
     }
     $email_to = array_filter(array_unique($this->getMailTo($object)));
     $email_cc = array_filter(array_unique($this->getMailCC($object)));
     $phids = array_merge($email_to, $email_cc);
     $handles = id(new PhabricatorHandleQuery())->setViewer($this->requireActor())->withPHIDs($phids)->execute();
     $template = $this->buildMailTemplate($object);
     $body = $this->buildMailBody($object, $xactions);
     $mail_tags = $this->getMailTags($object, $xactions);
     $action = $this->getMailAction($object, $xactions);
     $reply_handler = $this->buildReplyHandler($object);
     $reply_section = $reply_handler->getReplyHandlerInstructions();
     if ($reply_section !== null) {
         $body->addReplySection($reply_section);
     }
     $template->setFrom($this->getActingAsPHID())->setSubjectPrefix($this->getMailSubjectPrefix())->setVarySubjectPrefix('[' . $action . ']')->setThreadID($this->getMailThreadID($object), $this->getIsNewObject())->setRelatedPHID($object->getPHID())->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())->setMailTags($mail_tags)->setIsBulk(true)->setBody($body->render())->setHTMLBody($body->renderHTML());
     foreach ($body->getAttachments() as $attachment) {
         $template->addAttachment($attachment);
     }
     $herald_xscript = $this->getHeraldTranscript();
     if ($herald_xscript) {
         $herald_header = $herald_xscript->getXHeraldRulesHeader();
         $herald_header = HeraldTranscript::saveXHeraldRulesHeader($object->getPHID(), $herald_header);
     } else {
         $herald_header = HeraldTranscript::loadXHeraldRulesHeader($object->getPHID());
     }
     if ($herald_header) {
         $template->addHeader('X-Herald-Rules', $herald_header);
     }
     if ($object instanceof PhabricatorProjectInterface) {
         $this->addMailProjectMetadata($object, $template);
     }
     if ($this->getParentMessageID()) {
         $template->setParentMessageID($this->getParentMessageID());
     }
     $mails = $reply_handler->multiplexMail($template, array_select_keys($handles, $email_to), array_select_keys($handles, $email_cc));
     foreach ($mails as $mail) {
         $mail->saveAndSend();
     }
     $template->addTos($email_to);
     $template->addCCs($email_cc);
     return $template;
 }
 public function 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;
 }
 public final function applyTransactions(PhabricatorLiskDAO $object, array $xactions)
 {
     $this->object = $object;
     $this->xactions = $xactions;
     $this->isNewObject = $object->getPHID() === null;
     $this->validateEditParameters($object, $xactions);
     $actor = $this->requireActor();
     // NOTE: Some transaction expansion requires that the edited object be
     // attached.
     foreach ($xactions as $xaction) {
         $xaction->attachObject($object);
         $xaction->attachViewer($actor);
     }
     $xactions = $this->expandTransactions($object, $xactions);
     $xactions = $this->expandSupportTransactions($object, $xactions);
     $xactions = $this->combineTransactions($xactions);
     foreach ($xactions as $xaction) {
         $xaction = $this->populateTransaction($object, $xaction);
     }
     $is_preview = $this->getIsPreview();
     $read_locking = false;
     $transaction_open = false;
     if (!$is_preview) {
         $errors = array();
         $type_map = mgroup($xactions, 'getTransactionType');
         foreach ($this->getTransactionTypes() as $type) {
             $type_xactions = idx($type_map, $type, array());
             $errors[] = $this->validateTransaction($object, $type, $type_xactions);
         }
         $errors[] = $this->validateAllTransactions($object, $xactions);
         $errors = array_mergev($errors);
         $continue_on_missing = $this->getContinueOnMissingFields();
         foreach ($errors as $key => $error) {
             if ($continue_on_missing && $error->getIsMissingFieldError()) {
                 unset($errors[$key]);
             }
         }
         if ($errors) {
             throw new PhabricatorApplicationTransactionValidationException($errors);
         }
         $this->willApplyTransactions($object, $xactions);
         if ($object->getID()) {
             foreach ($xactions as $xaction) {
                 // If any of the transactions require a read lock, hold one and
                 // reload the object. We need to do this fairly early so that the
                 // call to `adjustTransactionValues()` (which populates old values)
                 // is based on the synchronized state of the object, which may differ
                 // from the state when it was originally loaded.
                 if ($this->shouldReadLock($object, $xaction)) {
                     $object->openTransaction();
                     $object->beginReadLocking();
                     $transaction_open = true;
                     $read_locking = true;
                     $object->reload();
                     break;
                 }
             }
         }
         if ($this->shouldApplyInitialEffects($object, $xactions)) {
             if (!$transaction_open) {
                 $object->openTransaction();
                 $transaction_open = true;
             }
         }
     }
     if ($this->shouldApplyInitialEffects($object, $xactions)) {
         $this->applyInitialEffects($object, $xactions);
     }
     foreach ($xactions as $xaction) {
         $this->adjustTransactionValues($object, $xaction);
     }
     try {
         $xactions = $this->filterTransactions($object, $xactions);
     } catch (Exception $ex) {
         if ($read_locking) {
             $object->endReadLocking();
         }
         if ($transaction_open) {
             $object->killTransaction();
         }
         throw $ex;
     }
     // TODO: Once everything is on EditEngine, just use getIsNewObject() to
     // figure this out instead.
     $mark_as_create = false;
     $create_type = PhabricatorTransactions::TYPE_CREATE;
     foreach ($xactions as $xaction) {
         if ($xaction->getTransactionType() == $create_type) {
             $mark_as_create = true;
         }
     }
     if ($mark_as_create) {
         foreach ($xactions as $xaction) {
             $xaction->setIsCreateTransaction(true);
         }
     }
     // Now that we've merged, filtered, and combined transactions, check for
     // required capabilities.
     foreach ($xactions as $xaction) {
         $this->requireCapabilities($object, $xaction);
     }
     $xactions = $this->sortTransactions($xactions);
     $file_phids = $this->extractFilePHIDs($object, $xactions);
     if ($is_preview) {
         $this->loadHandles($xactions);
         return $xactions;
     }
     $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())->setActor($actor)->setActingAsPHID($this->getActingAsPHID())->setContentSource($this->getContentSource());
     if (!$transaction_open) {
         $object->openTransaction();
     }
     try {
         foreach ($xactions as $xaction) {
             $this->applyInternalEffects($object, $xaction);
         }
         $xactions = $this->didApplyInternalEffects($object, $xactions);
         try {
             $object->save();
         } catch (AphrontDuplicateKeyQueryException $ex) {
             // This callback has an opportunity to throw a better exception,
             // so execution may end here.
             $this->didCatchDuplicateKeyException($object, $xactions, $ex);
             throw $ex;
         }
         foreach ($xactions as $xaction) {
             $xaction->setObjectPHID($object->getPHID());
             if ($xaction->getComment()) {
                 $xaction->setPHID($xaction->generatePHID());
                 $comment_editor->applyEdit($xaction, $xaction->getComment());
             } else {
                 $xaction->save();
             }
         }
         if ($file_phids) {
             $this->attachFiles($object, $file_phids);
         }
         foreach ($xactions as $xaction) {
             $this->applyExternalEffects($object, $xaction);
         }
         $xactions = $this->applyFinalEffects($object, $xactions);
         if ($read_locking) {
             $object->endReadLocking();
             $read_locking = false;
         }
         $object->saveTransaction();
     } catch (Exception $ex) {
         $object->killTransaction();
         throw $ex;
     }
     // Now that we've completely applied the core transaction set, try to apply
     // Herald rules. Herald rules are allowed to either take direct actions on
     // the database (like writing flags), or take indirect actions (like saving
     // some targets for CC when we generate mail a little later), or return
     // transactions which we'll apply normally using another Editor.
     // First, check if *this* is a sub-editor which is itself applying Herald
     // rules: if it is, stop working and return so we don't descend into
     // madness.
     // Otherwise, we're not a Herald editor, so process Herald rules (possibly
     // using a Herald editor to apply resulting transactions) and then send out
     // mail, notifications, and feed updates about everything.
     if ($this->getIsHeraldEditor()) {
         // We are the Herald editor, so stop work here and return the updated
         // transactions.
         return $xactions;
     } else {
         if ($this->getIsInverseEdgeEditor()) {
             // If we're applying inverse edge transactions, don't trigger Herald.
             // From a product perspective, the current set of inverse edges (most
             // often, mentions) aren't things users would expect to trigger Herald.
             // From a technical perspective, objects loaded by the inverse editor may
             // not have enough data to execute rules. At least for now, just stop
             // Herald from executing when applying inverse edges.
         } else {
             if ($this->shouldApplyHeraldRules($object, $xactions)) {
                 // We are not the Herald editor, so try to apply Herald rules.
                 $herald_xactions = $this->applyHeraldRules($object, $xactions);
                 if ($herald_xactions) {
                     $xscript_id = $this->getHeraldTranscript()->getID();
                     foreach ($herald_xactions as $herald_xaction) {
                         // Don't set a transcript ID if this is a transaction from another
                         // application or source, like Owners.
                         if ($herald_xaction->getAuthorPHID()) {
                             continue;
                         }
                         $herald_xaction->setMetadataValue('herald:transcriptID', $xscript_id);
                     }
                     // NOTE: We're acting as the omnipotent user because rules deal with
                     // their own policy issues. We use a synthetic author PHID (the
                     // Herald application) as the author of record, so that transactions
                     // will render in a reasonable way ("Herald assigned this task ...").
                     $herald_actor = PhabricatorUser::getOmnipotentUser();
                     $herald_phid = id(new PhabricatorHeraldApplication())->getPHID();
                     // TODO: It would be nice to give transactions a more specific source
                     // which points at the rule which generated them. You can figure this
                     // out from transcripts, but it would be cleaner if you didn't have to.
                     $herald_source = PhabricatorContentSource::newForSource(PhabricatorHeraldContentSource::SOURCECONST);
                     $herald_editor = newv(get_class($this), array())->setContinueOnNoEffect(true)->setContinueOnMissingFields(true)->setParentMessageID($this->getParentMessageID())->setIsHeraldEditor(true)->setActor($herald_actor)->setActingAsPHID($herald_phid)->setContentSource($herald_source);
                     $herald_xactions = $herald_editor->applyTransactions($object, $herald_xactions);
                     // Merge the new transactions into the transaction list: we want to
                     // send email and publish feed stories about them, too.
                     $xactions = array_merge($xactions, $herald_xactions);
                 }
                 // If Herald did not generate transactions, we may still need to handle
                 // "Send an Email" rules.
                 $adapter = $this->getHeraldAdapter();
                 $this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
                 $this->heraldForcedEmailPHIDs = $adapter->getForcedEmailPHIDs();
             }
         }
     }
     $this->didApplyTransactions($xactions);
     if ($object instanceof PhabricatorCustomFieldInterface) {
         // Maybe this makes more sense to move into the search index itself? For
         // now I'm putting it here since I think we might end up with things that
         // need it to be up to date once the next page loads, but if we don't go
         // there we could move it into search once search moves to the daemons.
         // It now happens in the search indexer as well, but the search indexer is
         // always daemonized, so the logic above still potentially holds. We could
         // possibly get rid of this. The major motivation for putting it in the
         // indexer was to enable reindexing to work.
         $fields = PhabricatorCustomField::getObjectFields($object, PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
         $fields->readFieldsFromStorage($object);
         $fields->rebuildIndexes($object);
     }
     $herald_xscript = $this->getHeraldTranscript();
     if ($herald_xscript) {
         $herald_header = $herald_xscript->getXHeraldRulesHeader();
         $herald_header = HeraldTranscript::saveXHeraldRulesHeader($object->getPHID(), $herald_header);
     } else {
         $herald_header = HeraldTranscript::loadXHeraldRulesHeader($object->getPHID());
     }
     $this->heraldHeader = $herald_header;
     // We're going to compute some of the data we'll use to publish these
     // transactions here, before queueing a worker.
     //
     // Primarily, this is more correct: we want to publish the object as it
     // exists right now. The worker may not execute for some time, and we want
     // to use the current To/CC list, not respect any changes which may occur
     // between now and when the worker executes.
     //
     // As a secondary benefit, this tends to reduce the amount of state that
     // Editors need to pass into workers.
     $object = $this->willPublish($object, $xactions);
     if (!$this->getDisableEmail()) {
         if ($this->shouldSendMail($object, $xactions)) {
             $this->mailToPHIDs = $this->getMailTo($object);
             $this->mailCCPHIDs = $this->getMailCC($object);
         }
     }
     if ($this->shouldPublishFeedStory($object, $xactions)) {
         $this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs($object, $xactions);
         $this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs($object, $xactions);
     }
     PhabricatorWorker::scheduleTask('PhabricatorApplicationTransactionPublishWorker', array('objectPHID' => $object->getPHID(), 'actorPHID' => $this->getActingAsPHID(), 'xactionPHIDs' => mpull($xactions, 'getPHID'), 'state' => $this->getWorkerState()), array('objectPHID' => $object->getPHID(), 'priority' => PhabricatorWorker::PRIORITY_ALERTS));
     return $xactions;
 }
 private function buildObjectTranscriptPanel(HeraldTranscript $xscript)
 {
     $adapter = $this->getAdapter();
     $field_names = $adapter->getFieldNameMap();
     $object_xscript = $xscript->getObjectTranscript();
     $data = array();
     if ($object_xscript) {
         $phid = $object_xscript->getPHID();
         $handles = $this->handles;
         $data += array(pht('Object Name') => $object_xscript->getName(), pht('Object Type') => $object_xscript->getType(), pht('Object PHID') => $phid, pht('Object Link') => $handles[$phid]->renderLink());
     }
     $data += $xscript->getMetadataMap();
     if ($object_xscript) {
         foreach ($object_xscript->getFields() as $field => $value) {
             $field = idx($field_names, $field, '[' . $field . '?]');
             $data['Field: ' . $field] = $value;
         }
     }
     $rows = array();
     foreach ($data as $name => $value) {
         if (!$value instanceof PhutilSafeHTML) {
             if (!is_scalar($value) && !is_null($value)) {
                 $value = implode("\n", $value);
             }
             if (strlen($value) > 256) {
                 $value = phutil_tag('textarea', array('class' => 'herald-field-value-transcript'), $value);
             }
         }
         $rows[] = array($name, $value);
     }
     $property_list = new PHUIPropertyListView();
     $property_list->setStacked(true);
     foreach ($rows as $row) {
         $property_list->addProperty($row[0], $row[1]);
     }
     $box = new PHUIObjectBoxView();
     $box->setHeaderText(pht('Object Transcript'));
     $box->appendChild($property_list);
     return $box;
 }
 public function save()
 {
     $revision = $this->getRevision();
     $is_new = $this->isNewRevision();
     if ($is_new) {
         $this->initializeNewRevision($revision);
     }
     $revision->loadRelationships();
     $this->willWriteRevision();
     if ($this->reviewers === null) {
         $this->reviewers = $revision->getReviewers();
     }
     if ($this->cc === null) {
         $this->cc = $revision->getCCPHIDs();
     }
     $diff = $this->getDiff();
     if ($diff) {
         $revision->setLineCount($diff->getLineCount());
     }
     // Save the revision, to generate its ID and PHID if it is new. We need
     // the ID/PHID in order to record them in Herald transcripts, but don't
     // want to hold a transaction open while running Herald because it is
     // potentially somewhat slow. The downside is that we may end up with a
     // saved revision/diff pair without appropriate CCs. We could be better
     // about this -- for example:
     //
     //  - Herald can't affect reviewers, so we could compute them before
     //    opening the transaction and then save them in the transaction.
     //  - Herald doesn't *really* need PHIDs to compute its effects, we could
     //    run it before saving these objects and then hand over the PHIDs later.
     //
     // But this should address the problem of orphaned revisions, which is
     // currently the only problem we experience in practice.
     $revision->openTransaction();
     if ($diff) {
         $revision->setBranchName($diff->getBranch());
         $revision->setArcanistProjectPHID($diff->getArcanistProjectPHID());
     }
     $revision->save();
     if ($diff) {
         $diff->setRevisionID($revision->getID());
         $diff->save();
     }
     $revision->saveTransaction();
     // We're going to build up three dictionaries: $add, $rem, and $stable. The
     // $add dictionary has added reviewers/CCs. The $rem dictionary has
     // reviewers/CCs who have been removed, and the $stable array is
     // reviewers/CCs who haven't changed. We're going to send new reviewers/CCs
     // a different ("welcome") email than we send stable reviewers/CCs.
     $old = array('rev' => array_fill_keys($revision->getReviewers(), true), 'ccs' => array_fill_keys($revision->getCCPHIDs(), true));
     $xscript_header = null;
     $xscript_uri = null;
     $new = array('rev' => array_fill_keys($this->reviewers, true), 'ccs' => array_fill_keys($this->cc, true));
     $rem_ccs = array();
     $xscript_phid = null;
     if ($diff) {
         $adapter = new HeraldDifferentialRevisionAdapter($revision, $diff);
         $adapter->setExplicitCCs($new['ccs']);
         $adapter->setExplicitReviewers($new['rev']);
         $adapter->setForbiddenCCs($revision->getUnsubscribedPHIDs());
         $xscript = HeraldEngine::loadAndApplyRules($adapter);
         $xscript_uri = '/herald/transcript/' . $xscript->getID() . '/';
         $xscript_phid = $xscript->getPHID();
         $xscript_header = $xscript->getXHeraldRulesHeader();
         $xscript_header = HeraldTranscript::saveXHeraldRulesHeader($revision->getPHID(), $xscript_header);
         $sub = array('rev' => array(), 'ccs' => $adapter->getCCsAddedByHerald());
         $rem_ccs = $adapter->getCCsRemovedByHerald();
     } else {
         $sub = array('rev' => array(), 'ccs' => array());
     }
     // Remove any CCs which are prevented by Herald rules.
     $sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs);
     $new['ccs'] = array_diff_key($new['ccs'], $rem_ccs);
     $add = array();
     $rem = array();
     $stable = array();
     foreach (array('rev', 'ccs') as $key) {
         $add[$key] = array();
         if ($new[$key] !== null) {
             $add[$key] += array_diff_key($new[$key], $old[$key]);
         }
         $add[$key] += array_diff_key($sub[$key], $old[$key]);
         $combined = $sub[$key];
         if ($new[$key] !== null) {
             $combined += $new[$key];
         }
         $rem[$key] = array_diff_key($old[$key], $combined);
         $stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]);
     }
     self::alterReviewers($revision, $this->reviewers, array_keys($rem['rev']), array_keys($add['rev']), $this->actorPHID);
     // We want to attribute new CCs to a "reasonPHID", representing the reason
     // they were added. This is either a user (if some user explicitly CCs
     // them, or uses "Add CCs...") or a Herald transcript PHID, indicating that
     // they were added by a Herald rule.
     if ($add['ccs'] || $rem['ccs']) {
         $reasons = array();
         foreach ($add['ccs'] as $phid => $ignored) {
             if (empty($new['ccs'][$phid])) {
                 $reasons[$phid] = $xscript_phid;
             } else {
                 $reasons[$phid] = $this->actorPHID;
             }
         }
         foreach ($rem['ccs'] as $phid => $ignored) {
             if (empty($new['ccs'][$phid])) {
                 $reasons[$phid] = $this->actorPHID;
             } else {
                 $reasons[$phid] = $xscript_phid;
             }
         }
     } else {
         $reasons = $this->actorPHID;
     }
     self::alterCCs($revision, $this->cc, array_keys($rem['ccs']), array_keys($add['ccs']), $reasons);
     $this->updateAuxiliaryFields();
     // Add the author and users included from Herald rules to the relevant set
     // of users so they get a copy of the email.
     if (!$this->silentUpdate) {
         if ($is_new) {
             $add['rev'][$this->getActorPHID()] = true;
             if ($diff) {
                 $add['rev'] += $adapter->getEmailPHIDsAddedByHerald();
             }
         } else {
             $stable['rev'][$this->getActorPHID()] = true;
             if ($diff) {
                 $stable['rev'] += $adapter->getEmailPHIDsAddedByHerald();
             }
         }
     }
     $mail = array();
     $phids = array($this->getActorPHID());
     $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     $actor_handle = $handles[$this->getActorPHID()];
     $changesets = null;
     $comment = null;
     if ($diff) {
         $changesets = $diff->loadChangesets();
         // TODO: This should probably be in DifferentialFeedbackEditor?
         if (!$is_new) {
             $comment = $this->createComment();
         }
         if ($comment) {
             $mail[] = id(new DifferentialNewDiffMail($revision, $actor_handle, $changesets))->setIsFirstMailAboutRevision($is_new)->setIsFirstMailToRecipients($is_new)->setComments($this->getComments())->setToPHIDs(array_keys($stable['rev']))->setCCPHIDs(array_keys($stable['ccs']));
         }
         // Save the changes we made above.
         $diff->setDescription(preg_replace('/\\n.*/s', '', $this->getComments()));
         $diff->save();
         $this->updateAffectedPathTable($revision, $diff, $changesets);
         $this->updateRevisionHashTable($revision, $diff);
         // An updated diff should require review, as long as it's not closed
         // or accepted. The "accepted" status is "sticky" to encourage courtesy
         // re-diffs after someone accepts with minor changes/suggestions.
         $status = $revision->getStatus();
         if ($status != ArcanistDifferentialRevisionStatus::CLOSED && $status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
             $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW);
         }
     } else {
         $diff = $revision->loadActiveDiff();
         if ($diff) {
             $changesets = $diff->loadChangesets();
         } else {
             $changesets = array();
         }
     }
     $revision->save();
     $this->didWriteRevision();
     $event_data = array('revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $is_new ? DifferentialAction::ACTION_CREATE : DifferentialAction::ACTION_UPDATE, 'feedback_content' => $is_new ? phutil_utf8_shorten($revision->getSummary(), 140) : $this->getComments(), 'actor_phid' => $revision->getAuthorPHID());
     id(new PhabricatorTimelineEvent('difx', $event_data))->recordEvent();
     id(new PhabricatorFeedStoryPublisher())->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL)->setStoryData($event_data)->setStoryTime(time())->setStoryAuthorPHID($revision->getAuthorPHID())->setRelatedPHIDs(array($revision->getPHID(), $revision->getAuthorPHID()))->setPrimaryObjectPHID($revision->getPHID())->setSubscribedPHIDs(array_merge(array($revision->getAuthorPHID()), $revision->getReviewers(), $revision->getCCPHIDs()))->publish();
     //  TODO: Move this into a worker task thing.
     PhabricatorSearchDifferentialIndexer::indexRevision($revision);
     if ($this->silentUpdate) {
         return;
     }
     $revision->loadRelationships();
     if ($add['rev']) {
         $message = id(new DifferentialNewDiffMail($revision, $actor_handle, $changesets))->setIsFirstMailAboutRevision($is_new)->setIsFirstMailToRecipients(true)->setToPHIDs(array_keys($add['rev']));
         if ($is_new) {
             // The first time we send an email about a revision, put the CCs in
             // the "CC:" field of the same "Review Requested" email that reviewers
             // get, so you don't get two initial emails if you're on a list that
             // is CC'd.
             $message->setCCPHIDs(array_keys($add['ccs']));
         }
         $mail[] = $message;
     }
     // If we added CCs, we want to send them an email, but only if they were not
     // already a reviewer and were not added as one (in these cases, they got
     // a "NewDiff" mail, either in the past or just a moment ago). You can still
     // get two emails, but only if a revision is updated and you are added as a
     // reviewer at the same time a list you are on is added as a CC, which is
     // rare and reasonable.
     $implied_ccs = self::getImpliedCCs($revision);
     $implied_ccs = array_fill_keys($implied_ccs, true);
     $add['ccs'] = array_diff_key($add['ccs'], $implied_ccs);
     if (!$is_new && $add['ccs']) {
         $mail[] = id(new DifferentialCCWelcomeMail($revision, $actor_handle, $changesets))->setIsFirstMailToRecipients(true)->setToPHIDs(array_keys($add['ccs']));
     }
     foreach ($mail as $message) {
         $message->setHeraldTranscriptURI($xscript_uri);
         $message->setXHeraldRulesHeader($xscript_header);
         $message->send();
     }
 }
 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 save()
 {
     $revision = $this->getRevision();
     // TODO
     //    $revision->openTransaction();
     $is_new = $this->isNewRevision();
     if ($is_new) {
         // These fields aren't nullable; set them to sensible defaults if they
         // haven't been configured. We're just doing this so we can generate an
         // ID for the revision if we don't have one already.
         $revision->setLineCount(0);
         if ($revision->getStatus() === null) {
             $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
         }
         if ($revision->getTitle() === null) {
             $revision->setTitle('Untitled Revision');
         }
         if ($revision->getAuthorPHID() === null) {
             $revision->setAuthorPHID($this->getActorPHID());
         }
         if ($revision->getSummary() === null) {
             $revision->setSummary('');
         }
         if ($revision->getTestPlan() === null) {
             $revision->setTestPlan('');
         }
         $revision->save();
     }
     $revision->loadRelationships();
     $this->willWriteRevision();
     if ($this->reviewers === null) {
         $this->reviewers = $revision->getReviewers();
     }
     if ($this->cc === null) {
         $this->cc = $revision->getCCPHIDs();
     }
     // We're going to build up three dictionaries: $add, $rem, and $stable. The
     // $add dictionary has added reviewers/CCs. The $rem dictionary has
     // reviewers/CCs who have been removed, and the $stable array is
     // reviewers/CCs who haven't changed. We're going to send new reviewers/CCs
     // a different ("welcome") email than we send stable reviewers/CCs.
     $old = array('rev' => array_fill_keys($revision->getReviewers(), true), 'ccs' => array_fill_keys($revision->getCCPHIDs(), true));
     $diff = $this->getDiff();
     $xscript_header = null;
     $xscript_uri = null;
     $new = array('rev' => array_fill_keys($this->reviewers, true), 'ccs' => array_fill_keys($this->cc, true));
     $rem_ccs = array();
     if ($diff) {
         $diff->setRevisionID($revision->getID());
         $revision->setLineCount($diff->getLineCount());
         $adapter = new HeraldDifferentialRevisionAdapter($revision, $diff);
         $adapter->setExplicitCCs($new['ccs']);
         $adapter->setExplicitReviewers($new['rev']);
         $adapter->setForbiddenCCs($revision->getUnsubscribedPHIDs());
         $xscript = HeraldEngine::loadAndApplyRules($adapter);
         $xscript_uri = PhabricatorEnv::getProductionURI('/herald/transcript/' . $xscript->getID() . '/');
         $xscript_phid = $xscript->getPHID();
         $xscript_header = $xscript->getXHeraldRulesHeader();
         $xscript_header = HeraldTranscript::saveXHeraldRulesHeader($revision->getPHID(), $xscript_header);
         $sub = array('rev' => array(), 'ccs' => $adapter->getCCsAddedByHerald());
         $rem_ccs = $adapter->getCCsRemovedByHerald();
     } else {
         $sub = array('rev' => array(), 'ccs' => array());
     }
     // Remove any CCs which are prevented by Herald rules.
     $sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs);
     $new['ccs'] = array_diff_key($new['ccs'], $rem_ccs);
     $add = array();
     $rem = array();
     $stable = array();
     foreach (array('rev', 'ccs') as $key) {
         $add[$key] = array();
         if ($new[$key] !== null) {
             $add[$key] += array_diff_key($new[$key], $old[$key]);
         }
         $add[$key] += array_diff_key($sub[$key], $old[$key]);
         $combined = $sub[$key];
         if ($new[$key] !== null) {
             $combined += $new[$key];
         }
         $rem[$key] = array_diff_key($old[$key], $combined);
         $stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]);
     }
     self::alterReviewers($revision, $this->reviewers, array_keys($rem['rev']), array_keys($add['rev']), $this->actorPHID);
     /*
     
         // TODO: When Herald is brought over, run through this stuff to figure
         // out which adds are Herald's fault.
     
         // TODO: Still need to do this.
     
         if ($add['ccs'] || $rem['ccs']) {
           foreach (array_keys($add['ccs']) as $id) {
             if (empty($new['ccs'][$id])) {
               $reason_phid = 'TODO';//$xscript_phid;
             } else {
               $reason_phid = $this->getActorPHID();
             }
           }
           foreach (array_keys($rem['ccs']) as $id) {
             if (empty($new['ccs'][$id])) {
               $reason_phid = $this->getActorPHID();
             } else {
               $reason_phid = 'TODO';//$xscript_phid;
             }
           }
         }
     */
     self::alterCCs($revision, $this->cc, array_keys($rem['ccs']), array_keys($add['ccs']), $this->actorPHID);
     $this->updateAuxiliaryFields();
     // Add the author and users included from Herald rules to the relevant set
     // of users so they get a copy of the email.
     if (!$this->silentUpdate) {
         if ($is_new) {
             $add['rev'][$this->getActorPHID()] = true;
             if ($diff) {
                 $add['rev'] += $adapter->getEmailPHIDsAddedByHerald();
             }
         } else {
             $stable['rev'][$this->getActorPHID()] = true;
             if ($diff) {
                 $stable['rev'] += $adapter->getEmailPHIDsAddedByHerald();
             }
         }
     }
     $mail = array();
     $phids = array($this->getActorPHID());
     $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
     $actor_handle = $handles[$this->getActorPHID()];
     $changesets = null;
     $comment = null;
     if ($diff) {
         $changesets = $diff->loadChangesets();
         // TODO: This should probably be in DifferentialFeedbackEditor?
         if (!$is_new) {
             $comment = $this->createComment();
         }
         if ($comment) {
             $mail[] = id(new DifferentialNewDiffMail($revision, $actor_handle, $changesets))->setIsFirstMailAboutRevision($is_new)->setIsFirstMailToRecipients($is_new)->setComments($this->getComments())->setToPHIDs(array_keys($stable['rev']))->setCCPHIDs(array_keys($stable['ccs']));
         }
         // Save the changes we made above.
         $diff->setDescription(substr($this->getComments(), 0, 80));
         $diff->save();
         // An updated diff should require review, as long as it's not committed
         // or accepted. The "accepted" status is "sticky" to encourage courtesy
         // re-diffs after someone accepts with minor changes/suggestions.
         $status = $revision->getStatus();
         if ($status != DifferentialRevisionStatus::COMMITTED && $status != DifferentialRevisionStatus::ACCEPTED) {
             $revision->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW);
         }
     } else {
         $diff = $revision->loadActiveDiff();
         if ($diff) {
             $changesets = $diff->loadChangesets();
         } else {
             $changesets = array();
         }
     }
     $revision->save();
     $this->didWriteRevision();
     $event_data = array('revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $is_new ? DifferentialAction::ACTION_CREATE : DifferentialAction::ACTION_UPDATE, 'feedback_content' => $is_new ? phutil_utf8_shorten($revision->getSummary(), 140) : $this->getComments(), 'actor_phid' => $revision->getAuthorPHID());
     id(new PhabricatorTimelineEvent('difx', $event_data))->recordEvent();
     id(new PhabricatorFeedStoryPublisher())->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL)->setStoryData($event_data)->setStoryTime(time())->setStoryAuthorPHID($revision->getAuthorPHID())->setRelatedPHIDs(array($revision->getPHID(), $revision->getAuthorPHID()))->publish();
     // TODO
     //    $revision->saveTransaction();
     //  TODO: Move this into a worker task thing.
     PhabricatorSearchDifferentialIndexer::indexRevision($revision);
     if ($this->silentUpdate) {
         return;
     }
     $revision->loadRelationships();
     if ($add['rev']) {
         $message = id(new DifferentialNewDiffMail($revision, $actor_handle, $changesets))->setIsFirstMailAboutRevision($is_new)->setIsFirstMailToRecipients(true)->setToPHIDs(array_keys($add['rev']));
         if ($is_new) {
             // The first time we send an email about a revision, put the CCs in
             // the "CC:" field of the same "Review Requested" email that reviewers
             // get, so you don't get two initial emails if you're on a list that
             // is CC'd.
             $message->setCCPHIDs(array_keys($add['ccs']));
         }
         $mail[] = $message;
     }
     // If you were added as a reviewer and a CC, just give you the reviewer
     // email. We could go to greater lengths to prevent this, but there's
     // bunch of stuff with list subscriptions anyway. You can still get two
     // emails, but only if a revision is updated and you are added as a reviewer
     // at the same time a list you are on is added as a CC, which is rare and
     // reasonable.
     $add['ccs'] = array_diff_key($add['ccs'], $add['rev']);
     if (!$is_new && $add['ccs']) {
         $mail[] = id(new DifferentialCCWelcomeMail($revision, $actor_handle, $changesets))->setIsFirstMailToRecipients(true)->setToPHIDs(array_keys($add['ccs']));
     }
     foreach ($mail as $message) {
         $message->setHeraldTranscriptURI($xscript_uri);
         $message->setXHeraldRulesHeader($xscript_header);
         $message->send();
     }
 }