Page MenuHomePhabricator

D11733.diff
No OneTemporary

D11733.diff

diff --git a/resources/sql/autopatches/20150210.invitephid.sql b/resources/sql/autopatches/20150210.invitephid.sql
new file mode 100644
--- /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
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1347,13 +1347,18 @@
'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 @@
'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 @@
'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 @@
'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
--- /dev/null
+++ b/src/applications/auth/data/PhabricatorAuthInviteAction.php
@@ -0,0 +1,192 @@
+<?php
+
+final class PhabricatorAuthInviteAction extends Phobject {
+
+ private $rawInput;
+ private $emailAddress;
+ private $userPHID;
+ private $issues = array();
+ private $action;
+
+ const ACTION_SEND = 'invite.send';
+ const ACTION_ERROR = 'invite.error';
+ const ACTION_IGNORE = 'invite.ignore';
+
+ const ISSUE_PARSE = 'invite.parse';
+ const ISSUE_DUPLICATE = 'invite.duplicate';
+ const ISSUE_UNVERIFIED = 'invite.unverified';
+ const ISSUE_VERIFIED = 'invite.verified';
+ const ISSUE_INVITED = 'invite.invited';
+ const ISSUE_ACCEPTED = 'invite.accepted';
+
+ public function getRawInput() {
+ return $this->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
--- a/src/applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php
+++ b/src/applications/auth/phid/PhabricatorAuthAuthFactorPHIDType.php
@@ -17,7 +17,7 @@
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
--- /dev/null
+++ b/src/applications/auth/phid/PhabricatorAuthInvitePHIDType.php
@@ -0,0 +1,31 @@
+<?php
+
+final class PhabricatorAuthInvitePHIDType extends PhabricatorPHIDType {
+
+ const TYPECONST = 'AINV';
+
+ public function getTypeName() {
+ return pht('Auth Invite');
+ }
+
+ public function newObject() {
+ return new PhabricatorAuthInvite();
+ }
+
+ protected function buildQueryForObjects(
+ PhabricatorObjectQuery $query,
+ array $phids) {
+ throw new PhutilMethodNotImplementedException();
+ }
+
+ public function loadHandles(
+ PhabricatorHandleQuery $query,
+ array $handles,
+ array $objects) {
+
+ foreach ($handles as $phid => $handle) {
+ $invite = $objects[$phid];
+ }
+ }
+
+}
diff --git a/src/applications/auth/query/PhabricatorAuthInviteQuery.php b/src/applications/auth/query/PhabricatorAuthInviteQuery.php
new file mode 100644
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthInviteQuery.php
@@ -0,0 +1,113 @@
+<?php
+
+final class PhabricatorAuthInviteQuery
+ extends PhabricatorCursorPagedPolicyAwareQuery {
+
+ private $ids;
+ private $phids;
+ private $emailAddresses;
+ private $verificationCodes;
+ private $authorPHIDs;
+
+ public function withIDs(array $ids) {
+ $this->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
--- /dev/null
+++ b/src/applications/auth/query/PhabricatorAuthInviteSearchEngine.php
@@ -0,0 +1,95 @@
+<?php
+
+final class PhabricatorAuthInviteSearchEngine
+ extends PhabricatorApplicationSearchEngine {
+
+ public function getResultTypeDescription() {
+ return pht('Email Invites');
+ }
+
+ public function getApplicationClassName() {
+ return 'PhabricatorAuthApplication';
+ }
+
+ public function buildSavedQueryFromRequest(AphrontRequest $request) {
+ $saved = new PhabricatorSavedQuery();
+
+ return $saved;
+ }
+
+ public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
+ $query = id(new PhabricatorAuthInviteQuery());
+
+ return $query;
+ }
+
+ public function buildSearchForm(
+ AphrontFormView $form,
+ PhabricatorSavedQuery $saved) {}
+
+ protected function getURI($path) {
+ return '/people/invite/'.$path;
+ }
+
+ protected function getBuiltinQueryNames() {
+ $names = array(
+ 'all' => 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
--- a/src/applications/auth/storage/PhabricatorAuthInvite.php
+++ b/src/applications/auth/storage/PhabricatorAuthInvite.php
@@ -1,7 +1,8 @@
<?php
final class PhabricatorAuthInvite
- extends PhabricatorUserDAO {
+ extends PhabricatorUserDAO
+ implements PhabricatorPolicyInterface {
protected $authorPHID;
protected $emailAddress;
@@ -9,9 +10,11 @@
protected $acceptedByPHID;
private $verificationCode;
+ private $viewerHasVerificationCode;
protected function getConfiguration() {
return array(
+ self::CONFIG_AUX_PHID => true,
self::CONFIG_COLUMN_SCHEMA => array(
'emailAddress' => 'sort128',
'verificationHash' => 'bytes12',
@@ -30,6 +33,11 @@
) + parent::getConfiguration();
}
+ public function generatePHID() {
+ return PhabricatorPHID::generateNewPHID(
+ PhabricatorAuthInvitePHIDType::TYPECONST);
+ }
+
public function getVerificationCode() {
if (!$this->getVerificationHash()) {
if ($this->verificationHash) {
@@ -52,4 +60,52 @@
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
--- /dev/null
+++ b/src/applications/auth/view/PhabricatorAuthInviteActionTableView.php
@@ -0,0 +1,80 @@
+<?php
+
+final class PhabricatorAuthInviteActionTableView extends AphrontView {
+
+ private $inviteActions;
+ private $handles;
+
+ public function setInviteActions(array $invite_actions) {
+ $this->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
--- a/src/applications/people/application/PhabricatorPeopleApplication.php
+++ b/src/applications/people/application/PhabricatorPeopleApplication.php
@@ -46,6 +46,12 @@
'(query/(?P<key>[^/]+)/)?' => 'PhabricatorPeopleListController',
'logs/(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhabricatorPeopleLogsController',
+ 'invite/' => array(
+ '(?:query/(?P<queryKey>[^/]+)/)?'
+ => 'PhabricatorPeopleInviteListController',
+ 'send/'
+ => 'PhabricatorPeopleInviteSendController',
+ ),
'approve/(?P<id>[1-9]\d*)/' => 'PhabricatorPeopleApproveController',
'(?P<via>disapprove)/(?P<id>[1-9]\d*)/'
=> 'PhabricatorPeopleDisableController',
diff --git a/src/applications/people/controller/PhabricatorPeopleController.php b/src/applications/people/controller/PhabricatorPeopleController.php
--- a/src/applications/people/controller/PhabricatorPeopleController.php
+++ b/src/applications/people/controller/PhabricatorPeopleController.php
@@ -34,6 +34,7 @@
}
$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
--- /dev/null
+++ b/src/applications/people/controller/PhabricatorPeopleInviteController.php
@@ -0,0 +1,14 @@
+<?php
+
+abstract class PhabricatorPeopleInviteController
+ extends PhabricatorPeopleController {
+
+ protected function buildApplicationCrumbs() {
+ $crumbs = parent::buildApplicationCrumbs();
+ $crumbs->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
--- /dev/null
+++ b/src/applications/people/controller/PhabricatorPeopleInviteListController.php
@@ -0,0 +1,44 @@
+<?php
+
+final class PhabricatorPeopleInviteListController
+ extends PhabricatorPeopleInviteController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $controller = id(new PhabricatorApplicationSearchController())
+ ->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
--- /dev/null
+++ b/src/applications/people/controller/PhabricatorPeopleInviteSendController.php
@@ -0,0 +1,185 @@
+<?php
+
+final class PhabricatorPeopleInviteSendController
+ extends PhabricatorPeopleInviteController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $this->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,
+ ));
+ }
+
+}

File Metadata

Mime Type
text/plain
Expires
Thu, May 9, 8:02 PM (3 w, 6 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/ui/yg/f323q2xlpnlmj3bi
Default Alt Text
D11733.diff (32 KB)

Event Timeline