From ae59760222529ce5c83a250dfea1b9ec3ef413b1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 11 Feb 2015 06:05:53 -0800 Subject: [PATCH] Add administrative invite interfaces Summary: Ref T7152. This implements the administrative UI for the upstream email invite workflow. Pieces of this will be reused in Instances to implement the instance invite workflow, although some of it is probably going to be a bit copy/pastey. This doesn't actually create or send invites yet, and they still can't be carried through registration. Test Plan: {F290970} {F290971} Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T7152 Differential Revision: https://secure.phabricator.com/D11733 --- .../sql/autopatches/20150210.invitephid.sql | 5 + src/__phutil_library_map__.php | 21 +- .../auth/data/PhabricatorAuthInviteAction.php | 192 ++++++++++++++++++ .../PhabricatorAuthAuthFactorPHIDType.php | 2 +- .../phid/PhabricatorAuthInvitePHIDType.php | 31 +++ .../auth/query/PhabricatorAuthInviteQuery.php | 113 +++++++++++ .../PhabricatorAuthInviteSearchEngine.php | 95 +++++++++ .../auth/storage/PhabricatorAuthInvite.php | 58 +++++- .../PhabricatorAuthInviteActionTableView.php | 80 ++++++++ .../PhabricatorPeopleApplication.php | 6 + .../PhabricatorPeopleController.php | 1 + .../PhabricatorPeopleInviteController.php | 14 ++ .../PhabricatorPeopleInviteListController.php | 44 ++++ .../PhabricatorPeopleInviteSendController.php | 185 +++++++++++++++++ 14 files changed, 844 insertions(+), 3 deletions(-) create mode 100644 resources/sql/autopatches/20150210.invitephid.sql create mode 100644 src/applications/auth/data/PhabricatorAuthInviteAction.php create mode 100644 src/applications/auth/phid/PhabricatorAuthInvitePHIDType.php create mode 100644 src/applications/auth/query/PhabricatorAuthInviteQuery.php create mode 100644 src/applications/auth/query/PhabricatorAuthInviteSearchEngine.php create mode 100644 src/applications/auth/view/PhabricatorAuthInviteActionTableView.php create mode 100644 src/applications/people/controller/PhabricatorPeopleInviteController.php create mode 100644 src/applications/people/controller/PhabricatorPeopleInviteListController.php create mode 100644 src/applications/people/controller/PhabricatorPeopleInviteSendController.php diff --git a/resources/sql/autopatches/20150210.invitephid.sql b/resources/sql/autopatches/20150210.invitephid.sql new file mode 100644 index 0000000000..eaa6bcd4f9 --- /dev/null +++ b/resources/sql/autopatches/20150210.invitephid.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_user.user_authinvite + ADD phid VARBINARY(64) NOT NULL; + +ALTER TABLE {$NAMESPACE}_user.user_authinvite + ADD UNIQUE KEY `key_phid` (phid); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b2dc43008a..faf1dfddf2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1347,13 +1347,18 @@ phutil_register_library_map(array( 'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php', 'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php', 'PhabricatorAuthInviteAccountException' => 'applications/auth/exception/PhabricatorAuthInviteAccountException.php', + 'PhabricatorAuthInviteAction' => 'applications/auth/data/PhabricatorAuthInviteAction.php', + 'PhabricatorAuthInviteActionTableView' => 'applications/auth/view/PhabricatorAuthInviteActionTableView.php', 'PhabricatorAuthInviteController' => 'applications/auth/controller/PhabricatorAuthInviteController.php', 'PhabricatorAuthInviteDialogException' => 'applications/auth/exception/PhabricatorAuthInviteDialogException.php', 'PhabricatorAuthInviteEngine' => 'applications/auth/engine/PhabricatorAuthInviteEngine.php', 'PhabricatorAuthInviteException' => 'applications/auth/exception/PhabricatorAuthInviteException.php', 'PhabricatorAuthInviteInvalidException' => 'applications/auth/exception/PhabricatorAuthInviteInvalidException.php', 'PhabricatorAuthInviteLoginException' => 'applications/auth/exception/PhabricatorAuthInviteLoginException.php', + 'PhabricatorAuthInvitePHIDType' => 'applications/auth/phid/PhabricatorAuthInvitePHIDType.php', + 'PhabricatorAuthInviteQuery' => 'applications/auth/query/PhabricatorAuthInviteQuery.php', 'PhabricatorAuthInviteRegisteredException' => 'applications/auth/exception/PhabricatorAuthInviteRegisteredException.php', + 'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php', 'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php', 'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php', 'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php', @@ -2142,6 +2147,9 @@ phutil_register_library_map(array( 'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php', 'PhabricatorPeopleFeedController' => 'applications/people/controller/PhabricatorPeopleFeedController.php', 'PhabricatorPeopleHovercardEventListener' => 'applications/people/event/PhabricatorPeopleHovercardEventListener.php', + 'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php', + 'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php', + 'PhabricatorPeopleInviteSendController' => 'applications/people/controller/PhabricatorPeopleInviteSendController.php', 'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php', 'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php', 'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php', @@ -4564,15 +4572,23 @@ phutil_register_library_map(array( 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 'PhabricatorAuthHighSecurityRequiredException' => 'Exception', - 'PhabricatorAuthInvite' => 'PhabricatorUserDAO', + 'PhabricatorAuthInvite' => array( + 'PhabricatorUserDAO', + 'PhabricatorPolicyInterface', + ), 'PhabricatorAuthInviteAccountException' => 'PhabricatorAuthInviteDialogException', + 'PhabricatorAuthInviteAction' => 'Phobject', + 'PhabricatorAuthInviteActionTableView' => 'AphrontView', 'PhabricatorAuthInviteController' => 'PhabricatorAuthController', 'PhabricatorAuthInviteDialogException' => 'PhabricatorAuthInviteException', 'PhabricatorAuthInviteEngine' => 'Phobject', 'PhabricatorAuthInviteException' => 'Exception', 'PhabricatorAuthInviteInvalidException' => 'PhabricatorAuthInviteDialogException', 'PhabricatorAuthInviteLoginException' => 'PhabricatorAuthInviteDialogException', + 'PhabricatorAuthInvitePHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthInviteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthInviteRegisteredException' => 'PhabricatorAuthInviteException', + 'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase', 'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException', 'PhabricatorAuthLinkController' => 'PhabricatorAuthController', @@ -5409,6 +5425,9 @@ phutil_register_library_map(array( 'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType', 'PhabricatorPeopleFeedController' => 'PhabricatorPeopleController', 'PhabricatorPeopleHovercardEventListener' => 'PhabricatorEventListener', + 'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController', + 'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController', + 'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController', 'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController', 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', diff --git a/src/applications/auth/data/PhabricatorAuthInviteAction.php b/src/applications/auth/data/PhabricatorAuthInviteAction.php new file mode 100644 index 0000000000..0fe9dd1dd1 --- /dev/null +++ b/src/applications/auth/data/PhabricatorAuthInviteAction.php @@ -0,0 +1,192 @@ +rawInput; + } + + public function getEmailAddress() { + return $this->emailAddress; + } + + public function getUserPHID() { + return $this->userPHID; + } + + public function getIssues() { + return $this->issues; + } + + public function setAction($action) { + $this->action = $action; + return $this; + } + + public function getAction() { + return $this->action; + } + + public function willSend() { + return ($this->action == self::ACTION_SEND); + } + + public function getShortNameForIssue($issue) { + $map = array( + self::ISSUE_PARSE => pht('Not a Valid Email Address'), + self::ISSUE_DUPLICATE => pht('Address Duplicated in Input'), + self::ISSUE_UNVERIFIED => pht('Unverified User Email'), + self::ISSUE_VERIFIED => pht('Verified User Email'), + self::ISSUE_INVITED => pht('Previously Invited'), + self::ISSUE_ACCEPTED => pht('Already Accepted Invite'), + ); + + return idx($map, $issue); + } + + public function getShortNameForAction($action) { + $map = array( + self::ACTION_SEND => pht('Will Send Invite'), + self::ACTION_ERROR => pht('Address Error'), + self::ACTION_IGNORE => pht('Will Ignore Address'), + ); + + return idx($map, $action); + } + + public function getIconForAction($action) { + switch ($action) { + case self::ACTION_SEND: + $icon = 'fa-envelope-o'; + $color = 'green'; + break; + case self::ACTION_IGNORE: + $icon = 'fa-ban'; + $color = 'grey'; + break; + case self::ACTION_ERROR: + $icon = 'fa-exclamation-triangle'; + $color = 'red'; + break; + } + + return id(new PHUIIconView()) + ->setIconFont("{$icon} {$color}"); + } + + public static function newActionListFromAddresses( + PhabricatorUser $viewer, + array $addresses) { + + $results = array(); + foreach ($addresses as $address) { + $result = new PhabricatorAuthInviteAction(); + $result->rawInput = $address; + + $email = new PhutilEmailAddress($address); + $result->emailAddress = phutil_utf8_strtolower($email->getAddress()); + + if (!preg_match('/^\S+@\S+\.\S+\z/', $result->emailAddress)) { + $result->issues[] = self::ISSUE_PARSE; + } + + $results[] = $result; + } + + // Identify duplicates. + $address_groups = mgroup($results, 'getEmailAddress'); + foreach ($address_groups as $address => $group) { + if (count($group) > 1) { + foreach ($group as $action) { + $action->issues[] = self::ISSUE_DUPLICATE; + } + } + } + + // Identify addresses which are already in the system. + $addresses = mpull($results, 'getEmailAddress'); + $email_objects = id(new PhabricatorUserEmail())->loadAllWhere( + 'address IN (%Ls)', + $addresses); + + $email_map = array(); + foreach ($email_objects as $email_object) { + $address_key = phutil_utf8_strtolower($email_object->getAddress()); + $email_map[$address_key] = $email_object; + } + + // Identify outstanding invites. + $invites = id(new PhabricatorAuthInviteQuery()) + ->setViewer($viewer) + ->withEmailAddresses($addresses) + ->execute(); + $invite_map = mpull($invites, null, 'getEmailAddress'); + + foreach ($results as $action) { + $email = idx($email_map, $action->getEmailAddress()); + if ($email) { + if ($email->getUserPHID()) { + $action->userPHID = $email->getUserPHID(); + if ($email->getIsVerified()) { + $action->issues[] = self::ISSUE_VERIFIED; + } else { + $action->issues[] = self::ISSUE_UNVERIFIED; + } + } + } + + $invite = idx($invite_map, $action->getEmailAddress()); + if ($invite) { + if ($invite->getAcceptedByPHID()) { + $action->issues[] = self::ISSUE_ACCEPTED; + if (!$action->userPHID) { + // This could be different from the user who is currently attached + // to the email address if the address was removed or added to a + // different account later. Only show it if the address was + // removed, since the current status is more up-to-date otherwise. + $action->userPHID = $invite->getAcceptedByPHID(); + } + } else { + $action->issues[] = self::ISSUE_INVITED; + } + } + } + + foreach ($results as $result) { + foreach ($result->getIssues() as $issue) { + switch ($issue) { + case self::ISSUE_PARSE: + $result->action = self::ACTION_ERROR; + break; + case self::ISSUE_ACCEPTED: + case self::ISSUE_VERIFIED: + $result->action = self::ACTION_IGNORE; + break; + } + } + if (!$result->action) { + $result->action = self::ACTION_SEND; + } + } + + return $results; + } + +} diff --git a/src/applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php b/src/applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php index 7de68cda66..ca21397fb6 100644 --- a/src/applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php +++ b/src/applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php @@ -17,7 +17,7 @@ final class PhabricatorAuthAuthFactorPHIDType extends PhabricatorPHIDType { array $phids) { // TODO: Maybe we need this eventually? - throw new Exception(pht('Not Supported')); + throw new PhutilMethodNotImplementedException(); } public function loadHandles( diff --git a/src/applications/auth/phid/PhabricatorAuthInvitePHIDType.php b/src/applications/auth/phid/PhabricatorAuthInvitePHIDType.php new file mode 100644 index 0000000000..0f1b205763 --- /dev/null +++ b/src/applications/auth/phid/PhabricatorAuthInvitePHIDType.php @@ -0,0 +1,31 @@ + $handle) { + $invite = $objects[$phid]; + } + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthInviteQuery.php b/src/applications/auth/query/PhabricatorAuthInviteQuery.php new file mode 100644 index 0000000000..28232fb140 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthInviteQuery.php @@ -0,0 +1,113 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withEmailAddresses(array $addresses) { + $this->emailAddresses = $addresses; + return $this; + } + + public function withVerificationCodes(array $codes) { + $this->verificationCodes = $codes; + return $this; + } + + public function withAuthorPHIDs(array $phids) { + $this->authorPHIDs = $phids; + return $this; + } + + protected function loadPage() { + $table = new PhabricatorAuthInvite(); + $conn_r = $table->establishConnection('r'); + + $data = queryfx_all( + $conn_r, + 'SELECT * FROM %T %Q %Q %Q', + $table->getTableName(), + $this->buildWhereClause($conn_r), + $this->buildOrderClause($conn_r), + $this->buildLimitClause($conn_r)); + + $invites = $table->loadAllFromArray($data); + + // If the objects were loaded via verification code, set a flag to make + // sure the viewer can see them. + if ($this->verificationCodes !== null) { + foreach ($invites as $invite) { + $invite->setViewerHasVerificationCode(true); + } + } + + return $invites; + } + + protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { + $where = array(); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn_r, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn_r, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->emailAddresses !== null) { + $where[] = qsprintf( + $conn_r, + 'emailAddress IN (%Ls)', + $this->emailAddresses); + } + + if ($this->verificationCodes !== null) { + $hashes = array(); + foreach ($this->verificationCodes as $code) { + $hashes[] = PhabricatorHash::digestForIndex($code); + } + $where[] = qsprintf( + $conn_r, + 'verificationHash IN (%Ls)', + $hashes); + } + + if ($this->authorPHIDs !== null) { + $where[] = qsprintf( + $conn_r, + 'authorPHID IN (%Ls)', + $this->authorPHIDs); + } + + $where[] = $this->buildPagingClause($conn_r); + + return $this->formatWhereClause($where); + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthInviteSearchEngine.php b/src/applications/auth/query/PhabricatorAuthInviteSearchEngine.php new file mode 100644 index 0000000000..8a01f598bc --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthInviteSearchEngine.php @@ -0,0 +1,95 @@ + pht('All'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function getRequiredHandlePHIDsForResultList( + array $invites, + PhabricatorSavedQuery $query) { + + $phids = array(); + foreach ($invites as $invite) { + $phids[$invite->getAuthorPHID()] = true; + if ($invite->getAcceptedByPHID()) { + $phids[$invite->getAcceptedByPHID()] = true; + } + } + + return array_keys($phids); + } + + protected function renderResultList( + array $invites, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($invites, 'PhabricatorAuthInvite'); + + $viewer = $this->requireViewer(); + + $rows = array(); + foreach ($invites as $invite) { + $rows[] = array( + $invite->getEmailAddress(), + $handles[$invite->getAuthorPHID()]->renderLink(), + ($invite->getAcceptedByPHID() + ? $handles[$invite->getAcceptedByPHID()]->renderLink() + : null), + phabricator_datetime($invite->getDateCreated(), $viewer), + ); + } + + $table = new AphrontTableView($rows); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Email Invitations')) + ->appendChild($table); + } +} diff --git a/src/applications/auth/storage/PhabricatorAuthInvite.php b/src/applications/auth/storage/PhabricatorAuthInvite.php index 72096aaa07..85b6477280 100644 --- a/src/applications/auth/storage/PhabricatorAuthInvite.php +++ b/src/applications/auth/storage/PhabricatorAuthInvite.php @@ -1,7 +1,8 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'emailAddress' => 'sort128', 'verificationHash' => 'bytes12', @@ -30,6 +33,11 @@ final class PhabricatorAuthInvite ) + parent::getConfiguration(); } + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + PhabricatorAuthInvitePHIDType::TYPECONST); + } + public function getVerificationCode() { if (!$this->getVerificationHash()) { if ($this->verificationHash) { @@ -52,4 +60,52 @@ final class PhabricatorAuthInvite return parent::save(); } + public function setViewerHasVerificationCode($loaded) { + $this->viewerHasVerificationCode = $loaded; + return $this; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::POLICY_ADMIN; + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + if ($this->viewerHasVerificationCode) { + return true; + } + + if ($viewer->getPHID()) { + if ($viewer->getPHID() == $this->getAuthorPHID()) { + // You can see invites you sent. + return true; + } + + if ($viewer->getPHID() == $this->getAcceptedByPHID()) { + // You can see invites you have accepted. + return true; + } + } + + return false; + } + + public function describeAutomaticCapability($capability) { + return pht( + 'Invites are visible to administrators, the inviting user, users with '. + 'an invite code, and the user who accepts the invite.'); + } + } diff --git a/src/applications/auth/view/PhabricatorAuthInviteActionTableView.php b/src/applications/auth/view/PhabricatorAuthInviteActionTableView.php new file mode 100644 index 0000000000..27d9047345 --- /dev/null +++ b/src/applications/auth/view/PhabricatorAuthInviteActionTableView.php @@ -0,0 +1,80 @@ +inviteActions = $invite_actions; + return $this; + } + + public function getInviteActions() { + return $this->inviteActions; + } + + public function setHandles(array $handles) { + $this->handles = $handles; + return $this; + } + + public function render() { + $actions = $this->getInviteActions(); + $handles = $this->handles; + + $rows = array(); + $rowc = array(); + foreach ($actions as $action) { + $issues = $action->getIssues(); + foreach ($issues as $key => $issue) { + $issues[$key] = $action->getShortNameForIssue($issue); + } + $issues = implode(', ', $issues); + + if (!$action->willSend()) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = null; + } + + $action_icon = $action->getIconForAction($action->getAction()); + $action_name = $action->getShortNameForAction($action->getAction()); + + $rows[] = array( + $action->getRawInput(), + $action->getEmailAddress(), + ($action->getUserPHID() + ? $handles[$action->getUserPHID()]->renderLink() + : null), + $issues, + $action_icon, + $action_name, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setRowClasses($rowc) + ->setHeaders( + array( + pht('Raw Address'), + pht('Parsed Address'), + pht('User'), + pht('Issues'), + null, + pht('Action'), + )) + ->setColumnClasses( + array( + '', + '', + '', + 'wide', + 'icon', + '', + )); + + return $table; + } + +} diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php index 01844fdae8..b00078d928 100644 --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -46,6 +46,12 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { '(query/(?P[^/]+)/)?' => 'PhabricatorPeopleListController', 'logs/(?:query/(?P[^/]+)/)?' => 'PhabricatorPeopleLogsController', + 'invite/' => array( + '(?:query/(?P[^/]+)/)?' + => 'PhabricatorPeopleInviteListController', + 'send/' + => 'PhabricatorPeopleInviteSendController', + ), 'approve/(?P[1-9]\d*)/' => 'PhabricatorPeopleApproveController', '(?Pdisapprove)/(?P[1-9]\d*)/' => 'PhabricatorPeopleDisableController', diff --git a/src/applications/people/controller/PhabricatorPeopleController.php b/src/applications/people/controller/PhabricatorPeopleController.php index c350caba8d..4177100a36 100644 --- a/src/applications/people/controller/PhabricatorPeopleController.php +++ b/src/applications/people/controller/PhabricatorPeopleController.php @@ -34,6 +34,7 @@ abstract class PhabricatorPeopleController extends PhabricatorController { } $nav->addFilter('logs', pht('Activity Logs')); + $nav->addFilter('invite', pht('Email Invitations')); } } diff --git a/src/applications/people/controller/PhabricatorPeopleInviteController.php b/src/applications/people/controller/PhabricatorPeopleInviteController.php new file mode 100644 index 0000000000..aabb0c7d80 --- /dev/null +++ b/src/applications/people/controller/PhabricatorPeopleInviteController.php @@ -0,0 +1,14 @@ +addTextCrumb( + pht('Invites'), + $this->getApplicationURI('invite/')); + return $crumbs; + } + +} diff --git a/src/applications/people/controller/PhabricatorPeopleInviteListController.php b/src/applications/people/controller/PhabricatorPeopleInviteListController.php new file mode 100644 index 0000000000..5f1741ad77 --- /dev/null +++ b/src/applications/people/controller/PhabricatorPeopleInviteListController.php @@ -0,0 +1,44 @@ +setQueryKey($request->getURIData('queryKey')) + ->setSearchEngine(new PhabricatorAuthInviteSearchEngine()) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function buildSideNavView($for_app = false) { + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + $viewer = $this->getRequest()->getUser(); + + id(new PhabricatorAuthInviteSearchEngine()) + ->setViewer($viewer) + ->addNavigationItems($nav->getMenu()); + + return $nav; + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $can_invite = $this->hasApplicationCapability( + PeopleCreateUsersCapability::CAPABILITY); + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Invite Users')) + ->setHref($this->getApplicationURI('invite/send/')) + ->setIcon('fa-plus-square') + ->setDisabled(!$can_invite) + ->setWorkflow(!$can_invite)); + + return $crumbs; + } + +} diff --git a/src/applications/people/controller/PhabricatorPeopleInviteSendController.php b/src/applications/people/controller/PhabricatorPeopleInviteSendController.php new file mode 100644 index 0000000000..6debefb1c0 --- /dev/null +++ b/src/applications/people/controller/PhabricatorPeopleInviteSendController.php @@ -0,0 +1,185 @@ +getViewer(); + + $this->requireApplicationCapability( + PeopleCreateUsersCapability::CAPABILITY); + + $is_confirm = false; + $errors = array(); + $confirm_errors = array(); + $e_emails = true; + + $message = $request->getStr('message'); + $emails = $request->getStr('emails'); + $severity = PHUIErrorView::SEVERITY_ERROR; + if ($request->isFormPost()) { + // NOTE: We aren't using spaces as a delimiter here because email + // addresses with names often include spaces. + $email_list = preg_split('/[,;\n]+/', $emails); + foreach ($email_list as $key => $email) { + if (!strlen(trim($email))) { + unset($email_list[$key]); + } + } + + if ($email_list) { + $e_emails = null; + } else { + $e_emails = pht('Required'); + $errors[] = pht( + 'To send invites, you must enter at least one email address.'); + } + + if (!$errors) { + $is_confirm = true; + + $actions = PhabricatorAuthInviteAction::newActionListFromAddresses( + $viewer, + $email_list); + + $any_valid = false; + $all_valid = true; + $action_send = PhabricatorAuthInviteAction::ACTION_SEND; + foreach ($actions as $action) { + if ($action->getAction() == $action_send) { + $any_valid = true; + } else { + $all_valid = false; + } + } + + if (!$any_valid) { + $confirm_errors[] = pht( + 'None of the provided addresses are valid invite recipients. '. + 'Review the table below for details. Revise the address list '. + 'to continue.'); + } else if ($all_valid) { + $confirm_errors[] = pht( + 'All of the addresses appear to be valid invite recipients. '. + 'Confirm the actions below to continue.'); + $severity = PHUIErrorView::SEVERITY_NOTICE; + } else { + $confirm_errors[] = pht( + 'Some of the addresses you entered do not appear to be '. + 'valid recipients. Review the table below. You can revise '. + 'the address list, or ignore these errors and continue.'); + $severity = PHUIErrorView::SEVERITY_WARNING; + } + + if ($any_valid && $request->getBool('confirm')) { + throw new Exception( + pht('TODO: This workflow is not yet fully implemented.')); + } + } + } + + if ($is_confirm) { + $title = pht('Confirm Invites'); + } else { + $title = pht('Invite Users'); + } + + $crumbs = $this->buildApplicationCrumbs(); + if ($is_confirm) { + $crumbs->addTextCrumb(pht('Confirm')); + } else { + $crumbs->addTextCrumb(pht('Invite Users')); + } + + $confirm_box = null; + if ($is_confirm) { + + $handles = array(); + if ($actions) { + $handles = $this->loadViewerHandles(mpull($actions, 'getUserPHID')); + } + + $invite_table = id(new PhabricatorAuthInviteActionTableView()) + ->setUser($viewer) + ->setInviteActions($actions) + ->setHandles($handles); + + $confirm_form = null; + if ($any_valid) { + $confirm_form = id(new AphrontFormView()) + ->setUser($viewer) + ->addHiddenInput('message', $message) + ->addHiddenInput('emails', $emails) + ->addHiddenInput('confirm', true) + ->appendRemarkupInstructions( + pht( + 'If everything looks good, click **Send Invitations** to '. + 'deliver email invitations these users. Otherwise, edit the '. + 'email list or personal message at the bottom of the page to '. + 'revise the invitations.')) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Send Invitations'))); + } + + $confirm_box = id(new PHUIObjectBoxView()) + ->setErrorView( + id(new PHUIErrorView()) + ->setErrors($confirm_errors) + ->setSeverity($severity)) + ->setHeaderText(pht('Confirm Invites')) + ->appendChild($invite_table) + ->appendChild($confirm_form); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendRemarkupInstructions( + pht( + 'To invite users to Phabricator, enter their email addresses below. '. + 'Separate addresses with commas or newlines.')) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Email Addresses')) + ->setName(pht('emails')) + ->setValue($emails) + ->setError($e_emails) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)) + ->appendRemarkupInstructions( + pht( + 'You can optionally include a heartfelt personal message in '. + 'the email.')) + ->appendChild( + id(new AphrontFormTextAreaControl()) + ->setLabel(pht('Message')) + ->setName(pht('message')) + ->setValue($message) + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue( + $is_confirm + ? pht('Update Preview') + : pht('Continue')) + ->addCancelButton($this->getApplicationURI('invite/'))); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText( + $is_confirm + ? pht('Revise Invites') + : pht('Invite Users')) + ->setFormErrors($errors) + ->appendChild($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $confirm_box, + $box, + ), + array( + 'title' => $title, + )); + } + +} -- 2.51.2