diff --git a/resources/sql/autopatches/20191113.identity.01.email.sql b/resources/sql/autopatches/20191113.identity.01.email.sql new file mode 100644 index 0000000000..938e6c0767 --- /dev/null +++ b/resources/sql/autopatches/20191113.identity.01.email.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_repository.repository_identity + ADD emailAddress VARCHAR(255) COLLATE {$COLLATE_SORT}; diff --git a/resources/sql/autopatches/20191113.identity.02.populate.php b/resources/sql/autopatches/20191113.identity.02.populate.php new file mode 100644 index 0000000000..ca86be0733 --- /dev/null +++ b/resources/sql/autopatches/20191113.identity.02.populate.php @@ -0,0 +1,26 @@ +establishConnection('w'); + +$iterator = new LiskRawMigrationIterator($conn, $table->getTableName()); +foreach ($iterator as $row) { + $name = $row['identityNameRaw']; + $name = phutil_utf8ize($name); + + $email = new PhutilEmailAddress($name); + $address = $email->getAddress(); + + try { + queryfx( + $conn, + 'UPDATE %R SET emailAddress = %ns WHERE id = %d', + $table, + $address, + $row['id']); + } catch (Exception $ex) { + // We may occasionally run into issues with binary or very long addresses. + // Just skip over them. + continue; + } +} diff --git a/src/applications/diffusion/controller/DiffusionIdentityViewController.php b/src/applications/diffusion/controller/DiffusionIdentityViewController.php index 20efe2749f..0ac131daf1 100644 --- a/src/applications/diffusion/controller/DiffusionIdentityViewController.php +++ b/src/applications/diffusion/controller/DiffusionIdentityViewController.php @@ -1,135 +1,138 @@ getViewer(); $id = $request->getURIData('id'); $identity = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$identity) { return new Aphront404Response(); } $title = pht('Identity %d', $identity->getID()); $curtain = $this->buildCurtain($identity); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($identity->getIdentityShortName()) - ->setHeaderIcon('fa-globe') - ->setPolicyObject($identity); + ->setHeaderIcon('fa-globe'); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($identity->getID()); + $crumbs->addTextCrumb($identity->getObjectName()); $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $identity, new PhabricatorRepositoryIdentityTransactionQuery()); $timeline->setShouldTerminate(true); $properties = $this->buildPropertyList($identity); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) ->setMainColumn(array( $properties, $timeline, )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function buildCurtain(PhabricatorRepositoryIdentity $identity) { $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $identity, PhabricatorPolicyCapability::CAN_EDIT); $id = $identity->getID(); $edit_uri = $this->getApplicationURI("identity/edit/{$id}/"); $curtain = $this->newCurtainView($identity); $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Identity')) ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); return $curtain; } private function buildPropertyList( PhabricatorRepositoryIdentity $identity) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) - ->setUser($viewer); + ->setViewer($viewer); + + $properties->addProperty( + pht('Email Address'), + $identity->getEmailAddress()); $effective_phid = $identity->getCurrentEffectiveUserPHID(); $automatic_phid = $identity->getAutomaticGuessedUserPHID(); $manual_phid = $identity->getManuallySetUserPHID(); if ($effective_phid) { $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setColor('green') ->setIcon('fa-check') ->setName('Assigned'); } else { $tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setColor('indigo') ->setIcon('fa-bomb') ->setName('Unassigned'); } $properties->addProperty( pht('Effective User'), $this->buildPropertyValue($effective_phid)); $properties->addProperty( pht('Automatically Detected User'), $this->buildPropertyValue($automatic_phid)); $properties->addProperty( - pht('Manually Set User'), + pht('Assigned To'), $this->buildPropertyValue($manual_phid)); $header = id(new PHUIHeaderView()) ->setHeader(array(pht('Identity Assignments'), $tag)); return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); } private function buildPropertyValue($value) { $viewer = $this->getViewer(); if ($value == DiffusionIdentityUnassignedDatasource::FUNCTION_TOKEN) { return phutil_tag('em', array(), pht('Explicitly Unassigned')); } else if (!$value) { - return null; + return phutil_tag('em', array(), pht('None')); } else { return $viewer->renderHandle($value); } } } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryListController.php b/src/applications/diffusion/controller/DiffusionRepositoryListController.php index 5a21d2e3f1..66226e5eab 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryListController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryListController.php @@ -1,36 +1,44 @@ setType(PHUIListItemView::TYPE_LABEL) ->setName(pht('Commits')); $items[] = id(new PHUIListItemView()) ->setName(pht('Browse Commits')) ->setHref($this->getApplicationURI('commit/')); + $items[] = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LABEL) + ->setName(pht('Identities')); + + $items[] = id(new PHUIListItemView()) + ->setName(pht('Browse Identities')) + ->setHref($this->getApplicationURI('identity/')); + return id(new PhabricatorRepositorySearchEngine()) ->setController($this) ->setNavigationItems($items) ->buildResponse(); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); id(new DiffusionRepositoryEditEngine()) ->setViewer($this->getViewer()) ->addActionToCrumbs($crumbs); return $crumbs; } } diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php index c64b1a296b..6ddf8d57c1 100644 --- a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php @@ -1,131 +1,130 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withIdentityNames(array $names) { $this->identityNames = $names; return $this; } public function withIdentityNameLike($name_like) { $this->identityNameLike = $name_like; return $this; } - public function withEmailAddress($address) { - $this->emailAddress = $address; + public function withEmailAddresses(array $addresses) { + $this->emailAddresses = $addresses; return $this; } public function withAssigneePHIDs(array $assignees) { $this->assigneePHIDs = $assignees; return $this; } public function withHasEffectivePHID($has_effective_phid) { $this->hasEffectivePHID = $has_effective_phid; return $this; } public function newResultObject() { return new PhabricatorRepositoryIdentity(); } protected function getPrimaryTableAlias() { return 'repository_identity'; } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'repository_identity.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'repository_identity.phid IN (%Ls)', $this->phids); } if ($this->assigneePHIDs !== null) { $where[] = qsprintf( $conn, 'repository_identity.currentEffectiveUserPHID IN (%Ls)', $this->assigneePHIDs); } if ($this->hasEffectivePHID !== null) { if ($this->hasEffectivePHID) { $where[] = qsprintf( $conn, 'repository_identity.currentEffectiveUserPHID IS NOT NULL'); } else { $where[] = qsprintf( $conn, 'repository_identity.currentEffectiveUserPHID IS NULL'); } } if ($this->identityNames !== null) { $name_hashes = array(); foreach ($this->identityNames as $name) { $name_hashes[] = PhabricatorHash::digestForIndex($name); } $where[] = qsprintf( $conn, 'repository_identity.identityNameHash IN (%Ls)', $name_hashes); } - if ($this->emailAddress !== null) { - $identity_style = "<{$this->emailAddress}>"; + if ($this->emailAddresses !== null) { $where[] = qsprintf( $conn, - 'repository_identity.identityNameRaw LIKE %<', - $identity_style); + 'repository_identity.emailAddress IN (%Ls)', + $this->emailAddresses); } if ($this->identityNameLike != null) { $where[] = qsprintf( $conn, 'repository_identity.identityNameRaw LIKE %~', $this->identityNameLike); } return $where; } public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } } diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index e3833bd10e..c33b296fdc 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -1,130 +1,159 @@ true, self::CONFIG_BINARY => array( 'identityNameRaw' => true, ), self::CONFIG_COLUMN_SCHEMA => array( 'identityNameHash' => 'bytes12', 'identityNameEncoding' => 'text16?', 'automaticGuessedUserPHID' => 'phid?', 'manuallySetUserPHID' => 'phid?', 'currentEffectiveUserPHID' => 'phid?', + 'emailAddress' => 'sort255?', ), self::CONFIG_KEY_SCHEMA => array( 'key_identity' => array( 'columns' => array('identityNameHash'), 'unique' => true, ), + 'key_email' => array( + 'columns' => array('emailAddress(64)'), + ), ), ) + parent::getConfiguration(); } public function getPHIDType() { return PhabricatorRepositoryIdentityPHIDType::TYPECONST; } public function setIdentityName($name_raw) { $this->setIdentityNameRaw($name_raw); $this->setIdentityNameHash(PhabricatorHash::digestForIndex($name_raw)); $this->setIdentityNameEncoding($this->detectEncodingForStorage($name_raw)); return $this; } public function getIdentityName() { return $this->getUTF8StringFromStorage( $this->getIdentityNameRaw(), $this->getIdentityNameEncoding()); } public function getIdentityEmailAddress() { $address = new PhutilEmailAddress($this->getIdentityName()); return $address->getAddress(); } public function getIdentityDisplayName() { $address = new PhutilEmailAddress($this->getIdentityName()); return $address->getDisplayName(); } public function getIdentityShortName() { // TODO return $this->getIdentityName(); } + public function getObjectName() { + return pht('Identity %d', $this->getID()); + } + public function getURI() { return '/diffusion/identity/view/'.$this->getID().'/'; } public function hasEffectiveUser() { return ($this->currentEffectiveUserPHID != null); } public function getIdentityDisplayPHID() { if ($this->hasEffectiveUser()) { return $this->getCurrentEffectiveUserPHID(); } else { return $this->getPHID(); } } public function save() { if ($this->manuallySetUserPHID) { $this->currentEffectiveUserPHID = $this->manuallySetUserPHID; } else { $this->currentEffectiveUserPHID = $this->automaticGuessedUserPHID; } + $email_address = $this->getIdentityEmailAddress(); + + // Raw identities are unrestricted binary data, and may consequently + // have arbitrarily long, binary email address information. We can't + // store this kind of information in the "emailAddress" column, which + // has column type "sort255". + + // This kind of address almost certainly not legitimate and users can + // manually set the target of the identity, so just discard it rather + // than trying especially hard to make it work. + + $byte_limit = $this->getColumnMaximumByteLength('emailAddress'); + $email_address = phutil_utf8ize($email_address); + if (strlen($email_address) > $byte_limit) { + $email_address = null; + } + + $this->setEmailAddress($email_address); + return parent::save(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability( - $capability, PhabricatorUser $viewer) { + $capability, + PhabricatorUser $viewer) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DiffusionRepositoryIdentityEditor(); } public function getApplicationTransactionTemplate() { return new PhabricatorRepositoryIdentityTransaction(); } } diff --git a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php index 3c129845cd..0ce01ce76a 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryIdentityChangeWorker.php @@ -1,34 +1,34 @@ getTaskData(); $user_phid = idx($task_data, 'userPHID'); $user = id(new PhabricatorPeopleQuery()) ->withPHIDs(array($user_phid)) ->setViewer($viewer) ->executeOne(); $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s ORDER BY address', $user->getPHID()); foreach ($emails as $email) { $identities = id(new PhabricatorRepositoryIdentityQuery()) ->setViewer($viewer) - ->withEmailAddress($email->getAddress()) + ->withEmailAddresses($email->getAddress()) ->execute(); foreach ($identities as $identity) { $identity->setAutomaticGuessedUserPHID($user->getPHID()) ->save(); } } } }