@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+844 -3
+5
resources/sql/autopatches/20150210.invitephid.sql
··· 1 + ALTER TABLE {$NAMESPACE}_user.user_authinvite 2 + ADD phid VARBINARY(64) NOT NULL; 3 + 4 + ALTER TABLE {$NAMESPACE}_user.user_authinvite 5 + ADD UNIQUE KEY `key_phid` (phid);
+20 -1
src/__phutil_library_map__.php
··· 1347 1347 'PhabricatorAuthHighSecurityToken' => 'applications/auth/data/PhabricatorAuthHighSecurityToken.php', 1348 1348 'PhabricatorAuthInvite' => 'applications/auth/storage/PhabricatorAuthInvite.php', 1349 1349 'PhabricatorAuthInviteAccountException' => 'applications/auth/exception/PhabricatorAuthInviteAccountException.php', 1350 + 'PhabricatorAuthInviteAction' => 'applications/auth/data/PhabricatorAuthInviteAction.php', 1351 + 'PhabricatorAuthInviteActionTableView' => 'applications/auth/view/PhabricatorAuthInviteActionTableView.php', 1350 1352 'PhabricatorAuthInviteController' => 'applications/auth/controller/PhabricatorAuthInviteController.php', 1351 1353 'PhabricatorAuthInviteDialogException' => 'applications/auth/exception/PhabricatorAuthInviteDialogException.php', 1352 1354 'PhabricatorAuthInviteEngine' => 'applications/auth/engine/PhabricatorAuthInviteEngine.php', 1353 1355 'PhabricatorAuthInviteException' => 'applications/auth/exception/PhabricatorAuthInviteException.php', 1354 1356 'PhabricatorAuthInviteInvalidException' => 'applications/auth/exception/PhabricatorAuthInviteInvalidException.php', 1355 1357 'PhabricatorAuthInviteLoginException' => 'applications/auth/exception/PhabricatorAuthInviteLoginException.php', 1358 + 'PhabricatorAuthInvitePHIDType' => 'applications/auth/phid/PhabricatorAuthInvitePHIDType.php', 1359 + 'PhabricatorAuthInviteQuery' => 'applications/auth/query/PhabricatorAuthInviteQuery.php', 1356 1360 'PhabricatorAuthInviteRegisteredException' => 'applications/auth/exception/PhabricatorAuthInviteRegisteredException.php', 1361 + 'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php', 1357 1362 'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php', 1358 1363 'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php', 1359 1364 'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php', ··· 2142 2147 'PhabricatorPeopleExternalPHIDType' => 'applications/people/phid/PhabricatorPeopleExternalPHIDType.php', 2143 2148 'PhabricatorPeopleFeedController' => 'applications/people/controller/PhabricatorPeopleFeedController.php', 2144 2149 'PhabricatorPeopleHovercardEventListener' => 'applications/people/event/PhabricatorPeopleHovercardEventListener.php', 2150 + 'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php', 2151 + 'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php', 2152 + 'PhabricatorPeopleInviteSendController' => 'applications/people/controller/PhabricatorPeopleInviteSendController.php', 2145 2153 'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php', 2146 2154 'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php', 2147 2155 'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php', ··· 4564 4572 'PhabricatorAuthFactorConfig' => 'PhabricatorAuthDAO', 4565 4573 'PhabricatorAuthFinishController' => 'PhabricatorAuthController', 4566 4574 'PhabricatorAuthHighSecurityRequiredException' => 'Exception', 4567 - 'PhabricatorAuthInvite' => 'PhabricatorUserDAO', 4575 + 'PhabricatorAuthInvite' => array( 4576 + 'PhabricatorUserDAO', 4577 + 'PhabricatorPolicyInterface', 4578 + ), 4568 4579 'PhabricatorAuthInviteAccountException' => 'PhabricatorAuthInviteDialogException', 4580 + 'PhabricatorAuthInviteAction' => 'Phobject', 4581 + 'PhabricatorAuthInviteActionTableView' => 'AphrontView', 4569 4582 'PhabricatorAuthInviteController' => 'PhabricatorAuthController', 4570 4583 'PhabricatorAuthInviteDialogException' => 'PhabricatorAuthInviteException', 4571 4584 'PhabricatorAuthInviteEngine' => 'Phobject', 4572 4585 'PhabricatorAuthInviteException' => 'Exception', 4573 4586 'PhabricatorAuthInviteInvalidException' => 'PhabricatorAuthInviteDialogException', 4574 4587 'PhabricatorAuthInviteLoginException' => 'PhabricatorAuthInviteDialogException', 4588 + 'PhabricatorAuthInvitePHIDType' => 'PhabricatorPHIDType', 4589 + 'PhabricatorAuthInviteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4575 4590 'PhabricatorAuthInviteRegisteredException' => 'PhabricatorAuthInviteException', 4591 + 'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine', 4576 4592 'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase', 4577 4593 'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException', 4578 4594 'PhabricatorAuthLinkController' => 'PhabricatorAuthController', ··· 5409 5425 'PhabricatorPeopleExternalPHIDType' => 'PhabricatorPHIDType', 5410 5426 'PhabricatorPeopleFeedController' => 'PhabricatorPeopleController', 5411 5427 'PhabricatorPeopleHovercardEventListener' => 'PhabricatorEventListener', 5428 + 'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController', 5429 + 'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController', 5430 + 'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController', 5412 5431 'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController', 5413 5432 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 5414 5433 'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
+192
src/applications/auth/data/PhabricatorAuthInviteAction.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthInviteAction extends Phobject { 4 + 5 + private $rawInput; 6 + private $emailAddress; 7 + private $userPHID; 8 + private $issues = array(); 9 + private $action; 10 + 11 + const ACTION_SEND = 'invite.send'; 12 + const ACTION_ERROR = 'invite.error'; 13 + const ACTION_IGNORE = 'invite.ignore'; 14 + 15 + const ISSUE_PARSE = 'invite.parse'; 16 + const ISSUE_DUPLICATE = 'invite.duplicate'; 17 + const ISSUE_UNVERIFIED = 'invite.unverified'; 18 + const ISSUE_VERIFIED = 'invite.verified'; 19 + const ISSUE_INVITED = 'invite.invited'; 20 + const ISSUE_ACCEPTED = 'invite.accepted'; 21 + 22 + public function getRawInput() { 23 + return $this->rawInput; 24 + } 25 + 26 + public function getEmailAddress() { 27 + return $this->emailAddress; 28 + } 29 + 30 + public function getUserPHID() { 31 + return $this->userPHID; 32 + } 33 + 34 + public function getIssues() { 35 + return $this->issues; 36 + } 37 + 38 + public function setAction($action) { 39 + $this->action = $action; 40 + return $this; 41 + } 42 + 43 + public function getAction() { 44 + return $this->action; 45 + } 46 + 47 + public function willSend() { 48 + return ($this->action == self::ACTION_SEND); 49 + } 50 + 51 + public function getShortNameForIssue($issue) { 52 + $map = array( 53 + self::ISSUE_PARSE => pht('Not a Valid Email Address'), 54 + self::ISSUE_DUPLICATE => pht('Address Duplicated in Input'), 55 + self::ISSUE_UNVERIFIED => pht('Unverified User Email'), 56 + self::ISSUE_VERIFIED => pht('Verified User Email'), 57 + self::ISSUE_INVITED => pht('Previously Invited'), 58 + self::ISSUE_ACCEPTED => pht('Already Accepted Invite'), 59 + ); 60 + 61 + return idx($map, $issue); 62 + } 63 + 64 + public function getShortNameForAction($action) { 65 + $map = array( 66 + self::ACTION_SEND => pht('Will Send Invite'), 67 + self::ACTION_ERROR => pht('Address Error'), 68 + self::ACTION_IGNORE => pht('Will Ignore Address'), 69 + ); 70 + 71 + return idx($map, $action); 72 + } 73 + 74 + public function getIconForAction($action) { 75 + switch ($action) { 76 + case self::ACTION_SEND: 77 + $icon = 'fa-envelope-o'; 78 + $color = 'green'; 79 + break; 80 + case self::ACTION_IGNORE: 81 + $icon = 'fa-ban'; 82 + $color = 'grey'; 83 + break; 84 + case self::ACTION_ERROR: 85 + $icon = 'fa-exclamation-triangle'; 86 + $color = 'red'; 87 + break; 88 + } 89 + 90 + return id(new PHUIIconView()) 91 + ->setIconFont("{$icon} {$color}"); 92 + } 93 + 94 + public static function newActionListFromAddresses( 95 + PhabricatorUser $viewer, 96 + array $addresses) { 97 + 98 + $results = array(); 99 + foreach ($addresses as $address) { 100 + $result = new PhabricatorAuthInviteAction(); 101 + $result->rawInput = $address; 102 + 103 + $email = new PhutilEmailAddress($address); 104 + $result->emailAddress = phutil_utf8_strtolower($email->getAddress()); 105 + 106 + if (!preg_match('/^\S+@\S+\.\S+\z/', $result->emailAddress)) { 107 + $result->issues[] = self::ISSUE_PARSE; 108 + } 109 + 110 + $results[] = $result; 111 + } 112 + 113 + // Identify duplicates. 114 + $address_groups = mgroup($results, 'getEmailAddress'); 115 + foreach ($address_groups as $address => $group) { 116 + if (count($group) > 1) { 117 + foreach ($group as $action) { 118 + $action->issues[] = self::ISSUE_DUPLICATE; 119 + } 120 + } 121 + } 122 + 123 + // Identify addresses which are already in the system. 124 + $addresses = mpull($results, 'getEmailAddress'); 125 + $email_objects = id(new PhabricatorUserEmail())->loadAllWhere( 126 + 'address IN (%Ls)', 127 + $addresses); 128 + 129 + $email_map = array(); 130 + foreach ($email_objects as $email_object) { 131 + $address_key = phutil_utf8_strtolower($email_object->getAddress()); 132 + $email_map[$address_key] = $email_object; 133 + } 134 + 135 + // Identify outstanding invites. 136 + $invites = id(new PhabricatorAuthInviteQuery()) 137 + ->setViewer($viewer) 138 + ->withEmailAddresses($addresses) 139 + ->execute(); 140 + $invite_map = mpull($invites, null, 'getEmailAddress'); 141 + 142 + foreach ($results as $action) { 143 + $email = idx($email_map, $action->getEmailAddress()); 144 + if ($email) { 145 + if ($email->getUserPHID()) { 146 + $action->userPHID = $email->getUserPHID(); 147 + if ($email->getIsVerified()) { 148 + $action->issues[] = self::ISSUE_VERIFIED; 149 + } else { 150 + $action->issues[] = self::ISSUE_UNVERIFIED; 151 + } 152 + } 153 + } 154 + 155 + $invite = idx($invite_map, $action->getEmailAddress()); 156 + if ($invite) { 157 + if ($invite->getAcceptedByPHID()) { 158 + $action->issues[] = self::ISSUE_ACCEPTED; 159 + if (!$action->userPHID) { 160 + // This could be different from the user who is currently attached 161 + // to the email address if the address was removed or added to a 162 + // different account later. Only show it if the address was 163 + // removed, since the current status is more up-to-date otherwise. 164 + $action->userPHID = $invite->getAcceptedByPHID(); 165 + } 166 + } else { 167 + $action->issues[] = self::ISSUE_INVITED; 168 + } 169 + } 170 + } 171 + 172 + foreach ($results as $result) { 173 + foreach ($result->getIssues() as $issue) { 174 + switch ($issue) { 175 + case self::ISSUE_PARSE: 176 + $result->action = self::ACTION_ERROR; 177 + break; 178 + case self::ISSUE_ACCEPTED: 179 + case self::ISSUE_VERIFIED: 180 + $result->action = self::ACTION_IGNORE; 181 + break; 182 + } 183 + } 184 + if (!$result->action) { 185 + $result->action = self::ACTION_SEND; 186 + } 187 + } 188 + 189 + return $results; 190 + } 191 + 192 + }
+1 -1
src/applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php
··· 17 17 array $phids) { 18 18 19 19 // TODO: Maybe we need this eventually? 20 - throw new Exception(pht('Not Supported')); 20 + throw new PhutilMethodNotImplementedException(); 21 21 } 22 22 23 23 public function loadHandles(
+31
src/applications/auth/phid/PhabricatorAuthInvitePHIDType.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthInvitePHIDType extends PhabricatorPHIDType { 4 + 5 + const TYPECONST = 'AINV'; 6 + 7 + public function getTypeName() { 8 + return pht('Auth Invite'); 9 + } 10 + 11 + public function newObject() { 12 + return new PhabricatorAuthInvite(); 13 + } 14 + 15 + protected function buildQueryForObjects( 16 + PhabricatorObjectQuery $query, 17 + array $phids) { 18 + throw new PhutilMethodNotImplementedException(); 19 + } 20 + 21 + public function loadHandles( 22 + PhabricatorHandleQuery $query, 23 + array $handles, 24 + array $objects) { 25 + 26 + foreach ($handles as $phid => $handle) { 27 + $invite = $objects[$phid]; 28 + } 29 + } 30 + 31 + }
+113
src/applications/auth/query/PhabricatorAuthInviteQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthInviteQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $phids; 8 + private $emailAddresses; 9 + private $verificationCodes; 10 + private $authorPHIDs; 11 + 12 + public function withIDs(array $ids) { 13 + $this->ids = $ids; 14 + return $this; 15 + } 16 + 17 + public function withPHIDs(array $phids) { 18 + $this->phids = $phids; 19 + return $this; 20 + } 21 + 22 + public function withEmailAddresses(array $addresses) { 23 + $this->emailAddresses = $addresses; 24 + return $this; 25 + } 26 + 27 + public function withVerificationCodes(array $codes) { 28 + $this->verificationCodes = $codes; 29 + return $this; 30 + } 31 + 32 + public function withAuthorPHIDs(array $phids) { 33 + $this->authorPHIDs = $phids; 34 + return $this; 35 + } 36 + 37 + protected function loadPage() { 38 + $table = new PhabricatorAuthInvite(); 39 + $conn_r = $table->establishConnection('r'); 40 + 41 + $data = queryfx_all( 42 + $conn_r, 43 + 'SELECT * FROM %T %Q %Q %Q', 44 + $table->getTableName(), 45 + $this->buildWhereClause($conn_r), 46 + $this->buildOrderClause($conn_r), 47 + $this->buildLimitClause($conn_r)); 48 + 49 + $invites = $table->loadAllFromArray($data); 50 + 51 + // If the objects were loaded via verification code, set a flag to make 52 + // sure the viewer can see them. 53 + if ($this->verificationCodes !== null) { 54 + foreach ($invites as $invite) { 55 + $invite->setViewerHasVerificationCode(true); 56 + } 57 + } 58 + 59 + return $invites; 60 + } 61 + 62 + protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { 63 + $where = array(); 64 + 65 + if ($this->ids !== null) { 66 + $where[] = qsprintf( 67 + $conn_r, 68 + 'id IN (%Ld)', 69 + $this->ids); 70 + } 71 + 72 + if ($this->phids !== null) { 73 + $where[] = qsprintf( 74 + $conn_r, 75 + 'phid IN (%Ls)', 76 + $this->phids); 77 + } 78 + 79 + if ($this->emailAddresses !== null) { 80 + $where[] = qsprintf( 81 + $conn_r, 82 + 'emailAddress IN (%Ls)', 83 + $this->emailAddresses); 84 + } 85 + 86 + if ($this->verificationCodes !== null) { 87 + $hashes = array(); 88 + foreach ($this->verificationCodes as $code) { 89 + $hashes[] = PhabricatorHash::digestForIndex($code); 90 + } 91 + $where[] = qsprintf( 92 + $conn_r, 93 + 'verificationHash IN (%Ls)', 94 + $hashes); 95 + } 96 + 97 + if ($this->authorPHIDs !== null) { 98 + $where[] = qsprintf( 99 + $conn_r, 100 + 'authorPHID IN (%Ls)', 101 + $this->authorPHIDs); 102 + } 103 + 104 + $where[] = $this->buildPagingClause($conn_r); 105 + 106 + return $this->formatWhereClause($where); 107 + } 108 + 109 + public function getQueryApplicationClass() { 110 + return 'PhabricatorAuthApplication'; 111 + } 112 + 113 + }
+95
src/applications/auth/query/PhabricatorAuthInviteSearchEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthInviteSearchEngine 4 + extends PhabricatorApplicationSearchEngine { 5 + 6 + public function getResultTypeDescription() { 7 + return pht('Email Invites'); 8 + } 9 + 10 + public function getApplicationClassName() { 11 + return 'PhabricatorAuthApplication'; 12 + } 13 + 14 + public function buildSavedQueryFromRequest(AphrontRequest $request) { 15 + $saved = new PhabricatorSavedQuery(); 16 + 17 + return $saved; 18 + } 19 + 20 + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { 21 + $query = id(new PhabricatorAuthInviteQuery()); 22 + 23 + return $query; 24 + } 25 + 26 + public function buildSearchForm( 27 + AphrontFormView $form, 28 + PhabricatorSavedQuery $saved) {} 29 + 30 + protected function getURI($path) { 31 + return '/people/invite/'.$path; 32 + } 33 + 34 + protected function getBuiltinQueryNames() { 35 + $names = array( 36 + 'all' => pht('All'), 37 + ); 38 + 39 + return $names; 40 + } 41 + 42 + public function buildSavedQueryFromBuiltin($query_key) { 43 + $query = $this->newSavedQuery(); 44 + $query->setQueryKey($query_key); 45 + 46 + switch ($query_key) { 47 + case 'all': 48 + return $query; 49 + } 50 + 51 + return parent::buildSavedQueryFromBuiltin($query_key); 52 + } 53 + 54 + protected function getRequiredHandlePHIDsForResultList( 55 + array $invites, 56 + PhabricatorSavedQuery $query) { 57 + 58 + $phids = array(); 59 + foreach ($invites as $invite) { 60 + $phids[$invite->getAuthorPHID()] = true; 61 + if ($invite->getAcceptedByPHID()) { 62 + $phids[$invite->getAcceptedByPHID()] = true; 63 + } 64 + } 65 + 66 + return array_keys($phids); 67 + } 68 + 69 + protected function renderResultList( 70 + array $invites, 71 + PhabricatorSavedQuery $query, 72 + array $handles) { 73 + assert_instances_of($invites, 'PhabricatorAuthInvite'); 74 + 75 + $viewer = $this->requireViewer(); 76 + 77 + $rows = array(); 78 + foreach ($invites as $invite) { 79 + $rows[] = array( 80 + $invite->getEmailAddress(), 81 + $handles[$invite->getAuthorPHID()]->renderLink(), 82 + ($invite->getAcceptedByPHID() 83 + ? $handles[$invite->getAcceptedByPHID()]->renderLink() 84 + : null), 85 + phabricator_datetime($invite->getDateCreated(), $viewer), 86 + ); 87 + } 88 + 89 + $table = new AphrontTableView($rows); 90 + 91 + return id(new PHUIObjectBoxView()) 92 + ->setHeaderText(pht('Email Invitations')) 93 + ->appendChild($table); 94 + } 95 + }
+57 -1
src/applications/auth/storage/PhabricatorAuthInvite.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorAuthInvite 4 - extends PhabricatorUserDAO { 4 + extends PhabricatorUserDAO 5 + implements PhabricatorPolicyInterface { 5 6 6 7 protected $authorPHID; 7 8 protected $emailAddress; ··· 9 10 protected $acceptedByPHID; 10 11 11 12 private $verificationCode; 13 + private $viewerHasVerificationCode; 12 14 13 15 protected function getConfiguration() { 14 16 return array( 17 + self::CONFIG_AUX_PHID => true, 15 18 self::CONFIG_COLUMN_SCHEMA => array( 16 19 'emailAddress' => 'sort128', 17 20 'verificationHash' => 'bytes12', ··· 30 33 ) + parent::getConfiguration(); 31 34 } 32 35 36 + public function generatePHID() { 37 + return PhabricatorPHID::generateNewPHID( 38 + PhabricatorAuthInvitePHIDType::TYPECONST); 39 + } 40 + 33 41 public function getVerificationCode() { 34 42 if (!$this->getVerificationHash()) { 35 43 if ($this->verificationHash) { ··· 50 58 } 51 59 52 60 return parent::save(); 61 + } 62 + 63 + public function setViewerHasVerificationCode($loaded) { 64 + $this->viewerHasVerificationCode = $loaded; 65 + return $this; 66 + } 67 + 68 + 69 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 70 + 71 + 72 + public function getCapabilities() { 73 + return array( 74 + PhabricatorPolicyCapability::CAN_VIEW, 75 + ); 76 + } 77 + 78 + public function getPolicy($capability) { 79 + switch ($capability) { 80 + case PhabricatorPolicyCapability::CAN_VIEW: 81 + return PhabricatorPolicies::POLICY_ADMIN; 82 + } 83 + } 84 + 85 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 86 + if ($this->viewerHasVerificationCode) { 87 + return true; 88 + } 89 + 90 + if ($viewer->getPHID()) { 91 + if ($viewer->getPHID() == $this->getAuthorPHID()) { 92 + // You can see invites you sent. 93 + return true; 94 + } 95 + 96 + if ($viewer->getPHID() == $this->getAcceptedByPHID()) { 97 + // You can see invites you have accepted. 98 + return true; 99 + } 100 + } 101 + 102 + return false; 103 + } 104 + 105 + public function describeAutomaticCapability($capability) { 106 + return pht( 107 + 'Invites are visible to administrators, the inviting user, users with '. 108 + 'an invite code, and the user who accepts the invite.'); 53 109 } 54 110 55 111 }
+80
src/applications/auth/view/PhabricatorAuthInviteActionTableView.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthInviteActionTableView extends AphrontView { 4 + 5 + private $inviteActions; 6 + private $handles; 7 + 8 + public function setInviteActions(array $invite_actions) { 9 + $this->inviteActions = $invite_actions; 10 + return $this; 11 + } 12 + 13 + public function getInviteActions() { 14 + return $this->inviteActions; 15 + } 16 + 17 + public function setHandles(array $handles) { 18 + $this->handles = $handles; 19 + return $this; 20 + } 21 + 22 + public function render() { 23 + $actions = $this->getInviteActions(); 24 + $handles = $this->handles; 25 + 26 + $rows = array(); 27 + $rowc = array(); 28 + foreach ($actions as $action) { 29 + $issues = $action->getIssues(); 30 + foreach ($issues as $key => $issue) { 31 + $issues[$key] = $action->getShortNameForIssue($issue); 32 + } 33 + $issues = implode(', ', $issues); 34 + 35 + if (!$action->willSend()) { 36 + $rowc[] = 'highlighted'; 37 + } else { 38 + $rowc[] = null; 39 + } 40 + 41 + $action_icon = $action->getIconForAction($action->getAction()); 42 + $action_name = $action->getShortNameForAction($action->getAction()); 43 + 44 + $rows[] = array( 45 + $action->getRawInput(), 46 + $action->getEmailAddress(), 47 + ($action->getUserPHID() 48 + ? $handles[$action->getUserPHID()]->renderLink() 49 + : null), 50 + $issues, 51 + $action_icon, 52 + $action_name, 53 + ); 54 + } 55 + 56 + $table = id(new AphrontTableView($rows)) 57 + ->setRowClasses($rowc) 58 + ->setHeaders( 59 + array( 60 + pht('Raw Address'), 61 + pht('Parsed Address'), 62 + pht('User'), 63 + pht('Issues'), 64 + null, 65 + pht('Action'), 66 + )) 67 + ->setColumnClasses( 68 + array( 69 + '', 70 + '', 71 + '', 72 + 'wide', 73 + 'icon', 74 + '', 75 + )); 76 + 77 + return $table; 78 + } 79 + 80 + }
+6
src/applications/people/application/PhabricatorPeopleApplication.php
··· 46 46 '(query/(?P<key>[^/]+)/)?' => 'PhabricatorPeopleListController', 47 47 'logs/(?:query/(?P<queryKey>[^/]+)/)?' 48 48 => 'PhabricatorPeopleLogsController', 49 + 'invite/' => array( 50 + '(?:query/(?P<queryKey>[^/]+)/)?' 51 + => 'PhabricatorPeopleInviteListController', 52 + 'send/' 53 + => 'PhabricatorPeopleInviteSendController', 54 + ), 49 55 'approve/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleApproveController', 50 56 '(?P<via>disapprove)/(?P<id>[1-9]\d*)/' 51 57 => 'PhabricatorPeopleDisableController',
+1
src/applications/people/controller/PhabricatorPeopleController.php
··· 34 34 } 35 35 36 36 $nav->addFilter('logs', pht('Activity Logs')); 37 + $nav->addFilter('invite', pht('Email Invitations')); 37 38 } 38 39 } 39 40
+14
src/applications/people/controller/PhabricatorPeopleInviteController.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorPeopleInviteController 4 + extends PhabricatorPeopleController { 5 + 6 + protected function buildApplicationCrumbs() { 7 + $crumbs = parent::buildApplicationCrumbs(); 8 + $crumbs->addTextCrumb( 9 + pht('Invites'), 10 + $this->getApplicationURI('invite/')); 11 + return $crumbs; 12 + } 13 + 14 + }
+44
src/applications/people/controller/PhabricatorPeopleInviteListController.php
··· 1 + <?php 2 + 3 + final class PhabricatorPeopleInviteListController 4 + extends PhabricatorPeopleInviteController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $controller = id(new PhabricatorApplicationSearchController()) 8 + ->setQueryKey($request->getURIData('queryKey')) 9 + ->setSearchEngine(new PhabricatorAuthInviteSearchEngine()) 10 + ->setNavigation($this->buildSideNavView()); 11 + 12 + return $this->delegateToController($controller); 13 + } 14 + 15 + public function buildSideNavView($for_app = false) { 16 + $nav = new AphrontSideNavFilterView(); 17 + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); 18 + 19 + $viewer = $this->getRequest()->getUser(); 20 + 21 + id(new PhabricatorAuthInviteSearchEngine()) 22 + ->setViewer($viewer) 23 + ->addNavigationItems($nav->getMenu()); 24 + 25 + return $nav; 26 + } 27 + 28 + protected function buildApplicationCrumbs() { 29 + $crumbs = parent::buildApplicationCrumbs(); 30 + 31 + $can_invite = $this->hasApplicationCapability( 32 + PeopleCreateUsersCapability::CAPABILITY); 33 + $crumbs->addAction( 34 + id(new PHUIListItemView()) 35 + ->setName(pht('Invite Users')) 36 + ->setHref($this->getApplicationURI('invite/send/')) 37 + ->setIcon('fa-plus-square') 38 + ->setDisabled(!$can_invite) 39 + ->setWorkflow(!$can_invite)); 40 + 41 + return $crumbs; 42 + } 43 + 44 + }
+185
src/applications/people/controller/PhabricatorPeopleInviteSendController.php
··· 1 + <?php 2 + 3 + final class PhabricatorPeopleInviteSendController 4 + extends PhabricatorPeopleInviteController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $this->getViewer(); 8 + 9 + $this->requireApplicationCapability( 10 + PeopleCreateUsersCapability::CAPABILITY); 11 + 12 + $is_confirm = false; 13 + $errors = array(); 14 + $confirm_errors = array(); 15 + $e_emails = true; 16 + 17 + $message = $request->getStr('message'); 18 + $emails = $request->getStr('emails'); 19 + $severity = PHUIErrorView::SEVERITY_ERROR; 20 + if ($request->isFormPost()) { 21 + // NOTE: We aren't using spaces as a delimiter here because email 22 + // addresses with names often include spaces. 23 + $email_list = preg_split('/[,;\n]+/', $emails); 24 + foreach ($email_list as $key => $email) { 25 + if (!strlen(trim($email))) { 26 + unset($email_list[$key]); 27 + } 28 + } 29 + 30 + if ($email_list) { 31 + $e_emails = null; 32 + } else { 33 + $e_emails = pht('Required'); 34 + $errors[] = pht( 35 + 'To send invites, you must enter at least one email address.'); 36 + } 37 + 38 + if (!$errors) { 39 + $is_confirm = true; 40 + 41 + $actions = PhabricatorAuthInviteAction::newActionListFromAddresses( 42 + $viewer, 43 + $email_list); 44 + 45 + $any_valid = false; 46 + $all_valid = true; 47 + $action_send = PhabricatorAuthInviteAction::ACTION_SEND; 48 + foreach ($actions as $action) { 49 + if ($action->getAction() == $action_send) { 50 + $any_valid = true; 51 + } else { 52 + $all_valid = false; 53 + } 54 + } 55 + 56 + if (!$any_valid) { 57 + $confirm_errors[] = pht( 58 + 'None of the provided addresses are valid invite recipients. '. 59 + 'Review the table below for details. Revise the address list '. 60 + 'to continue.'); 61 + } else if ($all_valid) { 62 + $confirm_errors[] = pht( 63 + 'All of the addresses appear to be valid invite recipients. '. 64 + 'Confirm the actions below to continue.'); 65 + $severity = PHUIErrorView::SEVERITY_NOTICE; 66 + } else { 67 + $confirm_errors[] = pht( 68 + 'Some of the addresses you entered do not appear to be '. 69 + 'valid recipients. Review the table below. You can revise '. 70 + 'the address list, or ignore these errors and continue.'); 71 + $severity = PHUIErrorView::SEVERITY_WARNING; 72 + } 73 + 74 + if ($any_valid && $request->getBool('confirm')) { 75 + throw new Exception( 76 + pht('TODO: This workflow is not yet fully implemented.')); 77 + } 78 + } 79 + } 80 + 81 + if ($is_confirm) { 82 + $title = pht('Confirm Invites'); 83 + } else { 84 + $title = pht('Invite Users'); 85 + } 86 + 87 + $crumbs = $this->buildApplicationCrumbs(); 88 + if ($is_confirm) { 89 + $crumbs->addTextCrumb(pht('Confirm')); 90 + } else { 91 + $crumbs->addTextCrumb(pht('Invite Users')); 92 + } 93 + 94 + $confirm_box = null; 95 + if ($is_confirm) { 96 + 97 + $handles = array(); 98 + if ($actions) { 99 + $handles = $this->loadViewerHandles(mpull($actions, 'getUserPHID')); 100 + } 101 + 102 + $invite_table = id(new PhabricatorAuthInviteActionTableView()) 103 + ->setUser($viewer) 104 + ->setInviteActions($actions) 105 + ->setHandles($handles); 106 + 107 + $confirm_form = null; 108 + if ($any_valid) { 109 + $confirm_form = id(new AphrontFormView()) 110 + ->setUser($viewer) 111 + ->addHiddenInput('message', $message) 112 + ->addHiddenInput('emails', $emails) 113 + ->addHiddenInput('confirm', true) 114 + ->appendRemarkupInstructions( 115 + pht( 116 + 'If everything looks good, click **Send Invitations** to '. 117 + 'deliver email invitations these users. Otherwise, edit the '. 118 + 'email list or personal message at the bottom of the page to '. 119 + 'revise the invitations.')) 120 + ->appendChild( 121 + id(new AphrontFormSubmitControl()) 122 + ->setValue(pht('Send Invitations'))); 123 + } 124 + 125 + $confirm_box = id(new PHUIObjectBoxView()) 126 + ->setErrorView( 127 + id(new PHUIErrorView()) 128 + ->setErrors($confirm_errors) 129 + ->setSeverity($severity)) 130 + ->setHeaderText(pht('Confirm Invites')) 131 + ->appendChild($invite_table) 132 + ->appendChild($confirm_form); 133 + } 134 + 135 + $form = id(new AphrontFormView()) 136 + ->setUser($viewer) 137 + ->appendRemarkupInstructions( 138 + pht( 139 + 'To invite users to Phabricator, enter their email addresses below. '. 140 + 'Separate addresses with commas or newlines.')) 141 + ->appendChild( 142 + id(new AphrontFormTextAreaControl()) 143 + ->setLabel(pht('Email Addresses')) 144 + ->setName(pht('emails')) 145 + ->setValue($emails) 146 + ->setError($e_emails) 147 + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)) 148 + ->appendRemarkupInstructions( 149 + pht( 150 + 'You can optionally include a heartfelt personal message in '. 151 + 'the email.')) 152 + ->appendChild( 153 + id(new AphrontFormTextAreaControl()) 154 + ->setLabel(pht('Message')) 155 + ->setName(pht('message')) 156 + ->setValue($message) 157 + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)) 158 + ->appendChild( 159 + id(new AphrontFormSubmitControl()) 160 + ->setValue( 161 + $is_confirm 162 + ? pht('Update Preview') 163 + : pht('Continue')) 164 + ->addCancelButton($this->getApplicationURI('invite/'))); 165 + 166 + $box = id(new PHUIObjectBoxView()) 167 + ->setHeaderText( 168 + $is_confirm 169 + ? pht('Revise Invites') 170 + : pht('Invite Users')) 171 + ->setFormErrors($errors) 172 + ->appendChild($form); 173 + 174 + return $this->buildApplicationPage( 175 + array( 176 + $crumbs, 177 + $confirm_box, 178 + $box, 179 + ), 180 + array( 181 + 'title' => $title, 182 + )); 183 + } 184 + 185 + }