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, + )); + } + +}