diff --git a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php index 54f005bbe..d658a5a6d 100644 --- a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php @@ -1,336 +1,287 @@ commit = $commit; return $this; } public function setAttachInlineComments($attach_inline_comments) { $this->attachInlineComments = $attach_inline_comments; return $this; } public function setNoEmail($no_email) { $this->noEmail = $no_email; return $this; } public function addComments(array $comments) { assert_instances_of($comments, 'PhabricatorAuditComment'); $commit = $this->commit; $actor = $this->getActor(); $other_comments = PhabricatorAuditComment::loadComments( $actor, $commit->getPHID()); $inline_comments = array(); if ($this->attachInlineComments) { $inline_comments = PhabricatorAuditInlineComment::loadDraftComments( $actor, $commit->getPHID()); } // When an actor submits an audit comment, we update all the audit requests // they have authority over to reflect the most recent status. The general // idea here is that if audit has triggered for, e.g., several packages, but // a user owns all of them, they can clear the audit requirement in one go // without auditing the commit for each trigger. $audit_phids = self::loadAuditPHIDsForUser($actor); $audit_phids = array_fill_keys($audit_phids, true); $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( 'commitPHID = %s', $commit->getPHID()); // TODO: We should validate the action, currently we allow anyone to, e.g., // close an audit if they muck with form parameters. I'll followup with this // and handle the no-effect cases (e.g., closing and already-closed audit). $actor_is_author = ($actor->getPHID() == $commit->getAuthorPHID()); // Pick a meaningful action, if we have one. $action = PhabricatorAuditActionConstants::COMMENT; foreach ($comments as $comment) { switch ($comment->getAction()) { case PhabricatorAuditActionConstants::CLOSE: case PhabricatorAuditActionConstants::RESIGN: case PhabricatorAuditActionConstants::ACCEPT: case PhabricatorAuditActionConstants::CONCERN: $action = $comment->getAction(); break; } } if ($action == PhabricatorAuditActionConstants::CLOSE) { if (!PhabricatorEnv::getEnvConfig('audit.can-author-close-audit')) { throw new Exception('Cannot Close Audit without enabling'. 'audit.can-author-close-audit'); } // "Close" means wipe out all the concerns. $concerned_status = PhabricatorAuditStatusConstants::CONCERNED; foreach ($requests as $request) { if ($request->getAuditStatus() == $concerned_status) { $request->setAuditStatus(PhabricatorAuditStatusConstants::CLOSED); $request->save(); } } } else if ($action == PhabricatorAuditActionConstants::RESIGN) { // "Resign" has unusual rules for writing user rows, only affects the // user row (never package/project rows), and always affects the user // row (other actions don't, if they were able to affect a package/project // row). $actor_request = null; foreach ($requests as $request) { if ($request->getAuditorPHID() == $actor->getPHID()) { $actor_request = $request; break; } } if (!$actor_request) { $actor_request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($commit->getPHID()) ->setAuditorPHID($actor->getPHID()) ->setAuditReasons(array('Resigned')); } $actor_request ->setAuditStatus(PhabricatorAuditStatusConstants::RESIGNED) ->save(); $requests[] = $actor_request; } else { $have_any_requests = false; foreach ($requests as $request) { if (empty($audit_phids[$request->getAuditorPHID()])) { continue; } $request_is_for_actor = ($request->getAuditorPHID() == $actor->getPHID()); $have_any_requests = true; $new_status = null; switch ($action) { case PhabricatorAuditActionConstants::COMMENT: case PhabricatorAuditActionConstants::ADD_CCS: case PhabricatorAuditActionConstants::ADD_AUDITORS: // Commenting or adding cc's/auditors doesn't change status. break; case PhabricatorAuditActionConstants::ACCEPT: if (!$actor_is_author || $request_is_for_actor) { // When modifying your own commits, you act only on behalf of // yourself, not your packages/projects -- the idea being that // you can't accept your own commits. $new_status = PhabricatorAuditStatusConstants::ACCEPTED; } break; case PhabricatorAuditActionConstants::CONCERN: if (!$actor_is_author || $request_is_for_actor) { // See above. $new_status = PhabricatorAuditStatusConstants::CONCERNED; } break; default: throw new Exception("Unknown action '{$action}'!"); } if ($new_status !== null) { $request->setAuditStatus($new_status); $request->save(); } } // If the actor has no current authority over any audit trigger, make a // new one to represent their audit state. if (!$have_any_requests) { $new_status = null; switch ($action) { case PhabricatorAuditActionConstants::COMMENT: case PhabricatorAuditActionConstants::ADD_AUDITORS: case PhabricatorAuditActionConstants::ADD_CCS: break; case PhabricatorAuditActionConstants::ACCEPT: $new_status = PhabricatorAuditStatusConstants::ACCEPTED; break; case PhabricatorAuditActionConstants::CONCERN: $new_status = PhabricatorAuditStatusConstants::CONCERNED; break; case PhabricatorAuditActionConstants::CLOSE: // Impossible to reach this block with 'close'. default: throw new Exception("Unknown or invalid action '{$action}'!"); } if ($new_status !== null) { $request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($commit->getPHID()) ->setAuditorPHID($actor->getPHID()) ->setAuditStatus($new_status) ->setAuditReasons(array('Voluntary Participant')) ->save(); $requests[] = $request; } } } $auditors = array(); foreach ($comments as $comment) { $meta = $comment->getMetadata(); $auditor_phids = idx( $meta, PhabricatorAuditComment::METADATA_ADDED_AUDITORS, array()); foreach ($auditor_phids as $phid) { $auditors[] = $phid; } } $requests_by_auditor = mpull($requests, null, 'getAuditorPHID'); $requests_phids = array_keys($requests_by_auditor); $auditors = array_diff($auditors, $requests_phids); if ($auditors) { foreach ($auditors as $auditor_phid) { $audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED; $requests[] = id (new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($commit->getPHID()) ->setAuditorPHID($auditor_phid) ->setAuditStatus($audit_requested) ->setAuditReasons( array('Added by '.$actor->getUsername())) ->save(); } } $commit->updateAuditStatus($requests); $commit->save(); $commit->attachAudits($requests); // Convert old comments into real transactions and apply them with a // normal editor. $xactions = array(); foreach ($comments as $comment) { $xactions[] = $comment->getTransactionForSave(); } foreach ($inline_comments as $inline) { $xactions[] = id(new PhabricatorAuditTransaction()) ->setTransactionType(PhabricatorAuditActionConstants::INLINE) ->attachComment($inline->getTransactionComment()); } $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_LEGACY, array()); $editor = id(new PhabricatorAuditEditor()) ->setActor($actor) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->setContentSource($content_source) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setDisableEmail($this->noEmail) ->applyTransactions($commit, $xactions); - - $feed_dont_publish_phids = array(); - foreach ($requests as $request) { - $status = $request->getAuditStatus(); - switch ($status) { - case PhabricatorAuditStatusConstants::RESIGNED: - case PhabricatorAuditStatusConstants::NONE: - case PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED: - $feed_dont_publish_phids[$request->getAuditorPHID()] = 1; - break; - default: - unset($feed_dont_publish_phids[$request->getAuditorPHID()]); - break; - } - } - $feed_dont_publish_phids = array_keys($feed_dont_publish_phids); - - $feed_phids = array_diff($requests_phids, $feed_dont_publish_phids); - foreach ($comments as $comment) { - $this->publishFeedStory($comment, $feed_phids); - } } /** * Load the PHIDs for all objects the user has the authority to act as an * audit for. This includes themselves, and any packages they are an owner * of. */ public static function loadAuditPHIDsForUser(PhabricatorUser $user) { $phids = array(); // TODO: This method doesn't really use the right viewer, but in practice we // never issue this query of this type on behalf of another user and are // unlikely to do so in the future. This entire method should be refactored // into a Query class, however, and then we should use a proper viewer. // The user can audit on their own behalf. $phids[$user->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); } - private function publishFeedStory( - PhabricatorAuditComment $comment, - array $more_phids) { - - $commit = $this->commit; - $actor = $this->getActor(); - - $related_phids = array_merge( - array( - $actor->getPHID(), - $commit->getPHID(), - ), - $more_phids); - - id(new PhabricatorFeedStoryPublisher()) - ->setRelatedPHIDs($related_phids) - ->setStoryAuthorPHID($actor->getPHID()) - ->setStoryTime(time()) - ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_AUDIT) - ->setStoryData( - array( - 'commitPHID' => $commit->getPHID(), - 'action' => $comment->getAction(), - 'content' => $comment->getContent(), - )) - ->publish(); - } - } diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index 5aef27863..d01679998 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -1,237 +1,243 @@ 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: 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: 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 PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: case PhabricatorAuditActionConstants::ADD_AUDITORS: 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 PhabricatorAuditActionConstants::ACTION: case PhabricatorAuditActionConstants::INLINE: return; case PhabricatorAuditActionConstants::ADD_AUDITORS: // TODO: For now, these are applied externally. return; } return parent::applyCustomExternalTransaction($object, $xaction); } 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 supportsSearch() { return true; } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } protected function buildReplyHandler(PhabricatorLiskDAO $object) { $reply_handler = PhabricatorEnv::newObjectFromConfig( 'metamta.diffusion.reply-handler'); $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}"; return id(new PhabricatorMetaMTAMail()) ->setSubject($subject) ->addHeader('Thread-Topic', $thread_topic); } protected function getMailTo(PhabricatorLiskDAO $object) { $phids = array(); 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; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() == $type_inline) { $inlines[] = $xaction; } } if ($inlines) { $body->addTextSection( pht('INLINE COMMENTS'), $this->renderInlineCommentsForMail($object, $inlines)); } $monogram = $object->getRepository()->formatCommitName( $object->getCommitIdentifier()); $body->addTextSection( pht('COMMIT'), PhabricatorEnv::getProductionURI('/'.$monogram)); return $body; } 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); } + protected function shouldPublishFeedStory( + PhabricatorLiskDAO $object, + array $xactions) { + return true; + } + } diff --git a/src/applications/audit/storage/PhabricatorAuditTransaction.php b/src/applications/audit/storage/PhabricatorAuditTransaction.php index 80b1ef643..94f588453 100644 --- a/src/applications/audit/storage/PhabricatorAuditTransaction.php +++ b/src/applications/audit/storage/PhabricatorAuditTransaction.php @@ -1,209 +1,296 @@ getTransactionType(); switch ($type) { case PhabricatorAuditActionConstants::ADD_CCS: case PhabricatorAuditActionConstants::ADD_AUDITORS: $old = $this->getOldValue(); $new = $this->getNewValue(); if (!is_array($old)) { $old = array(); } if (!is_array($new)) { $new = array(); } foreach (array_keys($old + $new) as $phid) { $phids[] = $phid; } break; } return $phids; } public function getActionName() { switch ($this->getTransactionType()) { case PhabricatorAuditActionConstants::ACTION: switch ($this->getNewValue()) { case PhabricatorAuditActionConstants::CONCERN: return pht('Raised Concern'); case PhabricatorAuditActionConstants::ACCEPT: return pht('Accepted'); case PhabricatorAuditActionConstants::RESIGN: return pht('Resigned'); case PhabricatorAuditActionConstants::CLOSE: return pht('Clsoed'); } break; case PhabricatorAuditActionConstants::ADD_AUDITORS: return pht('Added Auditors'); } return parent::getActionName(); } public function getColor() { $type = $this->getTransactionType(); switch ($type) { case PhabricatorAuditActionConstants::ACTION: switch ($this->getNewValue()) { case PhabricatorAuditActionConstants::CONCERN: return 'red'; case PhabricatorAuditActionConstants::ACCEPT: return 'green'; } } return parent::getColor(); } public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); $author_handle = $this->renderHandleLink($this->getAuthorPHID()); $type = $this->getTransactionType(); switch ($type) { case PhabricatorAuditActionConstants::ADD_CCS: case PhabricatorAuditActionConstants::ADD_AUDITORS: if (!is_array($old)) { $old = array(); } if (!is_array($new)) { $new = array(); } $add = array_keys(array_diff_key($new, $old)); $rem = array_keys(array_diff_key($old, $new)); break; } switch ($type) { case PhabricatorAuditActionConstants::INLINE: return pht( '%s added inline comments.', $author_handle); case PhabricatorAuditActionConstants::ADD_CCS: if ($add && $rem) { return pht( '%s edited subscribers; added: %s, removed: %s.', $author_handle, $this->renderHandleList($add), $this->renderHandleList($rem)); } else if ($add) { return pht( '%s added subscribers: %s.', $author_handle, $this->renderHandleList($add)); } else if ($rem) { return pht( '%s removed subscribers: %s.', $author_handle, $this->renderHandleList($rem)); } else { return pht( '%s added subscribers...', $author_handle); } case PhabricatorAuditActionConstants::ADD_AUDITORS: if ($add && $rem) { return pht( '%s edited auditors; added: %s, removed: %s.', $author_handle, $this->renderHandleList($add), $this->renderHandleList($rem)); } else if ($add) { return pht( '%s added auditors: %s.', $author_handle, $this->renderHandleList($add)); } else if ($rem) { return pht( '%s removed auditors: %s.', $author_handle, $this->renderHandleList($rem)); } else { return pht( '%s added auditors...', $author_handle); } case PhabricatorAuditActionConstants::ACTION: switch ($new) { case PhabricatorAuditActionConstants::ACCEPT: return pht( '%s accepted this commit.', $author_handle); case PhabricatorAuditActionConstants::CONCERN: return pht( '%s raised a concern with this commit.', $author_handle); case PhabricatorAuditActionConstants::RESIGN: return pht( '%s resigned from this audit.', $author_handle); case PhabricatorAuditActionConstants::CLOSE: return pht( '%s closed this audit.', $author_handle); } } return parent::getTitle(); } + public function getTitleForFeed(PhabricatorFeedStory $story) { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $author_handle = $this->renderHandleLink($this->getAuthorPHID()); + $object_handle = $this->renderHandleLink($this->getObjectPHID()); + + $type = $this->getTransactionType(); + + switch ($type) { + case PhabricatorAuditActionConstants::ADD_CCS: + case PhabricatorAuditActionConstants::ADD_AUDITORS: + if (!is_array($old)) { + $old = array(); + } + if (!is_array($new)) { + $new = array(); + } + $add = array_keys(array_diff_key($new, $old)); + $rem = array_keys(array_diff_key($old, $new)); + break; + } + + switch ($type) { + case PhabricatorAuditActionConstants::INLINE: + return pht( + '%s added inline comments to %s.', + $author_handle, + $object_handle); + + case PhabricatorAuditActionConstants::ADD_AUDITORS: + if ($add && $rem) { + return pht( + '%s edited auditors for %s; added: %s, removed: %s.', + $author_handle, + $object_handle, + $this->renderHandleList($add), + $this->renderHandleList($rem)); + } else if ($add) { + return pht( + '%s added auditors to %s: %s.', + $author_handle, + $object_handle, + $this->renderHandleList($add)); + } else if ($rem) { + return pht( + '%s removed auditors from %s: %s.', + $author_handle, + $object_handle, + $this->renderHandleList($rem)); + } else { + return pht( + '%s added auditors to %s...', + $author_handle, + $object_handle); + } + + case PhabricatorAuditActionConstants::ACTION: + switch ($new) { + case PhabricatorAuditActionConstants::ACCEPT: + return pht( + '%s accepted %s.', + $author_handle, + $object_handle); + case PhabricatorAuditActionConstants::CONCERN: + return pht( + '%s raised a concern with %s.', + $author_handle, + $object_handle); + case PhabricatorAuditActionConstants::RESIGN: + return pht( + '%s resigned from auditing %s.', + $author_handle, + $object_handle); + case PhabricatorAuditActionConstants::CLOSE: + return pht( + '%s closed the audit of %s.', + $author_handle, + $object_handle); + } + + } + + return parent::getTitleForFeed($story); + } + + // TODO: These two mail methods can likely be abstracted by introducing a // formal concept of "inline comment" transactions. public function shouldHideForMail(array $xactions) { $type_inline = PhabricatorAuditActionConstants::INLINE; switch ($this->getTransactionType()) { case $type_inline: foreach ($xactions as $xaction) { if ($xaction->getTransactionType() != $type_inline) { return true; } } return ($this !== head($xactions)); } return parent::shouldHideForMail($xactions); } public function getBodyForMail() { switch ($this->getTransactionType()) { case PhabricatorAuditActionConstants::INLINE: return null; } return parent::getBodyForMail(); } } diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php index 068be3126..5658a44c7 100644 --- a/src/applications/auth/controller/PhabricatorAuthLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -1,251 +1,251 @@ providerKey = $data['pkey']; $this->extraURIData = idx($data, 'extra'); } public function getExtraURIData() { return $this->extraURIData; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $response = $this->loadProvider(); if ($response) { return $response; } $provider = $this->provider; try { list($account, $response) = $provider->processLoginRequest($this); } catch (PhutilAuthUserAbortedException $ex) { if ($viewer->isLoggedIn()) { // If a logged-in user cancels, take them back to the external accounts // panel. $next_uri = '/settings/panel/external/'; } else { // If a logged-out user cancels, take them back to the auth start page. $next_uri = '/'; } // User explicitly hit "Cancel". $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle(pht('Authentication Canceled')) ->appendChild( pht('You canceled authentication.')) ->addCancelButton($next_uri, pht('Continue')); return id(new AphrontDialogResponse())->setDialog($dialog); } if ($response) { return $response; } if (!$account) { throw new Exception( 'Auth provider failed to load an account from processLoginRequest()!'); } if ($account->getUserPHID()) { // The account is already attached to a Phabricator user, so this is // either a login or a bad account link request. if (!$viewer->isLoggedIn()) { if ($provider->shouldAllowLogin()) { return $this->processLoginUser($account); } else { return $this->renderError( pht( 'The external account ("%s") you just authenticated with is '. 'not configured to allow logins on this Phabricator install. '. 'An administrator may have recently disabled it.', $provider->getProviderName())); } } else if ($viewer->getPHID() == $account->getUserPHID()) { // This is either an attempt to re-link an existing and already // linked account (which is silly) or a refresh of an external account // (e.g., an OAuth account). return id(new AphrontRedirectResponse()) ->setURI('/settings/panel/external/'); } else { return $this->renderError( pht( - 'The external account ("%s") you just used to login is alerady '. + 'The external account ("%s") you just used to login is already '. 'associated with another Phabricator user account. Login to the '. 'other Phabricator account and unlink the external account before '. 'linking it to a new Phabricator account.', $provider->getProviderName())); } } else { // The account is not yet attached to a Phabricator user, so this is // either a registration or an account link request. if (!$viewer->isLoggedIn()) { if ($provider->shouldAllowRegistration()) { return $this->processRegisterUser($account); } else { return $this->renderError( pht( 'The external account ("%s") you just authenticated with is '. 'not configured to allow registration on this Phabricator '. 'install. An administrator may have recently disabled it.', $provider->getProviderName())); } } else { if ($provider->shouldAllowAccountLink()) { return $this->processLinkUser($account); } else { return $this->renderError( pht( 'The external account ("%s") you just authenticated with is '. 'not configured to allow account linking on this Phabricator '. 'install. An administrator may have recently disabled it.', $provider->getProviderName())); } } } // This should be unreachable, but fail explicitly if we get here somehow. return new Aphront400Response(); } private function processLoginUser(PhabricatorExternalAccount $account) { $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $account->getUserPHID()); if (!$user) { return $this->renderError( pht( 'The external account you just logged in with is not associated '. 'with a valid Phabricator user.')); } return $this->loginUser($user); } private function processRegisterUser(PhabricatorExternalAccount $account) { $account_secret = $account->getAccountSecret(); $register_uri = $this->getApplicationURI('register/'.$account_secret.'/'); return $this->setAccountKeyAndContinue($account, $register_uri); } private function processLinkUser(PhabricatorExternalAccount $account) { $account_secret = $account->getAccountSecret(); $confirm_uri = $this->getApplicationURI('confirmlink/'.$account_secret.'/'); return $this->setAccountKeyAndContinue($account, $confirm_uri); } private function setAccountKeyAndContinue( PhabricatorExternalAccount $account, $next_uri) { if ($account->getUserPHID()) { throw new Exception('Account is already registered or linked.'); } // Regenerate the registration secret key, set it on the external account, // set a cookie on the user's machine, and redirect them to registration. // See PhabricatorAuthRegisterController for discussion of the registration // key. $registration_key = Filesystem::readRandomCharacters(32); $account->setProperty( 'registrationKey', PhabricatorHash::digest($registration_key)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $account->save(); unset($unguarded); $this->getRequest()->setTemporaryCookie( PhabricatorCookies::COOKIE_REGISTRATION, $registration_key); return id(new AphrontRedirectResponse())->setURI($next_uri); } private function loadProvider() { $provider = PhabricatorAuthProvider::getEnabledProviderByKey( $this->providerKey); if (!$provider) { return $this->renderError( pht( 'The account you are attempting to login with uses a nonexistent '. 'or disabled authentication provider (with key "%s"). An '. 'administrator may have recently disabled this provider.', $this->providerKey)); } $this->provider = $provider; return null; } protected function renderError($message) { return $this->renderErrorPage( pht('Login Failed'), array($message)); } public function buildProviderPageResponse( PhabricatorAuthProvider $provider, $content) { $crumbs = $this->buildApplicationCrumbs(); if ($this->getRequest()->getUser()->isLoggedIn()) { $crumbs->addTextCrumb(pht('Link Account'), $provider->getSettingsURI()); } else { $crumbs->addTextCrumb(pht('Login'), $this->getApplicationURI('start/')); } $crumbs->addTextCrumb($provider->getProviderName()); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => pht('Login'), )); } public function buildProviderErrorResponse( PhabricatorAuthProvider $provider, $message) { $message = pht( 'Authentication provider ("%s") encountered an error during login. %s', $provider->getProviderName(), $message); return $this->renderError($message); } } diff --git a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php index 3d3f747cd..a8bab3a3f 100644 --- a/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php +++ b/src/applications/diffusion/doorkeeper/DiffusionDoorkeeperCommitFeedStoryPublisher.php @@ -1,194 +1,200 @@ auditRequests; } public function canPublishStory(PhabricatorFeedStory $story, $object) { - return ($object instanceof PhabricatorRepositoryCommit); + return + ($story instanceof PhabricatorApplicationTransactionFeedStory) && + ($object instanceof PhabricatorRepositoryCommit); } public function isStoryAboutObjectCreation($object) { // TODO: Although creation stories exist, they currently don't have a // primary object PHID set, so they'll never make it here because they // won't pass `canPublishStory()`. return false; } public function isStoryAboutObjectClosure($object) { // TODO: This isn't quite accurate, but pretty close: check if this story // is a close (which clearly is about object closure) or is an "Accept" and // the commit is fully audited (which is almost certainly a closure). // After ApplicationTransactions, we could annotate feed stories more // explicitly. - $story = $this->getFeedStory(); - $action = $story->getStoryData()->getValue('action'); - - if ($action == PhabricatorAuditActionConstants::CLOSE) { - return true; - } - $fully_audited = PhabricatorAuditCommitStatusConstants::FULLY_AUDITED; - if (($action == PhabricatorAuditActionConstants::ACCEPT) && - $object->getAuditStatus() == $fully_audited) { - return true; + + $story = $this->getFeedStory(); + $xaction = $story->getPrimaryTransaction(); + switch ($xaction->getTransactionType()) { + case PhabricatorAuditActionConstants::ACTION: + switch ($xaction->getNewValue()) { + case PhabricatorAuditActionConstants::CLOSE: + return true; + case PhabricatorAuditActionConstants::ACCEPT: + if ($object->getAuditStatus() == $fully_audited) { + return true; + } + break; + } } return false; } public function willPublishStory($commit) { $requests = id(new DiffusionCommitQuery()) ->setViewer($this->getViewer()) ->withPHIDs(array($commit->getPHID())) ->needAuditRequests(true) ->executeOne() ->getAudits(); // TODO: This is messy and should be generalized, but we don't have a good // query for it yet. Since we run in the daemons, just do the easiest thing // we can for the moment. Figure out who all of the "active" (need to // audit) and "passive" (no action necessary) users are. $auditor_phids = mpull($requests, 'getAuditorPHID'); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($auditor_phids) ->execute(); $active = array(); $passive = array(); foreach ($requests as $request) { $status = $request->getAuditStatus(); $object = idx($objects, $request->getAuditorPHID()); if (!$object) { continue; } $request_phids = array(); if ($object instanceof PhabricatorUser) { $request_phids = array($object->getPHID()); } else if ($object instanceof PhabricatorOwnersPackage) { $request_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs( array($object->getID())); } else if ($object instanceof PhabricatorProject) { $project = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withIDs(array($object->getID())) ->needMembers(true) ->executeOne(); $request_phids = $project->getMemberPHIDs(); } else { // Dunno what this is. $request_phids = array(); } switch ($status) { case PhabricatorAuditStatusConstants::AUDIT_REQUIRED: case PhabricatorAuditStatusConstants::AUDIT_REQUESTED: case PhabricatorAuditStatusConstants::CONCERNED: $active += array_fuse($request_phids); break; default: $passive += array_fuse($request_phids); break; } } // Remove "Active" users from the "Passive" list. $passive = array_diff_key($passive, $active); $this->activePHIDs = $active; $this->passivePHIDs = $passive; $this->auditRequests = $requests; return $commit; } public function getOwnerPHID($object) { return $object->getAuthorPHID(); } public function getActiveUserPHIDs($object) { return $this->activePHIDs; } public function getPassiveUserPHIDs($object) { return $this->passivePHIDs; } public function getCCUserPHIDs($object) { return PhabricatorSubscribersQuery::loadSubscribersForPHID( $object->getPHID()); } public function getObjectTitle($object) { $prefix = $this->getTitlePrefix($object); $repository = $object->getRepository(); $name = $repository->formatCommitName($object->getCommitIdentifier()); $title = $object->getSummary(); return ltrim("{$prefix} {$name}: {$title}"); } public function getObjectURI($object) { $repository = $object->getRepository(); $name = $repository->formatCommitName($object->getCommitIdentifier()); return PhabricatorEnv::getProductionURI('/'.$name); } public function getObjectDescription($object) { $data = $object->loadCommitData(); if ($data) { return $data->getCommitMessage(); } return null; } public function isObjectClosed($object) { switch ($object->getAuditStatus()) { case PhabricatorAuditCommitStatusConstants::NEEDS_AUDIT: case PhabricatorAuditCommitStatusConstants::CONCERN_RAISED: case PhabricatorAuditCommitStatusConstants::PARTIALLY_AUDITED: return false; default: return true; } } public function getResponsibilityTitle($object) { $prefix = $this->getTitlePrefix($object); return pht('%s Audit', $prefix); } public function getStoryText($object) { $implied_context = $this->getRenderWithImpliedContext(); $story = $this->getFeedStory(); if ($story instanceof PhabricatorFeedStoryAudit) { $text = $story->renderForAsanaBridge($implied_context); } else { $text = $story->renderText(); } return $text; } private function getTitlePrefix(PhabricatorRepositoryCommit $commit) { $prefix_key = 'metamta.diffusion.subject-prefix'; return PhabricatorEnv::getEnvConfig($prefix_key); } } diff --git a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php index 4ccc26545..dc820fb6d 100644 --- a/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php +++ b/src/applications/transactions/feed/PhabricatorApplicationTransactionFeedStory.php @@ -1,78 +1,78 @@ getValue('objectPHID'); } public function getRequiredObjectPHIDs() { return $this->getValue('transactionPHIDs'); } public function getRequiredHandlePHIDs() { $phids = array(); $phids[] = $this->getValue('objectPHID'); foreach ($this->getValue('transactionPHIDs') as $xaction_phid) { $xaction = $this->getObject($xaction_phid); foreach ($xaction->getRequiredHandlePHIDs() as $handle_phid) { $phids[] = $handle_phid; } } return $phids; } protected function getPrimaryTransactionPHID() { return head($this->getValue('transactionPHIDs')); } - protected function getPrimaryTransaction() { + public function getPrimaryTransaction() { return $this->getObject($this->getPrimaryTransactionPHID()); } public function renderView() { $view = $this->newStoryView(); $handle = $this->getHandle($this->getPrimaryObjectPHID()); $view->setHref($handle->getURI()); $view->setAppIconFromPHID($handle->getPHID()); $xaction_phids = $this->getValue('transactionPHIDs'); $xaction = $this->getPrimaryTransaction(); $xaction->setHandles($this->getHandles()); $view->setTitle($xaction->getTitleForFeed($this)); foreach ($xaction_phids as $xaction_phid) { $secondary_xaction = $this->getObject($xaction_phid); $secondary_xaction->setHandles($this->getHandles()); $body = $secondary_xaction->getBodyForFeed($this); if (nonempty($body)) { $view->appendChild($body); } } $view->setImage( $this->getHandle($xaction->getAuthorPHID())->getImageURI()); return $view; } public function renderText() { $xaction = $this->getPrimaryTransaction(); $old_target = $xaction->getRenderingTarget(); $new_target = PhabricatorApplicationTransaction::TARGET_TEXT; $xaction->setRenderingTarget($new_target); $xaction->setHandles($this->getHandles()); $text = $xaction->getTitleForFeed($this); $xaction->setRenderingTarget($old_target); return $text; } }