diff --git a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php index 5567af5774..f5aeb49c17 100644 --- a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php @@ -1,58 +1,57 @@ getPHID()] = true; $owned_packages = id(new PhabricatorOwnersPackageQuery()) ->setViewer($user) ->withOwnerPHIDs(array($user->getPHID())) ->execute(); foreach ($owned_packages as $package) { $phids[$package->getPHID()] = true; } // The user can audit on behalf of all projects they are a member of. $projects = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withMemberPHIDs(array($user->getPHID())) ->execute(); foreach ($projects as $project) { $phids[$project->getPHID()] = true; } return array_keys($phids); } public static function newReplyHandlerForCommit($commit) { - $reply_handler = PhabricatorEnv::newObjectFromConfig( - 'metamta.diffusion.reply-handler'); + $reply_handler = new PhabricatorAuditReplyHandler(); $reply_handler->setMailReceiver($commit); return $reply_handler; } public static function getMailThreading( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { return array( 'diffusion-audit-'.$commit->getPHID(), 'Commit r'.$repository->getCallsign().$commit->getCommitIdentifier(), ); } } diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index d98f866afa..4ce57bd95c 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -1,1000 +1,998 @@ auditReasonMap[$phid])) { $this->auditReasonMap[$phid] = array(); } $this->auditReasonMap[$phid][] = $reason; return $this; } private function getAuditReasons($phid) { if (isset($this->auditReasonMap[$phid])) { return $this->auditReasonMap[$phid]; } if ($this->getIsHeraldEditor()) { $name = 'herald'; } else { $name = $this->getActor()->getUsername(); } return array('Added by '.$name.'.'); } public function setRawPatch($patch) { $this->rawPatch = $patch; return $this; } public function getRawPatch() { return $this->rawPatch; } public function getEditorApplicationClass() { return 'PhabricatorAuditApplication'; } public function getEditorObjectsDescription() { return pht('Audits'); } public function getTransactionTypes() { $types = parent::getTransactionTypes(); $types[] = PhabricatorTransactions::TYPE_COMMENT; $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_INLINESTATE; $types[] = PhabricatorAuditTransaction::TYPE_COMMIT; // TODO: These will get modernized eventually, but that can happen one // at a time later on. $types[] = PhabricatorAuditActionConstants::ACTION; $types[] = PhabricatorAuditActionConstants::INLINE; $types[] = PhabricatorAuditActionConstants::ADD_AUDITORS; return $types; } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::INLINE: return $xaction->hasComment(); } return parent::transactionHasEffect($object, $xaction); } protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditTransaction::TYPE_COMMIT: return null; case PhabricatorAuditActionConstants::ADD_AUDITORS: // TODO: For now, just record the added PHIDs. Eventually, turn these // into real edge transactions, probably? return array(); } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditActionConstants::ADD_AUDITORS: case PhabricatorAuditTransaction::TYPE_COMMIT: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditActionConstants::ADD_AUDITORS: case PhabricatorAuditTransaction::TYPE_COMMIT: return; } return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case PhabricatorTransactions::TYPE_SUBSCRIBERS: case PhabricatorTransactions::TYPE_EDGE: case PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditTransaction::TYPE_COMMIT: return; case PhabricatorAuditActionConstants::INLINE: $reply = $xaction->getComment()->getReplyToComment(); if ($reply && !$reply->getHasReplies()) { $reply->setHasReplies(1)->save(); } return; case PhabricatorAuditActionConstants::ADD_AUDITORS: $new = $xaction->getNewValue(); if (!is_array($new)) { $new = array(); } $old = $xaction->getOldValue(); if (!is_array($old)) { $old = array(); } $add = array_diff_key($new, $old); $actor = $this->requireActor(); $requests = $object->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); foreach ($add as $phid) { if (isset($requests[$phid])) { continue; } if ($this->getIsHeraldEditor()) { $audit_requested = $xaction->getMetadataValue('auditStatus'); $audit_reason_map = $xaction->getMetadataValue('auditReasonMap'); $audit_reason = $audit_reason_map[$phid]; } else { $audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED; $audit_reason = $this->getAuditReasons($phid); } $requests[] = id (new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($object->getPHID()) ->setAuditorPHID($phid) ->setAuditStatus($audit_requested) ->setAuditReasons($audit_reason) ->save(); } $object->attachAudits($requests); return; } return parent::applyCustomExternalTransaction($object, $xaction); } protected function applyBuiltinExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_INLINESTATE: $table = new PhabricatorAuditTransactionComment(); $conn_w = $table->establishConnection('w'); foreach ($xaction->getNewValue() as $phid => $state) { queryfx( $conn_w, 'UPDATE %T SET fixedState = %s WHERE phid = %s', $table->getTableName(), $state, $phid); } return; } return parent::applyBuiltinExternalTransaction($object, $xaction); } protected function applyFinalEffects( PhabricatorLiskDAO $object, array $xactions) { // Load auditors explicitly; we may not have them if the caller was a // generic piece of infrastructure. $commit = id(new DiffusionCommitQuery()) ->setViewer($this->requireActor()) ->withIDs(array($object->getID())) ->needAuditRequests(true) ->executeOne(); if (!$commit) { throw new Exception( pht('Failed to load commit during transaction finalization!')); } $object->attachAudits($commit->getAudits()); $status_concerned = PhabricatorAuditStatusConstants::CONCERNED; $status_closed = PhabricatorAuditStatusConstants::CLOSED; $status_resigned = PhabricatorAuditStatusConstants::RESIGNED; $status_accepted = PhabricatorAuditStatusConstants::ACCEPTED; $status_concerned = PhabricatorAuditStatusConstants::CONCERNED; $actor_phid = $this->getActingAsPHID(); $actor_is_author = ($object->getAuthorPHID()) && ($actor_phid == $object->getAuthorPHID()); $import_status_flag = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $import_status_flag = PhabricatorRepositoryCommit::IMPORTED_HERALD; break; case PhabricatorAuditActionConstants::ACTION: $new = $xaction->getNewValue(); switch ($new) { case PhabricatorAuditActionConstants::CLOSE: // "Close" means wipe out all the concerns. $requests = $object->getAudits(); foreach ($requests as $request) { if ($request->getAuditStatus() == $status_concerned) { $request ->setAuditStatus($status_closed) ->save(); } } break; case PhabricatorAuditActionConstants::RESIGN: $requests = $object->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); $actor_request = idx($requests, $actor_phid); // If the actor doesn't currently have a relationship to the // commit, add one explicitly. For example, this allows members // of a project to resign from a commit and have it drop out of // their queue. if (!$actor_request) { $actor_request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($object->getPHID()) ->setAuditorPHID($actor_phid); $requests[] = $actor_request; $object->attachAudits($requests); } $actor_request ->setAuditStatus($status_resigned) ->save(); break; case PhabricatorAuditActionConstants::ACCEPT: case PhabricatorAuditActionConstants::CONCERN: if ($new == PhabricatorAuditActionConstants::ACCEPT) { $new_status = $status_accepted; } else { $new_status = $status_concerned; } $requests = $object->getAudits(); $requests = mpull($requests, null, 'getAuditorPHID'); // Figure out which requests the actor has authority over: these // are user requests where they are the auditor, and packages // and projects they are a member of. if ($actor_is_author) { // When modifying your own commits, you act only on behalf of // yourself, not your packages/projects -- the idea being that // you can't accept your own commits. $authority_phids = array($actor_phid); } else { $authority_phids = PhabricatorAuditCommentEditor::loadAuditPHIDsForUser( $this->requireActor()); } $authority = array_select_keys( $requests, $authority_phids); if (!$authority) { // If the actor has no authority over any existing requests, // create a new request for them. $actor_request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($object->getPHID()) ->setAuditorPHID($actor_phid) ->setAuditStatus($new_status) ->save(); $requests[$actor_phid] = $actor_request; $object->attachAudits($requests); } else { // Otherwise, update the audit status of the existing requests. foreach ($authority as $request) { $request ->setAuditStatus($new_status) ->save(); } } break; } break; } } $requests = $object->getAudits(); $object->updateAuditStatus($requests); $object->save(); if ($import_status_flag) { $object->writeImportStatusFlag($import_status_flag); } return $xactions; } protected function expandTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $xactions = parent::expandTransaction($object, $xaction); switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $request = $this->createAuditRequestTransactionFromCommitMessage( $object); if ($request) { $xactions[] = $request; $this->setUnmentionablePHIDMap($request->getNewValue()); } break; default: break; } if (!$this->didExpandInlineState) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: case PhabricatorAuditActionConstants::ACTION: $this->didExpandInlineState = true; $actor_phid = $this->getActingAsPHID(); $actor_is_author = ($object->getAuthorPHID() == $actor_phid); if (!$actor_is_author) { break; } $state_map = PhabricatorTransactions::getInlineStateMap(); $inlines = id(new DiffusionDiffInlineCommentQuery()) ->setViewer($this->getActor()) ->withCommitPHIDs(array($object->getPHID())) ->withFixedStates(array_keys($state_map)) ->execute(); if (!$inlines) { break; } $old_value = mpull($inlines, 'getFixedState', 'getPHID'); $new_value = array(); foreach ($old_value as $key => $state) { $new_value[$key] = $state_map[$state]; } $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_INLINESTATE) ->setIgnoreOnNoEffect(true) ->setOldValue($old_value) ->setNewValue($new_value); break; } } return $xactions; } private function createAuditRequestTransactionFromCommitMessage( PhabricatorRepositoryCommit $commit) { $data = $commit->getCommitData(); $message = $data->getCommitMessage(); $matches = null; if (!preg_match('/^Auditors:\s*(.*)$/im', $message, $matches)) { return array(); } $phids = id(new PhabricatorObjectListQuery()) ->setViewer($this->getActor()) ->setAllowPartialResults(true) ->setAllowedTypes( array( PhabricatorPeopleUserPHIDType::TYPECONST, PhabricatorProjectProjectPHIDType::TYPECONST, )) ->setObjectList($matches[1]) ->execute(); if (!$phids) { return array(); } foreach ($phids as $phid) { $this->addAuditReason($phid, 'Requested by Author'); } return id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS) ->setNewValue(array_fuse($phids)); } protected function sortTransactions(array $xactions) { $xactions = parent::sortTransactions($xactions); $head = array(); $tail = array(); foreach ($xactions as $xaction) { $type = $xaction->getTransactionType(); if ($type == PhabricatorAuditActionConstants::INLINE) { $tail[] = $xaction; } else { $head[] = $xaction; } } return array_values(array_merge($head, $tail)); } protected function validateTransaction( PhabricatorLiskDAO $object, $type, array $xactions) { $errors = parent::validateTransaction($object, $type, $xactions); foreach ($xactions as $xaction) { switch ($type) { case PhabricatorAuditActionConstants::ACTION: $error = $this->validateAuditAction( $object, $type, $xaction, $xaction->getNewValue()); if ($error) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), $error, $xaction); } break; } } return $errors; } private function validateAuditAction( PhabricatorLiskDAO $object, $type, PhabricatorAuditTransaction $xaction, $action) { $can_author_close_key = 'audit.can-author-close-audit'; $can_author_close = PhabricatorEnv::getEnvConfig($can_author_close_key); $actor_is_author = ($object->getAuthorPHID()) && ($object->getAuthorPHID() == $this->getActingAsPHID()); switch ($action) { case PhabricatorAuditActionConstants::CLOSE: if (!$actor_is_author) { return pht( 'You can not close this audit because you are not the author '. 'of the commit.'); } if (!$can_author_close) { return pht( 'You can not close this audit because "%s" is disabled in '. 'the Phabricator configuration.', $can_author_close_key); } break; } return null; } protected function supportsSearch() { return true; } protected function expandCustomRemarkupBlockTransactions( PhabricatorLiskDAO $object, array $xactions, $blocks, PhutilMarkupEngine $engine) { // we are only really trying to find unmentionable phids here... // don't bother with this outside initial commit (i.e. create) // transaction $is_commit = false; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $is_commit = true; break; } } // "result" is always an array.... $result = array(); if (!$is_commit) { return $result; } $flat_blocks = array_mergev($blocks); $huge_block = implode("\n\n", $flat_blocks); $phid_map = array(); $phid_map[] = $this->getUnmentionablePHIDMap(); $monograms = array(); $task_refs = id(new ManiphestCustomFieldStatusParser()) ->parseCorpus($huge_block); foreach ($task_refs as $match) { foreach ($match['monograms'] as $monogram) { $monograms[] = $monogram; } } $rev_refs = id(new DifferentialCustomFieldDependsOnParser()) ->parseCorpus($huge_block); foreach ($rev_refs as $match) { foreach ($match['monograms'] as $monogram) { $monograms[] = $monogram; } } $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getActor()) ->withNames($monograms) ->execute(); $phid_map[] = mpull($objects, 'getPHID', 'getPHID'); $phid_map = array_mergev($phid_map); $this->setUnmentionablePHIDMap($phid_map); return $result; } - protected function buildReplyHandler(PhabricatorLiskDAO $object) { - $reply_handler = PhabricatorEnv::newObjectFromConfig( - 'metamta.diffusion.reply-handler'); + $reply_handler = new PhabricatorAuditReplyHandler(); $reply_handler->setMailReceiver($object); return $reply_handler; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { // For backward compatibility, use this legacy thread ID. return 'diffusion-audit-'.$object->getPHID(); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $identifier = $object->getCommitIdentifier(); $repository = $object->getRepository(); $monogram = $repository->getMonogram(); $summary = $object->getSummary(); $name = $repository->formatCommitName($identifier); $subject = "{$name}: {$summary}"; $thread_topic = "Commit {$monogram}{$identifier}"; $template = id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->addHeader('Thread-Topic', $thread_topic); $this->attachPatch( $template, $object); return $template; } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); if ($this->heraldEmailPHIDs) { $phids = $this->heraldEmailPHIDs; } if ($object->getAuthorPHID()) { $phids[] = $object->getAuthorPHID(); } $status_resigned = PhabricatorAuditStatusConstants::RESIGNED; foreach ($object->getAudits() as $audit) { if ($audit->getAuditStatus() != $status_resigned) { $phids[] = $audit->getAuditorPHID(); } } return $phids; } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); $type_inline = PhabricatorAuditActionConstants::INLINE; $type_push = PhabricatorAuditTransaction::TYPE_COMMIT; $is_commit = false; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } if ($xaction->getTransactionType() == $type_push) { $is_commit = true; } } if ($inlines) { $body->addTextSection( pht('INLINE COMMENTS'), $this->renderInlineCommentsForMail($object, $inlines)); } if ($is_commit) { $data = $object->getCommitData(); $body->addTextSection(pht('AFFECTED FILES'), $this->affectedFiles); $this->inlinePatch( $body, $object); } // Reload the commit to pull commit data. $commit = id(new DiffusionCommitQuery()) ->setViewer($this->requireActor()) ->withIDs(array($object->getID())) ->needCommitData(true) ->executeOne(); $data = $commit->getCommitData(); $user_phids = array(); $author_phid = $commit->getAuthorPHID(); if ($author_phid) { $user_phids[$author_phid][] = pht('Author'); } $committer_phid = $data->getCommitDetail('committerPHID'); if ($committer_phid && ($committer_phid != $author_phid)) { $user_phids[$committer_phid][] = pht('Committer'); } // we loaded this in applyFinalEffects $audit_requests = $object->getAudits(); $auditor_phids = mpull($audit_requests, 'getAuditorPHID'); foreach ($auditor_phids as $auditor_phid) { $user_phids[$auditor_phid][] = pht('Auditor'); } // TODO: It would be nice to show pusher here too, but that information // is a little tricky to get at right now. if ($user_phids) { $handle_phids = array_keys($user_phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($handle_phids) ->execute(); $user_info = array(); foreach ($user_phids as $phid => $roles) { $user_info[] = pht( '%s (%s)', $handles[$phid]->getName(), implode(', ', $roles)); } $body->addTextSection( pht('USERS'), implode("\n", $user_info)); } $monogram = $object->getRepository()->formatCommitName( $object->getCommitIdentifier()); $body->addLinkSection( pht('COMMIT'), PhabricatorEnv::getProductionURI('/'.$monogram)); return $body; } private function attachPatch( PhabricatorMetaMTAMail $template, PhabricatorRepositoryCommit $commit) { if (!$this->getRawPatch()) { return; } $attach_key = 'metamta.diffusion.attach-patches'; $attach_patches = PhabricatorEnv::getEnvConfig($attach_key); if (!$attach_patches) { return; } $repository = $commit->getRepository(); $encoding = $repository->getDetail('encoding', 'UTF-8'); $raw_patch = $this->getRawPatch(); $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $template->addAttachment( new PhabricatorMetaMTAAttachment( $raw_patch, $commit_name.'.patch', 'text/x-patch; charset='.$encoding)); } private function inlinePatch( PhabricatorMetaMTAMailBody $body, PhabricatorRepositoryCommit $commit) { if (!$this->getRawPatch()) { return; } $inline_key = 'metamta.diffusion.inline-patches'; $inline_patches = PhabricatorEnv::getEnvConfig($inline_key); if (!$inline_patches) { return; } $repository = $commit->getRepository(); $raw_patch = $this->getRawPatch(); $result = null; $len = substr_count($raw_patch, "\n"); if ($len <= $inline_patches) { // We send email as utf8, so we need to convert the text to utf8 if // we can. $encoding = $repository->getDetail('encoding', 'UTF-8'); if ($encoding) { $raw_patch = phutil_utf8_convert($raw_patch, 'UTF-8', $encoding); } $result = phutil_utf8ize($raw_patch); } if ($result) { $result = "PATCH\n\n{$result}\n"; } $body->addRawSection($result); } private function renderInlineCommentsForMail( PhabricatorLiskDAO $object, array $inline_xactions) { $inlines = mpull($inline_xactions, 'getComment'); $block = array(); $path_map = id(new DiffusionPathQuery()) ->withPathIDs(mpull($inlines, 'getPathID')) ->execute(); $path_map = ipull($path_map, 'path', 'id'); foreach ($inlines as $inline) { $path = idx($path_map, $inline->getPathID()); if ($path === null) { continue; } $start = $inline->getLineNumber(); $len = $inline->getLineLength(); if ($len) { $range = $start.'-'.($start + $len); } else { $range = $start; } $content = $inline->getContent(); $block[] = "{$path}:{$range} {$content}"; } return implode("\n", $block); } public function getMailTagsMap() { return array( PhabricatorAuditTransaction::MAILTAG_COMMIT => pht('A commit is created.'), PhabricatorAuditTransaction::MAILTAG_ACTION_CONCERN => pht('A commit has a concerned raised against it.'), PhabricatorAuditTransaction::MAILTAG_ACTION_ACCEPT => pht('A commit is accepted.'), PhabricatorAuditTransaction::MAILTAG_ACTION_RESIGN => pht('A commit has an auditor resign.'), PhabricatorAuditTransaction::MAILTAG_ACTION_CLOSE => pht('A commit is closed.'), PhabricatorAuditTransaction::MAILTAG_ADD_AUDITORS => pht('A commit has auditors added.'), PhabricatorAuditTransaction::MAILTAG_ADD_CCS => pht("A commit's subscribers change."), PhabricatorAuditTransaction::MAILTAG_PROJECTS => pht("A commit's projects change."), PhabricatorAuditTransaction::MAILTAG_COMMENT => pht('Someone comments on a commit.'), PhabricatorAuditTransaction::MAILTAG_OTHER => pht('Other commit activity not listed above occurs.'), ); } protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorAuditTransaction::TYPE_COMMIT: $repository = $object->getRepository(); if (!$repository->shouldPublish()) { return false; } return true; default: break; } } return parent::shouldApplyHeraldRules($object, $xactions); } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldCommitAdapter()) ->setCommit($object); } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { $xactions = array(); $audit_phids = $adapter->getAuditMap(); foreach ($audit_phids as $phid => $rule_ids) { foreach ($rule_ids as $rule_id) { $this->addAuditReason( $phid, pht( '%s Triggered Audit', "H{$rule_id}")); } } if ($audit_phids) { $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorAuditActionConstants::ADD_AUDITORS) ->setNewValue(array_fuse(array_keys($audit_phids))) ->setMetadataValue( 'auditStatus', PhabricatorAuditStatusConstants::AUDIT_REQUIRED) ->setMetadataValue( 'auditReasonMap', $this->auditReasonMap); } $cc_phids = $adapter->getAddCCMap(); $add_ccs = array('+' => array()); foreach ($cc_phids as $phid => $rule_ids) { $add_ccs['+'][$phid] = $phid; } $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue($add_ccs); $this->heraldEmailPHIDs = $adapter->getEmailPHIDs(); HarbormasterBuildable::applyBuildPlans( $object->getPHID(), $object->getRepository()->getPHID(), $adapter->getBuildPlans()); $limit = self::MAX_FILES_SHOWN_IN_EMAIL; $files = $adapter->loadAffectedPaths(); sort($files); if (count($files) > $limit) { array_splice($files, $limit); $files[] = pht( '(This commit affected more than %d files. Only %d are shown here '. 'and additional ones are truncated.)', $limit, $limit); } $this->affectedFiles = implode("\n", $files); return $xactions; } private function isCommitMostlyImported(PhabricatorLiskDAO $object) { $has_message = PhabricatorRepositoryCommit::IMPORTED_MESSAGE; $has_changes = PhabricatorRepositoryCommit::IMPORTED_CHANGE; // Don't publish feed stories or email about events which occur during // import. In particular, this affects tasks being attached when they are // closed by "Fixes Txxxx" in a commit message. See T5851. $mask = ($has_message | $has_changes); return $object->isPartiallyImported($mask); } private function shouldPublishRepositoryActivity( PhabricatorLiskDAO $object, array $xactions) { // not every code path loads the repository so tread carefully // TODO: They should, and then we should simplify this. if ($object->getRepository($assert_attached = false)) { $repository = $object->getRepository(); if (!$repository->shouldPublish()) { return false; } } return $this->isCommitMostlyImported($object); } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function shouldEnableMentions( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { return $this->shouldPublishRepositoryActivity($object, $xactions); } } diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index df60dab08d..c7fde5f6e4 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -1,240 +1,248 @@ newIssue('config.unknown.'.$key) ->setShortName($short) ->setName($name) ->setSummary($summary); $stack = PhabricatorEnv::getConfigSourceStack(); $stack = $stack->getStack(); $found = array(); $found_local = false; $found_database = false; foreach ($stack as $source_key => $source) { $value = $source->getKeys(array($key)); if ($value) { $found[] = $source->getName(); if ($source instanceof PhabricatorConfigDatabaseSource) { $found_database = true; } if ($source instanceof PhabricatorConfigLocalSource) { $found_local = true; } } } $message = $message."\n\n".pht( 'This configuration value is defined in these %d '. 'configuration source(s): %s.', count($found), implode(', ', $found)); $issue->setMessage($message); if ($found_local) { $command = csprintf('phabricator/ $ ./bin/config delete %s', $key); $issue->addCommand($command); } if ($found_database) { $issue->addPhabricatorConfig($key); } } } /** * Return a map of deleted config options. Keys are option keys; values are * explanations of what happened to the option. */ public static function getAncientConfig() { $reason_auth = pht( 'This option has been migrated to the "Auth" application. Your old '. 'configuration is still in effect, but now stored in "Auth" instead of '. 'configuration. Going forward, you can manage authentication from '. 'the web UI.'); $auth_config = array( 'controller.oauth-registration', 'auth.password-auth-enabled', 'facebook.auth-enabled', 'facebook.registration-enabled', 'facebook.auth-permanent', 'facebook.application-id', 'facebook.application-secret', 'facebook.require-https-auth', 'github.auth-enabled', 'github.registration-enabled', 'github.auth-permanent', 'github.application-id', 'github.application-secret', 'google.auth-enabled', 'google.registration-enabled', 'google.auth-permanent', 'google.application-id', 'google.application-secret', 'ldap.auth-enabled', 'ldap.hostname', 'ldap.port', 'ldap.base_dn', 'ldap.search_attribute', 'ldap.search-first', 'ldap.username-attribute', 'ldap.real_name_attributes', 'ldap.activedirectory_domain', 'ldap.version', 'ldap.referrals', 'ldap.anonymous-user-name', 'ldap.anonymous-user-password', 'ldap.start-tls', 'disqus.auth-enabled', 'disqus.registration-enabled', 'disqus.auth-permanent', 'disqus.application-id', 'disqus.application-secret', 'phabricator.oauth-uri', 'phabricator.auth-enabled', 'phabricator.registration-enabled', 'phabricator.auth-permanent', 'phabricator.application-id', 'phabricator.application-secret', ); $ancient_config = array_fill_keys($auth_config, $reason_auth); $markup_reason = pht( 'Custom remarkup rules are now added by subclassing '. 'PhabricatorRemarkupCustomInlineRule or '. 'PhabricatorRemarkupCustomBlockRule.'); $session_reason = pht( 'Sessions now expire and are garbage collected rather than having an '. 'arbitrary concurrency limit.'); $differential_field_reason = pht( 'All Differential fields are now managed through the configuration '. 'option "%s". Use that option to configure which fields are shown.', 'differential.fields'); $reply_domain_reason = pht( 'Individual application reply handler domains have been removed. '. 'Configure a reply domain with "%s".', 'metamta.reply-handler-domain'); + $reply_handler_reason = pht( + 'Reply handlers can no longer be overridden with configuration.'); + $ancient_config += array( 'phid.external-loaders' => pht( 'External loaders have been replaced. Extend `PhabricatorPHIDType` '. 'to implement new PHID and handle types.'), 'maniphest.custom-task-extensions-class' => pht( 'Maniphest fields are now loaded automatically. You can configure '. 'them with `maniphest.fields`.'), 'maniphest.custom-fields' => pht( 'Maniphest fields are now defined in '. '`maniphest.custom-field-definitions`. Existing definitions have '. 'been migrated.'), 'differential.custom-remarkup-rules' => $markup_reason, 'differential.custom-remarkup-block-rules' => $markup_reason, 'auth.sshkeys.enabled' => pht( 'SSH keys are now actually useful, so they are always enabled.'), 'differential.anonymous-access' => pht( 'Phabricator now has meaningful global access controls. See '. '`policy.allow-public`.'), 'celerity.resource-path' => pht( 'An alternate resource map is no longer supported. Instead, use '. 'multiple maps. See T4222.'), 'metamta.send-immediately' => pht( 'Mail is now always delivered by the daemons.'), 'auth.sessions.conduit' => $session_reason, 'auth.sessions.web' => $session_reason, 'tokenizer.ondemand' => pht( 'Phabricator now manages typeahead strategies automatically.'), 'differential.revision-custom-detail-renderer' => pht( 'Obsolete; use standard rendering events instead.'), 'differential.show-host-field' => $differential_field_reason, 'differential.show-test-plan-field' => $differential_field_reason, 'differential.field-selector' => $differential_field_reason, 'phabricator.show-beta-applications' => pht( 'This option has been renamed to `phabricator.show-prototypes` '. 'to emphasize the unfinished nature of many prototype applications. '. 'Your existing setting has been migrated.'), 'notification.user' => pht( 'The notification server no longer requires root permissions. Start '. 'the server as the user you want it to run under.'), 'notification.debug' => pht( 'Notifications no longer have a dedicated debugging mode.'), 'translation.provider' => pht( 'The translation implementation has changed and providers are no '. 'longer used or supported.'), 'config.mask' => pht( 'Use `config.hide` instead of this option.'), 'phd.start-taskmasters' => pht( 'Taskmasters now use an autoscaling pool. You can configure the '. 'pool size with `phd.taskmasters`.'), 'storage.engine-selector' => pht( 'Phabricator now automatically discovers available storage engines '. 'at runtime.'), 'storage.upload-size-limit' => pht( 'Phabricator now supports arbitrarily large files. Consult the '. 'documentation for configuration details.'), 'security.allow-outbound-http' => pht( 'This option has been replaced with the more granular option '. '`security.outbound-blacklist`.'), 'metamta.reply.show-hints' => pht( 'Phabricator no longer shows reply hints in mail.'), 'metamta.differential.reply-handler-domain' => $reply_domain_reason, 'metamta.diffusion.reply-handler-domain' => $reply_domain_reason, 'metamta.macro.reply-handler-domain' => $reply_domain_reason, 'metamta.maniphest.reply-handler-domain' => $reply_domain_reason, 'metamta.pholio.reply-handler-domain' => $reply_domain_reason, + + 'metamta.diffusion.reply-handler' => $reply_handler_reason, + 'metamta.differential.reply-handler' => $reply_handler_reason, + 'metamta.maniphest.reply-handler' => $reply_handler_reason, + 'metamta.package.reply-handler' => $reply_handler_reason, ); return $ancient_config; } } diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php index 08492aa644..6169d0df88 100644 --- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php +++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php @@ -1,311 +1,304 @@ getFieldKey()] = array( 'disabled' => $field->shouldDisableByDefault(), ); } return array( $this->newOption( 'differential.fields', $custom_field_type, $default_fields) ->setCustomData( id(new DifferentialRevision())->getCustomFieldBaseClass()) ->setDescription( pht( "Select and reorder revision fields.\n\n". "NOTE: This feature is under active development and subject ". "to change.")), $this->newOption( 'differential.whitespace-matters', 'list', array( '/\.py$/', '/\.l?hs$/', )) ->setDescription( pht( "List of file regexps where whitespace is meaningful and should ". "not use 'ignore-all' by default")), $this->newOption('differential.require-test-plan-field', 'bool', true) ->setBoolOptions( array( pht("Require 'Test Plan' field"), pht("Make 'Test Plan' field optional"), )) ->setSummary(pht('Require "Test Plan" field?')) ->setDescription( pht( "Differential has a required 'Test Plan' field by default. You ". "can make it optional by setting this to false. You can also ". "completely remove it above, if you prefer.")), $this->newOption('differential.enable-email-accept', 'bool', false) ->setBoolOptions( array( pht('Enable Email "!accept" Action'), pht('Disable Email "!accept" Action'), )) ->setSummary(pht('Enable or disable "!accept" action via email.')) ->setDescription( pht( 'If inbound email is configured, users can interact with '. 'revisions by using "!actions" in email replies (for example, '. '"!resign" or "!rethink"). However, by default, users may not '. '"!accept" revisions via email: email authentication can be '. 'configured to be very weak, and email "!accept" is kind of '. 'sketchy and implies the revision may not actually be receiving '. 'thorough review. You can enable "!accept" by setting this '. 'option to true.')), $this->newOption('differential.generated-paths', 'list', array()) ->setSummary(pht('File regexps to treat as automatically generated.')) ->setDescription( pht( 'List of file regexps that should be treated as if they are '. 'generated by an automatic process, and thus be hidden by '. 'default in Differential.'. "\n\n". 'NOTE: This property is cached, so you will need to purge the '. 'cache after making changes if you want the new configuration '. 'to affect existing revisions. For instructions, see '. '**[[ %s | Managing Caches ]]** in the documentation.', $caches_href)) ->addExample("/config\.h$/\n#/autobuilt/#", pht('Valid Setting')), $this->newOption('differential.sticky-accept', 'bool', true) ->setBoolOptions( array( pht('Accepts persist across updates'), pht('Accepts are reset by updates'), )) ->setSummary( pht('Should "Accepted" revisions remain "Accepted" after updates?')) ->setDescription( pht( 'Normally, when revisions that have been "Accepted" are updated, '. 'they remain "Accepted". This allows reviewers to suggest minor '. 'alterations when accepting, and encourages authors to update '. 'if they make minor changes in response to this feedback.'. "\n\n". 'If you want updates to always require re-review, you can disable '. 'the "stickiness" of the "Accepted" status with this option. '. 'This may make the process for minor changes much more burdensome '. 'to both authors and reviewers.')), $this->newOption('differential.allow-self-accept', 'bool', false) ->setBoolOptions( array( pht('Allow self-accept'), pht('Disallow self-accept'), )) ->setSummary(pht('Allows users to accept their own revisions.')) ->setDescription( pht( "If you set this to true, users can accept their own revisions. ". "This action is disabled by default because it's most likely not ". "a behavior you want, but it proves useful if you are working ". "alone on a project and want to make use of all of ". "differential's features.")), $this->newOption('differential.always-allow-close', 'bool', false) ->setBoolOptions( array( pht('Allow any user'), pht('Restrict to submitter'), )) ->setSummary(pht('Allows any user to close accepted revisions.')) ->setDescription( pht( 'If you set this to true, any user can close any revision so '. 'long as it has been accepted. This can be useful depending on '. 'your development model. For example, github-style pull requests '. 'where the reviewer is often the actual committer can benefit '. 'from turning this option to true. If false, only the submitter '. 'can close a revision.')), $this->newOption('differential.always-allow-abandon', 'bool', false) ->setBoolOptions( array( pht('Allow any user'), pht('Restrict to submitter'), )) ->setSummary(pht('Allows any user to abandon revisions.')) ->setDescription( pht( 'If you set this to true, any user can abandon any revision. If '. 'false, only the submitter can abandon a revision.')), $this->newOption('differential.allow-reopen', 'bool', false) ->setBoolOptions( array( pht('Enable reopen'), pht('Disable reopen'), )) ->setSummary(pht('Allows any user to reopen a closed revision.')) ->setDescription( pht('If you set this to true, any user can reopen a revision so '. 'long as it has been closed. This can be useful if a revision '. 'is accidentally closed or if a developer changes his or her '. 'mind after closing a revision. If it is false, reopening '. 'is not allowed.')), $this->newOption('differential.close-on-accept', 'bool', false) ->setBoolOptions( array( pht('Treat Accepted Revisions as "Closed"'), pht('Treat Accepted Revisions as "Open"'), )) ->setSummary(pht('Allows "Accepted" to act as a closed status.')) ->setDescription( pht( 'Normally, Differential revisions remain on the dashboard when '. 'they are "Accepted", and the author then commits the changes '. 'to "Close" the revision and move it off the dashboard.'. "\n\n". 'If you have an unusual workflow where Differential is used for '. 'post-commit review (normally called "Audit", elsewhere in '. 'Phabricator), you can set this flag to treat the "Accepted" '. 'state as a "Closed" state and end the review workflow early.'. "\n\n". 'This sort of workflow is very unusual. Very few installs should '. 'need to change this option.')), $this->newOption('differential.days-fresh', 'int', 1) ->setSummary( pht( "For how many business days should a revision be considered ". "'fresh'?")) ->setDescription( pht( 'Revisions newer than this number of days are marked as fresh in '. 'Action Required and Revisions Waiting on You views. Only work '. 'days (not weekends and holidays) are included. Set to 0 to '. 'disable this feature.')), $this->newOption('differential.days-stale', 'int', 3) ->setSummary( pht("After this many days, a revision will be considered 'stale'.")) ->setDescription( pht( "Similar to `differential.days-fresh` but marks stale revisions. ". "If the revision is even older than it is when marked as 'old'.")), - $this->newOption( - 'metamta.differential.reply-handler', - 'class', - 'DifferentialReplyHandler') - ->setLocked(true) - ->setBaseClass('PhabricatorMailReplyHandler') - ->setDescription(pht('Alternate reply handler class.')), $this->newOption( 'metamta.differential.subject-prefix', 'string', '[Differential]') ->setDescription(pht('Subject prefix for Differential mail.')), $this->newOption( 'metamta.differential.attach-patches', 'bool', false) ->setBoolOptions( array( pht('Attach Patches'), pht('Do Not Attach Patches'), )) ->setSummary(pht('Attach patches to email, as text attachments.')) ->setDescription( pht( 'If you set this to true, Phabricator will attach patches to '. 'Differential mail (as text attachments). This will not work if '. 'you are using SendGrid as your mail adapter.')), $this->newOption( 'metamta.differential.inline-patches', 'int', 0) ->setSummary(pht('Inline patches in email, as body text.')) ->setDescription( pht( "To include patches inline in email bodies, set this to a ". "positive integer. Patches will be inlined if they are at most ". "that many lines. For instance, a value of 100 means 'inline ". "patches if they are no longer than 100 lines'. By default, ". "patches are not inlined.")), // TODO: Implement 'enum'? Options are 'unified' or 'git'. $this->newOption( 'metamta.differential.patch-format', 'string', 'unified') ->setDescription( pht("Format for inlined or attached patches: 'git' or 'unified'.")), $this->newOption( 'metamta.differential.unified-comment-context', 'bool', false) ->setBoolOptions( array( pht('Show context'), pht('Do not show context'), )) ->setSummary(pht('Show diff context around inline comments in email.')) ->setDescription( pht( 'Normally, inline comments in emails are shown with a file and '. 'line but without any diff context. Enabling this option adds '. 'diff context and the comment thread.')), ); } } diff --git a/src/applications/differential/mail/DifferentialMail.php b/src/applications/differential/mail/DifferentialMail.php index e285ee37c7..07494c5282 100644 --- a/src/applications/differential/mail/DifferentialMail.php +++ b/src/applications/differential/mail/DifferentialMail.php @@ -1,15 +1,12 @@ setMailReceiver($revision); - return $reply_handler; } } diff --git a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php index f6dfb92762..3455077100 100644 --- a/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php +++ b/src/applications/diffusion/config/PhabricatorDiffusionConfigOptions.php @@ -1,134 +1,127 @@ newOption( 'metamta.diffusion.subject-prefix', 'string', '[Diffusion]') ->setDescription(pht('Subject prefix for Diffusion mail.')), - $this->newOption( - 'metamta.diffusion.reply-handler', - 'class', - 'PhabricatorAuditReplyHandler') - ->setLocked(true) - ->setBaseClass('PhabricatorMailReplyHandler') - ->setDescription(pht('Override mail reply handler class.')), $this->newOption( 'metamta.diffusion.attach-patches', 'bool', false) ->setBoolOptions( array( pht('Attach Patches'), pht('Do Not Attach Patches'), )) ->setDescription(pht( 'Set this to true if you want patches to be attached to commit '. 'notifications from Diffusion.')), $this->newOption('metamta.diffusion.inline-patches', 'int', 0) ->setSummary(pht('Include patches in Diffusion mail as body text.')) ->setDescription( pht( 'To include patches in Diffusion email bodies, set this to a '. 'positive integer. Patches will be inlined if they are at most '. 'that many lines. By default, patches are not inlined.')), $this->newOption('metamta.diffusion.byte-limit', 'int', 1024 * 1024) ->setDescription(pht('Hard byte limit on including patches in email.')), $this->newOption('metamta.diffusion.time-limit', 'int', 60) ->setDescription(pht('Hard time limit on generating patches.')), $this->newOption( 'audit.can-author-close-audit', 'bool', false) ->setBoolOptions( array( pht('Enable Closing Audits'), pht('Disable Closing Audits'), )) ->setDescription(pht('Controls whether Author can Close Audits.')), $this->newOption('bugtraq.url', 'string', '') ->addExample('https://bugs.php.net/%BUGID%', pht('PHP bugs')) ->addExample('/%BUGID%', pht('Local Maniphest URL')) ->setDescription(pht( 'URL of external bug tracker used by Diffusion. %s will be '. 'substituted by the bug ID.', '%BUGID%')), $this->newOption('bugtraq.logregex', 'list', array()) ->addExample(array('/\B#([1-9]\d*)\b/'), pht('Issue #123')) ->addExample( array('/[Ii]ssues?:?(\s*,?\s*#\d+)+/', '/(\d+)/'), pht('Issue #123, #456')) ->addExample(array('/(?addExample('/[A-Z]{2,}-\d+/', pht('JIRA-1234')) ->setDescription(pht( 'Regular expression to link external bug tracker. See '. 'http://tortoisesvn.net/docs/release/TortoiseSVN_en/'. 'tsvn-dug-bugtracker.html for further explanation.')), $this->newOption('diffusion.allow-http-auth', 'bool', false) ->setBoolOptions( array( pht('Allow HTTP Basic Auth'), pht('Disable HTTP Basic Auth'), )) ->setSummary(pht('Enable HTTP Basic Auth for repositories.')) ->setDescription( pht( "Phabricator can serve repositories over HTTP, using HTTP basic ". "auth.\n\n". "Because HTTP basic auth is less secure than SSH auth, it is ". "disabled by default. You can enable it here if you'd like to use ". "it anyway. There's nothing fundamentally insecure about it as ". "long as Phabricator uses HTTPS, but it presents a much lower ". "barrier to attackers than SSH does.\n\n". "Consider using SSH for authenticated access to repositories ". "instead of HTTP.")), $this->newOption('diffusion.ssh-user', 'string', null) ->setLocked(true) ->setSummary(pht('Login username for SSH connections to repositories.')) ->setDescription( pht( 'When constructing clone URIs to show to users, Diffusion will '. 'fill in this login username. If you have configured a VCS user '. 'like `git`, you should provide it here.')), $this->newOption('diffusion.ssh-port', 'int', null) ->setLocked(true) ->setSummary(pht('Port for SSH connections to repositories.')) ->setDescription( pht( 'When constructing clone URIs to show to users, Diffusion by '. 'default will not display a port assuming the default for your '. 'VCS. Explicitly declare when running on a non-standard port.')), $this->newOption('diffusion.ssh-host', 'string', null) ->setLocked(true) ->setSummary(pht('Host for SSH connections to repositories.')) ->setDescription( pht( 'If you accept Phabricator SSH traffic on a different host '. 'from web traffic (for example, if you use different SSH and '. 'web load balancers), you can set the SSH hostname here. This '. 'is an advanced option.')), ); } } diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index cef998fb66..9f7245e515 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -1,362 +1,355 @@ array( 'name' => pht('Unbreak Now!'), 'short' => pht('Unbreak!'), 'color' => 'indigo', ), 90 => array( 'name' => pht('Needs Triage'), 'short' => pht('Triage'), 'color' => 'violet', ), 80 => array( 'name' => pht('High'), 'short' => pht('High'), 'color' => 'red', ), 50 => array( 'name' => pht('Normal'), 'short' => pht('Normal'), 'color' => 'orange', ), 25 => array( 'name' => pht('Low'), 'short' => pht('Low'), 'color' => 'yellow', ), 0 => array( 'name' => pht('Wishlist'), 'short' => pht('Wish'), 'color' => 'sky', ), ); $status_type = 'custom:ManiphestStatusConfigOptionType'; $status_defaults = array( 'open' => array( 'name' => pht('Open'), 'special' => ManiphestTaskStatus::SPECIAL_DEFAULT, ), 'resolved' => array( 'name' => pht('Resolved'), 'name.full' => pht('Closed, Resolved'), 'closed' => true, 'special' => ManiphestTaskStatus::SPECIAL_CLOSED, 'prefixes' => array( 'closed', 'closes', 'close', 'fix', 'fixes', 'fixed', 'resolve', 'resolves', 'resolved', ), 'suffixes' => array( 'as resolved', 'as fixed', ), ), 'wontfix' => array( 'name' => pht('Wontfix'), 'name.full' => pht('Closed, Wontfix'), 'closed' => true, 'prefixes' => array( 'wontfix', 'wontfixes', 'wontfixed', ), 'suffixes' => array( 'as wontfix', ), ), 'invalid' => array( 'name' => pht('Invalid'), 'name.full' => pht('Closed, Invalid'), 'closed' => true, 'prefixes' => array( 'invalidate', 'invalidates', 'invalidated', ), 'suffixes' => array( 'as invalid', ), ), 'duplicate' => array( 'name' => pht('Duplicate'), 'name.full' => pht('Closed, Duplicate'), 'transaction.icon' => 'fa-times', 'special' => ManiphestTaskStatus::SPECIAL_DUPLICATE, 'closed' => true, ), 'spite' => array( 'name' => pht('Spite'), 'name.full' => pht('Closed, Spite'), 'name.action' => pht('Spited'), 'transaction.icon' => 'fa-thumbs-o-down', 'silly' => true, 'closed' => true, 'prefixes' => array( 'spite', 'spites', 'spited', ), 'suffixes' => array( 'out of spite', 'as spite', ), ), ); $status_description = $this->deformat(pht(<<.// Allows you to specify a list of text prefixes which will trigger a task transition into this status when mentioned in a commit message. For example, providing "closes" here will allow users to move tasks to this status by writing `Closes T123` in commit messages. - `suffixes` //Optional list.// Allows you to specify a list of text suffixes which will trigger a task transition into this status when mentioned in a commit message, after a valid prefix. For example, providing "as invalid" here will allow users to move tasks to this status by writing `Closes T123 as invalid`, even if another status is selected by the "Closes" prefix. Statuses will appear in the UI in the order specified. Note the status marked `special` as `duplicate` is not settable directly and will not appear in UI elements, and that any status marked `silly` does not appear if Phabricator is configured with `phabricator.serious-business` set to true. Examining the default configuration and examples below will probably be helpful in understanding these options. EOTEXT )); $status_example = array( 'open' => array( 'name' => 'Open', 'special' => 'default', ), 'closed' => array( 'name' => 'Closed', 'special' => 'closed', 'closed' => true, ), 'duplicate' => array( 'name' => 'Duplicate', 'special' => 'duplicate', 'closed' => true, ), ); $json = new PhutilJSON(); $status_example = $json->encodeFormatted($status_example); // This is intentionally blank for now, until we can move more Maniphest // logic to custom fields. $default_fields = array(); foreach ($default_fields as $key => $enabled) { $default_fields[$key] = array( 'disabled' => !$enabled, ); } $custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType'; return array( $this->newOption('maniphest.custom-field-definitions', 'wild', array()) ->setSummary(pht('Custom Maniphest fields.')) ->setDescription( pht( 'Array of custom fields for Maniphest tasks. For details on '. 'adding custom fields to Maniphest, see "Configuring Custom '. 'Fields" in the documentation.')) ->addExample( '{"mycompany:estimated-hours": {"name": "Estimated Hours", '. '"type": "int", "caption": "Estimated number of hours this will '. 'take."}}', pht('Valid Setting')), $this->newOption('maniphest.fields', $custom_field_type, $default_fields) ->setCustomData(id(new ManiphestTask())->getCustomFieldBaseClass()) ->setDescription(pht('Select and reorder task fields.')), $this->newOption('maniphest.priorities', 'wild', $priority_defaults) ->setSummary(pht('Configure Maniphest priority names.')) ->setDescription( pht( 'Allows you to edit or override the default priorities available '. 'in Maniphest, like "High", "Normal" and "Low". The configuration '. 'should contain a map of priority constants to priority '. 'specifications (see defaults below for examples).'. "\n\n". 'The keys you can define for a priority are:'. "\n\n". ' - `name` Name of the priority.'."\n". ' - `short` Alternate shorter name, used in UIs where there is '. ' not much space available.'."\n". ' - `color` A color for this priority, like "red" or "blue".'. "\n\n". 'You can choose which priority is the default for newly created '. 'tasks with `maniphest.default-priority`.')), $this->newOption('maniphest.statuses', $status_type, $status_defaults) ->setSummary(pht('Configure Maniphest task statuses.')) ->setDescription($status_description) ->addExample($status_example, pht('Minimal Valid Config')), $this->newOption('maniphest.default-priority', 'int', 90) ->setSummary(pht('Default task priority for create flows.')) ->setDescription( pht( 'Choose a default priority for newly created tasks. You can '. 'review and adjust available priorities by using the '. '{{maniphest.priorities}} configuration option. The default value '. '(`90`) corresponds to the default "Needs Triage" priority.')), - $this->newOption( - 'metamta.maniphest.reply-handler', - 'class', - 'ManiphestReplyHandler') - ->setLocked(true) - ->setBaseClass('PhabricatorMailReplyHandler') - ->setDescription(pht('Override reply handler class.')), $this->newOption( 'metamta.maniphest.subject-prefix', 'string', '[Maniphest]') ->setDescription(pht('Subject prefix for Maniphest mail.')), $this->newOption( 'metamta.maniphest.public-create-email', 'string', null) ->setLocked(true) ->setLockedMessage(pht( 'This configuration is deprecated. See description for details.')) ->setSummary(pht('DEPRECATED - Allow filing bugs via email.')) ->setDescription( pht( 'This config has been deprecated in favor of [[ '. '/applications/view/PhabricatorManiphestApplication/ | '. 'application settings ]], which allow for multiple email '. 'addresses and other functionality.'."\n\n". 'You can configure an email address like '. '"bugs@phabricator.example.com" which will automatically create '. 'Maniphest tasks when users send email to it. This relies on the '. '"From" address to authenticate users, so it is is not completely '. 'secure. To set this up, enter a complete email address like '. '"bugs@phabricator.example.com" and then configure mail to that '. 'address so it routed to Phabricator (if you\'ve already '. 'configured reply handlers, you\'re probably already done). See '. '"Configuring Inbound Email" in the documentation for more '. 'information.')), $this->newOption( 'metamta.maniphest.default-public-author', 'string', null) ->setLocked(true) ->setLockedMessage(pht( 'This configuration is deprecated. See description for details.')) ->setSummary(pht( 'DEPRECATED - Username anonymous bugs are filed under.')) ->setDescription( pht( 'This config has been deprecated in favor of [[ '. '/applications/view/PhabricatorManiphestApplication/ | '. 'application settings ]], which allow for multiple email '. 'addresses each with its own default author, and other '. 'functionality.'."\n\n". 'If you enable `metamta.maniphest.public-create-email` and create '. 'an email address like "bugs@phabricator.example.com", it will '. 'default to rejecting mail which doesn\'t come from a known user. '. 'However, you might want to let anyone send email to this '. 'address; to do so, set a default author here (a Phabricator '. 'username). A typical use of this might be to create a "System '. 'Agent" user called "bugs" and use that name here. If you specify '. 'a valid username, mail will always be accepted and used to '. 'create a task, even if the sender is not a system user. The '. 'original email address will be stored in an `From Email` field '. 'on the task.')), $this->newOption( 'maniphest.priorities.unbreak-now', 'int', 100) ->setSummary(pht('Priority used to populate "Unbreak Now" on home.')) ->setDescription( pht( 'Temporary setting. If set, this priority is used to populate the '. '"Unbreak Now" panel on the home page. You should adjust this if '. 'you adjust priorities using `maniphest.priorities`.')), $this->newOption( 'maniphest.priorities.needs-triage', 'int', 90) ->setSummary(pht('Priority used to populate "Needs Triage" on home.')) ->setDescription( pht( 'Temporary setting. If set, this priority is used to populate the '. '"Needs Triage" panel on the home page. You should adjust this if '. 'you adjust priorities using `maniphest.priorities`.')), ); } } diff --git a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php index d28beb3b74..2df618c879 100644 --- a/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php +++ b/src/applications/maniphest/mail/ManiphestCreateMailReceiver.php @@ -1,37 +1,36 @@ canAcceptApplicationMail($maniphest_app, $mail); } protected function processReceivedMail( PhabricatorMetaMTAReceivedMail $mail, PhabricatorUser $sender) { $task = ManiphestTask::initializeNewTask($sender); $task->setOriginalEmailSource($mail->getHeader('From')); - $handler = PhabricatorEnv::newObjectFromConfig( - 'metamta.maniphest.reply-handler'); + $handler = new ManiphestReplyHandler(); $handler->setMailReceiver($task); $handler->setActor($sender); $handler->setExcludeMailRecipientPHIDs( $mail->loadExcludeMailRecipientPHIDs()); if ($this->getApplicationEmail()) { $handler->setApplicationEmail($this->getApplicationEmail()); } $handler->processEmail($mail); $mail->setRelatedPHID($task->getPHID()); } } diff --git a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php index 59d97cfcc8..672152c2ff 100644 --- a/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php +++ b/src/applications/maniphest/mail/ManiphestTaskMailReceiver.php @@ -1,42 +1,41 @@ setViewer($viewer) ->withIDs(array($id)) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true) ->execute(); return head($results); } protected function processReceivedObjectMail( PhabricatorMetaMTAReceivedMail $mail, PhabricatorLiskDAO $object, PhabricatorUser $sender) { - $handler = PhabricatorEnv::newObjectFromConfig( - 'metamta.maniphest.reply-handler'); + $handler = new ManiphestReplyHandler(); $handler->setMailReceiver($object); $handler->setActor($sender); $handler->setExcludeMailRecipientPHIDs( $mail->loadExcludeMailRecipientPHIDs()); $handler->processEmail($mail); } } diff --git a/src/applications/owners/config/PhabricatorOwnersConfigOptions.php b/src/applications/owners/config/PhabricatorOwnersConfigOptions.php index ec0944b818..7d7b3e958d 100644 --- a/src/applications/owners/config/PhabricatorOwnersConfigOptions.php +++ b/src/applications/owners/config/PhabricatorOwnersConfigOptions.php @@ -1,36 +1,29 @@ newOption( - 'metamta.package.reply-handler', - 'class', - 'OwnersPackageReplyHandler') - ->setLocked(true) - ->setBaseClass('PhabricatorMailReplyHandler') - ->setDescription(pht('Reply handler for owners mail.')), $this->newOption('metamta.package.subject-prefix', 'string', '[Package]') ->setDescription(pht('Subject prefix for Owners email.')), ); } } diff --git a/src/applications/owners/mail/PackageMail.php b/src/applications/owners/mail/PackageMail.php index 58060d9633..00a8b196f6 100644 --- a/src/applications/owners/mail/PackageMail.php +++ b/src/applications/owners/mail/PackageMail.php @@ -1,210 +1,209 @@ package = $package; } abstract protected function getVerb(); abstract protected function isNewThread(); final protected function getPackage() { return $this->package; } final protected function getHandles() { return $this->handles; } final protected function getOwners() { return $this->owners; } final protected function getPaths() { return $this->paths; } final protected function getMailTo() { return $this->mailTo; } final protected function renderPackageTitle() { return $this->getPackage()->getName(); } final protected function renderRepoSubSection($repository_phid, $paths) { $handles = $this->getHandles(); $section = array(); $section[] = ' In repository '.$handles[$repository_phid]->getName(). ' - '.PhabricatorEnv::getProductionURI($handles[$repository_phid] ->getURI()); foreach ($paths as $path => $excluded) { $section[] = ' '.($excluded ? 'Excluded' : 'Included').' '.$path; } return implode("\n", $section); } protected function needSend() { return true; } protected function loadData() { $package = $this->getPackage(); $owners = $package->loadOwners(); $this->owners = $owners; $owner_phids = mpull($owners, 'getUserPHID'); $primary_owner_phid = $package->getPrimaryOwnerPHID(); $mail_to = $owner_phids; if (!in_array($primary_owner_phid, $owner_phids)) { $mail_to[] = $primary_owner_phid; } $this->mailTo = $mail_to; $this->paths = array(); $repository_paths = mgroup($package->loadPaths(), 'getRepositoryPHID'); foreach ($repository_paths as $repository_phid => $paths) { $this->paths[$repository_phid] = mpull($paths, 'getExcluded', 'getPath'); } $phids = array_merge( $this->mailTo, array($package->getActorPHID()), array_keys($this->paths)); $this->handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getActor()) ->withPHIDs($phids) ->execute(); } final protected function renderSummarySection() { $package = $this->getPackage(); $handles = $this->getHandles(); $section = array(); $section[] = $handles[$package->getActorPHID()]->getName().' '. strtolower($this->getVerb()).' '.$this->renderPackageTitle().'.'; $section[] = ''; $section[] = 'PACKAGE DETAIL'; $section[] = ' '.PhabricatorEnv::getProductionURI( '/owners/package/'.$package->getID().'/'); return implode("\n", $section); } protected function renderDescriptionSection() { return "PACKAGE DESCRIPTION\n". ' '.$this->getPackage()->getDescription(); } protected function renderPrimaryOwnerSection() { $handles = $this->getHandles(); return "PRIMARY OWNER\n". ' '.$handles[$this->getPackage()->getPrimaryOwnerPHID()]->getName(); } protected function renderOwnersSection() { $handles = $this->getHandles(); $owners = $this->getOwners(); if (!$owners) { return null; } $owners = mpull($owners, 'getUserPHID'); $owners = array_select_keys($handles, $owners); $owners = mpull($owners, 'getName'); return "OWNERS\n". ' '.implode(', ', $owners); } protected function renderAuditingEnabledSection() { return "AUDITING ENABLED STATUS\n". ' '.($this->getPackage()->getAuditingEnabled() ? 'Enabled' : 'Disabled'); } protected function renderPathsSection() { $section = array(); $section[] = 'PATHS'; foreach ($this->paths as $repository_phid => $paths) { $section[] = $this->renderRepoSubSection($repository_phid, $paths); } return implode("\n", $section); } final protected function renderBody() { $body = array(); $body[] = $this->renderSummarySection(); $body[] = $this->renderDescriptionSection(); $body[] = $this->renderPrimaryOwnerSection(); $body[] = $this->renderOwnersSection(); $body[] = $this->renderAuditingEnabledSection(); $body[] = $this->renderPathsSection(); $body = array_filter($body); return implode("\n\n", $body)."\n"; } final public function send() { $mails = $this->prepareMails(); foreach ($mails as $mail) { $mail->saveAndSend(); } } final public function prepareMails() { if (!$this->needSend()) { return array(); } $this->loadData(); $package = $this->getPackage(); $prefix = PhabricatorEnv::getEnvConfig('metamta.package.subject-prefix'); $verb = $this->getVerb(); $threading = $this->getMailThreading(); list($thread_id, $thread_topic) = $threading; $template = id(new PhabricatorMetaMTAMail()) ->setSubject($this->renderPackageTitle()) ->setSubjectPrefix($prefix) ->setVarySubjectPrefix("[{$verb}]") ->setFrom($package->getActorPHID()) ->setThreadID($thread_id, $this->isNewThread()) ->addHeader('Thread-Topic', $thread_topic) ->setRelatedPHID($package->getPHID()) ->setIsBulk(true) ->setBody($this->renderBody()); $reply_handler = $this->newReplyHandler(); $mails = $reply_handler->multiplexMail( $template, array_select_keys($this->getHandles(), $this->getMailTo()), array()); return $mails; } private function getMailThreading() { return array( 'package-'.$this->getPackage()->getPHID(), 'Package '.$this->getPackage()->getOriginalName(), ); } private function newReplyHandler() { - $reply_handler = PhabricatorEnv::newObjectFromConfig( - 'metamta.package.reply-handler'); + $reply_handler = new OwnersPackageReplyHandler(); $reply_handler->setMailReceiver($this->getPackage()); return $reply_handler; } }