Path: blob/master/src/applications/audit/editor/PhabricatorAuditEditor.php
12256 views
<?php12final class PhabricatorAuditEditor3extends PhabricatorApplicationTransactionEditor {45const MAX_FILES_SHOWN_IN_EMAIL = 1000;67private $affectedFiles;8private $rawPatch;9private $auditorPHIDs = array();1011private $didExpandInlineState = false;12private $oldAuditStatus = null;1314public function setRawPatch($patch) {15$this->rawPatch = $patch;16return $this;17}1819public function getRawPatch() {20return $this->rawPatch;21}2223public function getEditorApplicationClass() {24return 'PhabricatorDiffusionApplication';25}2627public function getEditorObjectsDescription() {28return pht('Audits');29}3031public function getTransactionTypes() {32$types = parent::getTransactionTypes();3334$types[] = PhabricatorTransactions::TYPE_COMMENT;35$types[] = PhabricatorTransactions::TYPE_EDGE;36$types[] = PhabricatorTransactions::TYPE_INLINESTATE;3738$types[] = PhabricatorAuditTransaction::TYPE_COMMIT;3940// TODO: These will get modernized eventually, but that can happen one41// at a time later on.42$types[] = PhabricatorAuditActionConstants::INLINE;4344return $types;45}4647protected function expandTransactions(48PhabricatorLiskDAO $object,49array $xactions) {5051foreach ($xactions as $xaction) {52switch ($xaction->getTransactionType()) {53case PhabricatorTransactions::TYPE_INLINESTATE:54$this->didExpandInlineState = true;55break;56}57}5859$this->oldAuditStatus = $object->getAuditStatus();6061return parent::expandTransactions($object, $xactions);62}6364protected function transactionHasEffect(65PhabricatorLiskDAO $object,66PhabricatorApplicationTransaction $xaction) {6768switch ($xaction->getTransactionType()) {69case PhabricatorAuditActionConstants::INLINE:70return $xaction->hasComment();71}7273return parent::transactionHasEffect($object, $xaction);74}7576protected function getCustomTransactionOldValue(77PhabricatorLiskDAO $object,78PhabricatorApplicationTransaction $xaction) {79switch ($xaction->getTransactionType()) {80case PhabricatorAuditActionConstants::INLINE:81case PhabricatorAuditTransaction::TYPE_COMMIT:82return null;83}8485return parent::getCustomTransactionOldValue($object, $xaction);86}8788protected function getCustomTransactionNewValue(89PhabricatorLiskDAO $object,90PhabricatorApplicationTransaction $xaction) {9192switch ($xaction->getTransactionType()) {93case PhabricatorAuditActionConstants::INLINE:94case PhabricatorAuditTransaction::TYPE_COMMIT:95return $xaction->getNewValue();96}9798return parent::getCustomTransactionNewValue($object, $xaction);99}100101protected function applyCustomInternalTransaction(102PhabricatorLiskDAO $object,103PhabricatorApplicationTransaction $xaction) {104105switch ($xaction->getTransactionType()) {106case PhabricatorAuditActionConstants::INLINE:107$comment = $xaction->getComment();108109$comment->setAttribute('editing', false);110111PhabricatorVersionedDraft::purgeDrafts(112$comment->getPHID(),113$this->getActingAsPHID());114return;115case PhabricatorAuditTransaction::TYPE_COMMIT:116return;117}118119return parent::applyCustomInternalTransaction($object, $xaction);120}121122protected function applyCustomExternalTransaction(123PhabricatorLiskDAO $object,124PhabricatorApplicationTransaction $xaction) {125126switch ($xaction->getTransactionType()) {127case PhabricatorAuditTransaction::TYPE_COMMIT:128return;129case PhabricatorAuditActionConstants::INLINE:130$reply = $xaction->getComment()->getReplyToComment();131if ($reply && !$reply->getHasReplies()) {132$reply->setHasReplies(1)->save();133}134return;135}136137return parent::applyCustomExternalTransaction($object, $xaction);138}139140protected function applyBuiltinExternalTransaction(141PhabricatorLiskDAO $object,142PhabricatorApplicationTransaction $xaction) {143144switch ($xaction->getTransactionType()) {145case PhabricatorTransactions::TYPE_INLINESTATE:146$table = new PhabricatorAuditTransactionComment();147$conn_w = $table->establishConnection('w');148foreach ($xaction->getNewValue() as $phid => $state) {149queryfx(150$conn_w,151'UPDATE %T SET fixedState = %s WHERE phid = %s',152$table->getTableName(),153$state,154$phid);155}156break;157}158159return parent::applyBuiltinExternalTransaction($object, $xaction);160}161162protected function applyFinalEffects(163PhabricatorLiskDAO $object,164array $xactions) {165166// Load auditors explicitly; we may not have them if the caller was a167// generic piece of infrastructure.168169$commit = id(new DiffusionCommitQuery())170->setViewer($this->requireActor())171->withIDs(array($object->getID()))172->needAuditRequests(true)173->executeOne();174if (!$commit) {175throw new Exception(176pht('Failed to load commit during transaction finalization!'));177}178$object->attachAudits($commit->getAudits());179180$actor_phid = $this->getActingAsPHID();181$actor_is_author = ($object->getAuthorPHID()) &&182($actor_phid == $object->getAuthorPHID());183184$import_status_flag = null;185foreach ($xactions as $xaction) {186switch ($xaction->getTransactionType()) {187case PhabricatorAuditTransaction::TYPE_COMMIT:188$import_status_flag = PhabricatorRepositoryCommit::IMPORTED_PUBLISH;189break;190}191}192193$old_status = $this->oldAuditStatus;194195$requests = $object->getAudits();196$object->updateAuditStatus($requests);197198$new_status = $object->getAuditStatus();199200$object->save();201202if ($import_status_flag) {203$object->writeImportStatusFlag($import_status_flag);204}205206// If the commit has changed state after this edit, add an informational207// transaction about the state change.208if ($old_status != $new_status) {209if ($object->isAuditStatusPartiallyAudited()) {210// This state isn't interesting enough to get a transaction. The211// best way we could lead the user forward is something like "This212// commit still requires additional audits." but that's redundant and213// probably not very useful.214} else {215$xaction = $object->getApplicationTransactionTemplate()216->setTransactionType(DiffusionCommitStateTransaction::TRANSACTIONTYPE)217->setOldValue($old_status)218->setNewValue($new_status);219220$xaction = $this->populateTransaction($object, $xaction);221222$xaction->save();223}224}225226// Collect auditor PHIDs for building mail.227$this->auditorPHIDs = mpull($object->getAudits(), 'getAuditorPHID');228229return $xactions;230}231232protected function expandTransaction(233PhabricatorLiskDAO $object,234PhabricatorApplicationTransaction $xaction) {235236$auditors_type = DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE;237238$xactions = parent::expandTransaction($object, $xaction);239240switch ($xaction->getTransactionType()) {241case PhabricatorAuditTransaction::TYPE_COMMIT:242$phids = $this->getAuditRequestTransactionPHIDsFromCommitMessage(243$object);244if ($phids) {245$xactions[] = $object->getApplicationTransactionTemplate()246->setTransactionType($auditors_type)247->setNewValue(248array(249'+' => array_fuse($phids),250));251$this->addUnmentionablePHIDs($phids);252}253break;254default:255break;256}257258if (!$this->didExpandInlineState) {259switch ($xaction->getTransactionType()) {260case PhabricatorTransactions::TYPE_COMMENT:261$this->didExpandInlineState = true;262263$query_template = id(new DiffusionDiffInlineCommentQuery())264->withCommitPHIDs(array($object->getPHID()));265266$state_xaction = $this->newInlineStateTransaction(267$object,268$query_template);269270if ($state_xaction) {271$xactions[] = $state_xaction;272}273break;274}275}276277return $xactions;278}279280private function getAuditRequestTransactionPHIDsFromCommitMessage(281PhabricatorRepositoryCommit $commit) {282283$actor = $this->getActor();284$data = $commit->getCommitData();285$message = $data->getCommitMessage();286287$result = DifferentialCommitMessageParser::newStandardParser($actor)288->setRaiseMissingFieldErrors(false)289->parseFields($message);290291$field_key = DifferentialAuditorsCommitMessageField::FIELDKEY;292$phids = idx($result, $field_key, null);293294if (!$phids) {295return array();296}297298// If a commit lists its author as an auditor, just pretend it does not.299foreach ($phids as $key => $phid) {300if ($phid == $commit->getAuthorPHID()) {301unset($phids[$key]);302}303}304305if (!$phids) {306return array();307}308309return $phids;310}311312protected function sortTransactions(array $xactions) {313$xactions = parent::sortTransactions($xactions);314315$head = array();316$tail = array();317318foreach ($xactions as $xaction) {319$type = $xaction->getTransactionType();320if ($type == PhabricatorAuditActionConstants::INLINE) {321$tail[] = $xaction;322} else {323$head[] = $xaction;324}325}326327return array_values(array_merge($head, $tail));328}329330protected function supportsSearch() {331return true;332}333334protected function expandCustomRemarkupBlockTransactions(335PhabricatorLiskDAO $object,336array $xactions,337array $changes,338PhutilMarkupEngine $engine) {339340$actor = $this->getActor();341$result = array();342343// Some interactions (like "Fixes Txxx" interacting with Maniphest) have344// already been processed, so we're only re-parsing them here to avoid345// generating an extra redundant mention. Other interactions are being346// processed for the first time.347348// We're only recognizing magic in the commit message itself, not in349// audit comments.350351$is_commit = false;352foreach ($xactions as $xaction) {353switch ($xaction->getTransactionType()) {354case PhabricatorAuditTransaction::TYPE_COMMIT:355$is_commit = true;356break;357}358}359360if (!$is_commit) {361return $result;362}363364$flat_blocks = mpull($changes, 'getNewValue');365$huge_block = implode("\n\n", $flat_blocks);366$phid_map = array();367$monograms = array();368369$task_refs = id(new ManiphestCustomFieldStatusParser())370->parseCorpus($huge_block);371foreach ($task_refs as $match) {372foreach ($match['monograms'] as $monogram) {373$monograms[] = $monogram;374}375}376377$rev_refs = id(new DifferentialCustomFieldDependsOnParser())378->parseCorpus($huge_block);379foreach ($rev_refs as $match) {380foreach ($match['monograms'] as $monogram) {381$monograms[] = $monogram;382}383}384385$objects = id(new PhabricatorObjectQuery())386->setViewer($this->getActor())387->withNames($monograms)388->execute();389$phid_map[] = mpull($objects, 'getPHID', 'getPHID');390391$reverts_refs = id(new DifferentialCustomFieldRevertsParser())392->parseCorpus($huge_block);393$reverts = array_mergev(ipull($reverts_refs, 'monograms'));394if ($reverts) {395$reverted_objects = DiffusionCommitRevisionQuery::loadRevertedObjects(396$actor,397$object,398$reverts,399$object->getRepository());400401$reverted_phids = mpull($reverted_objects, 'getPHID', 'getPHID');402403$reverts_edge = DiffusionCommitRevertsCommitEdgeType::EDGECONST;404$result[] = id(new PhabricatorAuditTransaction())405->setTransactionType(PhabricatorTransactions::TYPE_EDGE)406->setMetadataValue('edge:type', $reverts_edge)407->setNewValue(array('+' => $reverted_phids));408409$phid_map[] = $reverted_phids;410}411412// See T13463. Copy "related task" edges from the associated revision, if413// one exists.414415$revision = DiffusionCommitRevisionQuery::loadRevisionForCommit(416$actor,417$object);418if ($revision) {419$task_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(420$revision->getPHID(),421DifferentialRevisionHasTaskEdgeType::EDGECONST);422$task_phids = array_fuse($task_phids);423424if ($task_phids) {425$related_edge = DiffusionCommitHasTaskEdgeType::EDGECONST;426$result[] = id(new PhabricatorAuditTransaction())427->setTransactionType(PhabricatorTransactions::TYPE_EDGE)428->setMetadataValue('edge:type', $related_edge)429->setNewValue(array('+' => $task_phids));430}431432// Mark these objects as unmentionable, since the explicit relationship433// is stronger and any mentions are redundant.434$phid_map[] = $task_phids;435}436437$phid_map = array_mergev($phid_map);438$this->addUnmentionablePHIDs($phid_map);439440return $result;441}442443protected function buildReplyHandler(PhabricatorLiskDAO $object) {444$reply_handler = new PhabricatorAuditReplyHandler();445$reply_handler->setMailReceiver($object);446return $reply_handler;447}448449protected function getMailSubjectPrefix() {450return pht('[Diffusion]');451}452453protected function getMailThreadID(PhabricatorLiskDAO $object) {454// For backward compatibility, use this legacy thread ID.455return 'diffusion-audit-'.$object->getPHID();456}457458protected function buildMailTemplate(PhabricatorLiskDAO $object) {459$identifier = $object->getCommitIdentifier();460$repository = $object->getRepository();461462$summary = $object->getSummary();463$name = $repository->formatCommitName($identifier);464465$subject = "{$name}: {$summary}";466467$template = id(new PhabricatorMetaMTAMail())468->setSubject($subject);469470$this->attachPatch(471$template,472$object);473474return $template;475}476477protected function getMailTo(PhabricatorLiskDAO $object) {478$this->requireAuditors($object);479480$phids = array();481482if ($object->getAuthorPHID()) {483$phids[] = $object->getAuthorPHID();484}485486foreach ($object->getAudits() as $audit) {487if (!$audit->isResigned()) {488$phids[] = $audit->getAuditorPHID();489}490}491492$phids[] = $this->getActingAsPHID();493494return $phids;495}496497protected function newMailUnexpandablePHIDs(PhabricatorLiskDAO $object) {498$this->requireAuditors($object);499500$phids = array();501502foreach ($object->getAudits() as $auditor) {503if ($auditor->isResigned()) {504$phids[] = $auditor->getAuditorPHID();505}506}507508return $phids;509}510511protected function getObjectLinkButtonLabelForMail(512PhabricatorLiskDAO $object) {513return pht('View Commit');514}515516protected function buildMailBody(517PhabricatorLiskDAO $object,518array $xactions) {519520$body = parent::buildMailBody($object, $xactions);521522$type_inline = PhabricatorAuditActionConstants::INLINE;523$type_push = PhabricatorAuditTransaction::TYPE_COMMIT;524525$is_commit = false;526$inlines = array();527foreach ($xactions as $xaction) {528if ($xaction->getTransactionType() == $type_inline) {529$inlines[] = $xaction;530}531if ($xaction->getTransactionType() == $type_push) {532$is_commit = true;533}534}535536if ($inlines) {537$body->addTextSection(538pht('INLINE COMMENTS'),539$this->renderInlineCommentsForMail($object, $inlines));540}541542if ($is_commit) {543$data = $object->getCommitData();544$body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles);545$this->inlinePatch(546$body,547$object);548}549550$data = $object->getCommitData();551552$user_phids = array();553554$author_phid = $object->getAuthorPHID();555if ($author_phid) {556$user_phids[$author_phid][] = pht('Author');557}558559$committer_phid = $data->getCommitDetail('committerPHID');560if ($committer_phid && ($committer_phid != $author_phid)) {561$user_phids[$committer_phid][] = pht('Committer');562}563564foreach ($this->auditorPHIDs as $auditor_phid) {565$user_phids[$auditor_phid][] = pht('Auditor');566}567568// TODO: It would be nice to show pusher here too, but that information569// is a little tricky to get at right now.570571if ($user_phids) {572$handle_phids = array_keys($user_phids);573$handles = id(new PhabricatorHandleQuery())574->setViewer($this->requireActor())575->withPHIDs($handle_phids)576->execute();577578$user_info = array();579foreach ($user_phids as $phid => $roles) {580$user_info[] = pht(581'%s (%s)',582$handles[$phid]->getName(),583implode(', ', $roles));584}585586$body->addTextSection(587pht('USERS'),588implode("\n", $user_info));589}590591$monogram = $object->getRepository()->formatCommitName(592$object->getCommitIdentifier());593594$body->addLinkSection(595pht('COMMIT'),596PhabricatorEnv::getProductionURI('/'.$monogram));597598return $body;599}600601private function attachPatch(602PhabricatorMetaMTAMail $template,603PhabricatorRepositoryCommit $commit) {604605if (!$this->getRawPatch()) {606return;607}608609$attach_key = 'metamta.diffusion.attach-patches';610$attach_patches = PhabricatorEnv::getEnvConfig($attach_key);611if (!$attach_patches) {612return;613}614615$repository = $commit->getRepository();616$encoding = $repository->getDetail('encoding', 'UTF-8');617618$raw_patch = $this->getRawPatch();619$commit_name = $repository->formatCommitName(620$commit->getCommitIdentifier());621622$template->addAttachment(623new PhabricatorMailAttachment(624$raw_patch,625$commit_name.'.patch',626'text/x-patch; charset='.$encoding));627}628629private function inlinePatch(630PhabricatorMetaMTAMailBody $body,631PhabricatorRepositoryCommit $commit) {632633if (!$this->getRawPatch()) {634return;635}636637$inline_key = 'metamta.diffusion.inline-patches';638$inline_patches = PhabricatorEnv::getEnvConfig($inline_key);639if (!$inline_patches) {640return;641}642643$repository = $commit->getRepository();644$raw_patch = $this->getRawPatch();645$result = null;646$len = substr_count($raw_patch, "\n");647if ($len <= $inline_patches) {648// We send email as utf8, so we need to convert the text to utf8 if649// we can.650$encoding = $repository->getDetail('encoding', 'UTF-8');651if ($encoding) {652$raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding);653}654$result = phutil_utf8ize($raw_patch);655}656657if ($result) {658$result = "PATCH\n\n{$result}\n";659}660$body->addRawSection($result);661}662663private function renderInlineCommentsForMail(664PhabricatorLiskDAO $object,665array $inline_xactions) {666667$inlines = mpull($inline_xactions, 'getComment');668669$block = array();670671$path_map = id(new DiffusionPathQuery())672->withPathIDs(mpull($inlines, 'getPathID'))673->execute();674$path_map = ipull($path_map, 'path', 'id');675676foreach ($inlines as $inline) {677$path = idx($path_map, $inline->getPathID());678if ($path === null) {679continue;680}681682$start = $inline->getLineNumber();683$len = $inline->getLineLength();684if ($len) {685$range = $start.'-'.($start + $len);686} else {687$range = $start;688}689690$content = $inline->getContent();691$block[] = "{$path}:{$range} {$content}";692}693694return implode("\n", $block);695}696697public function getMailTagsMap() {698return array(699PhabricatorAuditTransaction::MAILTAG_COMMIT =>700pht('A commit is created.'),701PhabricatorAuditTransaction::MAILTAG_ACTION_CONCERN =>702pht('A commit has a concerned raised against it.'),703PhabricatorAuditTransaction::MAILTAG_ACTION_ACCEPT =>704pht('A commit is accepted.'),705PhabricatorAuditTransaction::MAILTAG_ACTION_RESIGN =>706pht('A commit has an auditor resign.'),707PhabricatorAuditTransaction::MAILTAG_ACTION_CLOSE =>708pht('A commit is closed.'),709PhabricatorAuditTransaction::MAILTAG_ADD_AUDITORS =>710pht('A commit has auditors added.'),711PhabricatorAuditTransaction::MAILTAG_ADD_CCS =>712pht("A commit's subscribers change."),713PhabricatorAuditTransaction::MAILTAG_PROJECTS =>714pht("A commit's projects change."),715PhabricatorAuditTransaction::MAILTAG_COMMENT =>716pht('Someone comments on a commit.'),717PhabricatorAuditTransaction::MAILTAG_OTHER =>718pht('Other commit activity not listed above occurs.'),719);720}721722protected function shouldApplyHeraldRules(723PhabricatorLiskDAO $object,724array $xactions) {725726foreach ($xactions as $xaction) {727switch ($xaction->getTransactionType()) {728case PhabricatorAuditTransaction::TYPE_COMMIT:729$repository = $object->getRepository();730$publisher = $repository->newPublisher();731if (!$publisher->shouldPublishCommit($object)) {732return false;733}734return true;735default:736break;737}738}739return parent::shouldApplyHeraldRules($object, $xactions);740}741742protected function buildHeraldAdapter(743PhabricatorLiskDAO $object,744array $xactions) {745return id(new HeraldCommitAdapter())746->setObject($object);747}748749protected function didApplyHeraldRules(750PhabricatorLiskDAO $object,751HeraldAdapter $adapter,752HeraldTranscript $transcript) {753754$limit = self::MAX_FILES_SHOWN_IN_EMAIL;755$files = $adapter->loadAffectedPaths();756sort($files);757if (count($files) > $limit) {758array_splice($files, $limit);759$files[] = pht(760'(This commit affected more than %d files. Only %d are shown here '.761'and additional ones are truncated.)',762$limit,763$limit);764}765$this->affectedFiles = implode("\n", $files);766767return array();768}769770private function isCommitMostlyImported(PhabricatorLiskDAO $object) {771$has_message = PhabricatorRepositoryCommit::IMPORTED_MESSAGE;772$has_changes = PhabricatorRepositoryCommit::IMPORTED_CHANGE;773774// Don't publish feed stories or email about events which occur during775// import. In particular, this affects tasks being attached when they are776// closed by "Fixes Txxxx" in a commit message. See T5851.777778$mask = ($has_message | $has_changes);779780return $object->isPartiallyImported($mask);781}782783784private function shouldPublishRepositoryActivity(785PhabricatorLiskDAO $object,786array $xactions) {787788// not every code path loads the repository so tread carefully789// TODO: They should, and then we should simplify this.790$repository = $object->getRepository($assert_attached = false);791if ($repository != PhabricatorLiskDAO::ATTACHABLE) {792$publisher = $repository->newPublisher();793if (!$publisher->shouldPublishCommit($object)) {794return false;795}796}797798return $this->isCommitMostlyImported($object);799}800801protected function shouldSendMail(802PhabricatorLiskDAO $object,803array $xactions) {804return $this->shouldPublishRepositoryActivity($object, $xactions);805}806807protected function shouldEnableMentions(808PhabricatorLiskDAO $object,809array $xactions) {810return $this->shouldPublishRepositoryActivity($object, $xactions);811}812813protected function shouldPublishFeedStory(814PhabricatorLiskDAO $object,815array $xactions) {816return $this->shouldPublishRepositoryActivity($object, $xactions);817}818819protected function getCustomWorkerState() {820return array(821'rawPatch' => $this->rawPatch,822'affectedFiles' => $this->affectedFiles,823'auditorPHIDs' => $this->auditorPHIDs,824);825}826827protected function getCustomWorkerStateEncoding() {828return array(829'rawPatch' => self::STORAGE_ENCODING_BINARY,830);831}832833protected function loadCustomWorkerState(array $state) {834$this->rawPatch = idx($state, 'rawPatch');835$this->affectedFiles = idx($state, 'affectedFiles');836$this->auditorPHIDs = idx($state, 'auditorPHIDs');837return $this;838}839840protected function willPublish(PhabricatorLiskDAO $object, array $xactions) {841return id(new DiffusionCommitQuery())842->setViewer($this->requireActor())843->withIDs(array($object->getID()))844->needAuditRequests(true)845->needCommitData(true)846->executeOne();847}848849private function requireAuditors(PhabricatorRepositoryCommit $commit) {850if ($commit->hasAttachedAudits()) {851return;852}853854$with_auditors = id(new DiffusionCommitQuery())855->setViewer($this->getActor())856->needAuditRequests(true)857->withPHIDs(array($commit->getPHID()))858->executeOne();859if (!$with_auditors) {860throw new Exception(861pht(862'Failed to reload commit ("%s").',863$commit->getPHID()));864}865866$commit->attachAudits($with_auditors->getAudits());867}868869}870871872