From f0c6ee48233a199ab35ae64293fbed99305a1316 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 16 Jan 2019 09:56:44 -0800 Subject: [PATCH] Add "Contact Numbers" so we can send users SMS mesages Summary: Ref T920. To send you SMS messages, we need to know your phone number. This adds bare-bone basics (transactions, storage, editor, etc). From here: **Disabling Numbers**: I'll let you disable numbers in an upcoming diff. **Primary Number**: I think I'm just going to let you pick a number as "primary", similar to how email works. We could imagine a world where you have one "MFA" number and one "notifications" number, but this seems unlikely-ish? **Publishing Numbers (Profile / API)**: At some point, we could let you say that a number is public / "show on my profile" and provide API access / directory features. Not planning to touch this for now. **Non-Phone Numbers**: Eventually this could be a list of other similar contact mechanisms (APNS/GCM devices, Whatsapp numbers, ICQ number, twitter handle so MFA can slide into your DM's?). Not planning to touch this for now, but the path should be straightforward when we get there. This is why it's called "Contact Number", not "Phone Number". **MFA-Required + SMS**: Right now, if the only MFA provider is SMS and MFA is required on the install, you can't actually get into Settings to add a contact number to configure SMS. I'll look at the best way to deal with this in an upcoming diff -- likely, giving you partial access to more of Setings before you get thorugh the MFA gate. Conceptually, it seems reasonable to let you adjust some other settings, like "Language" and "Accessibility", before you set up MFA, so if the "you need to add MFA" portal was more like a partial Settings screen, maybe that's pretty reasonable. **Verifying Numbers**: We'll probably need to tackle this eventually, but I'm not planning to worry about it for now. Test Plan: {F6137174} Reviewers: amckinley Reviewed By: amckinley Subscribers: avivey, PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T920 Differential Revision: https://secure.phabricator.com/D19988 --- .../20190116.contact.01.number.sql | 11 ++ .../20190116.contact.02.xaction.sql | 19 +++ src/__phutil_library_map__.php | 33 ++++ .../PhabricatorAuthApplication.php | 6 + ...PhabricatorAuthContactNumberController.php | 16 ++ ...ricatorAuthContactNumberEditController.php | 12 ++ ...ricatorAuthContactNumberViewController.php | 98 ++++++++++++ ...PhabricatorAuthContactNumberEditEngine.php | 86 +++++++++++ .../PhabricatorAuthContactNumberEditor.php | 38 +++++ .../PhabricatorAuthContactNumberPHIDType.php | 38 +++++ .../PhabricatorAuthContactNumberQuery.php | 90 +++++++++++ ...catorAuthContactNumberTransactionQuery.php | 10 ++ .../storage/PhabricatorAuthContactNumber.php | 141 ++++++++++++++++++ ...habricatorAuthContactNumberTransaction.php | 18 +++ ...atorAuthContactNumberNumberTransaction.php | 91 +++++++++++ ...icatorAuthContactNumberTransactionType.php | 4 + .../message/PhabricatorPhoneNumber.php | 14 +- .../PhabricatorPhoneNumberTestCase.php | 37 +++++ .../view/PhabricatorSearchResultView.php | 2 +- ...PhabricatorContactNumbersSettingsPanel.php | 69 +++++++++ 20 files changed, 830 insertions(+), 3 deletions(-) create mode 100644 resources/sql/autopatches/20190116.contact.01.number.sql create mode 100644 resources/sql/autopatches/20190116.contact.02.xaction.sql create mode 100644 src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php create mode 100644 src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php create mode 100644 src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php create mode 100644 src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php create mode 100644 src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php create mode 100644 src/applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php create mode 100644 src/applications/auth/query/PhabricatorAuthContactNumberQuery.php create mode 100644 src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php create mode 100644 src/applications/auth/storage/PhabricatorAuthContactNumber.php create mode 100644 src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php create mode 100644 src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php create mode 100644 src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php create mode 100644 src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php diff --git a/resources/sql/autopatches/20190116.contact.01.number.sql b/resources/sql/autopatches/20190116.contact.01.number.sql new file mode 100644 index 0000000000..14e2b78d1d --- /dev/null +++ b/resources/sql/autopatches/20190116.contact.01.number.sql @@ -0,0 +1,11 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_contactnumber ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + contactNumber VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, + status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + uniqueKey BINARY(12), + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190116.contact.02.xaction.sql b/resources/sql/autopatches/20190116.contact.02.xaction.sql new file mode 100644 index 0000000000..bd0d361bc5 --- /dev/null +++ b/resources/sql/autopatches/20190116.contact.02.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_auth.auth_contactnumbertransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9a5341c0ba..132ea75367 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2200,6 +2200,18 @@ phutil_register_library_map(array( 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php', 'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php', + 'PhabricatorAuthContactNumber' => 'applications/auth/storage/PhabricatorAuthContactNumber.php', + 'PhabricatorAuthContactNumberController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberController.php', + 'PhabricatorAuthContactNumberEditController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php', + 'PhabricatorAuthContactNumberEditEngine' => 'applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php', + 'PhabricatorAuthContactNumberEditor' => 'applications/auth/editor/PhabricatorAuthContactNumberEditor.php', + 'PhabricatorAuthContactNumberNumberTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php', + 'PhabricatorAuthContactNumberPHIDType' => 'applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php', + 'PhabricatorAuthContactNumberQuery' => 'applications/auth/query/PhabricatorAuthContactNumberQuery.php', + 'PhabricatorAuthContactNumberTransaction' => 'applications/auth/storage/PhabricatorAuthContactNumberTransaction.php', + 'PhabricatorAuthContactNumberTransactionQuery' => 'applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php', + 'PhabricatorAuthContactNumberTransactionType' => 'applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php', + 'PhabricatorAuthContactNumberViewController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php', 'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php', 'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php', 'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php', @@ -2739,6 +2751,7 @@ phutil_register_library_map(array( 'PhabricatorConpherenceWidgetVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceWidgetVisibleSetting.php', 'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php', 'PhabricatorConsoleContentSource' => 'infrastructure/contentsource/PhabricatorConsoleContentSource.php', + 'PhabricatorContactNumbersSettingsPanel' => 'applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php', 'PhabricatorContentSource' => 'infrastructure/contentsource/PhabricatorContentSource.php', 'PhabricatorContentSourceModule' => 'infrastructure/contentsource/PhabricatorContentSourceModule.php', 'PhabricatorContentSourceView' => 'infrastructure/contentsource/PhabricatorContentSourceView.php', @@ -3870,6 +3883,7 @@ phutil_register_library_map(array( 'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php', 'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php', 'PhabricatorPhoneNumber' => 'applications/metamta/message/PhabricatorPhoneNumber.php', + 'PhabricatorPhoneNumberTestCase' => 'applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php', 'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php', 'PhabricatorPhortuneContentSource' => 'applications/phortune/contentsource/PhabricatorPhortuneContentSource.php', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php', @@ -7884,6 +7898,23 @@ phutil_register_library_map(array( 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker', 'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController', + 'PhabricatorAuthContactNumber' => array( + 'PhabricatorAuthDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorAuthContactNumberController' => 'PhabricatorAuthController', + 'PhabricatorAuthContactNumberEditController' => 'PhabricatorAuthContactNumberController', + 'PhabricatorAuthContactNumberEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType', + 'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorAuthContactNumberTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorAuthContactNumberTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorAuthContactNumberViewController' => 'PhabricatorAuthContactNumberController', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorAuthDAO' => 'PhabricatorLiskDAO', 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', @@ -8524,6 +8555,7 @@ phutil_register_library_map(array( 'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting', 'PhabricatorConsoleApplication' => 'PhabricatorApplication', 'PhabricatorConsoleContentSource' => 'PhabricatorContentSource', + 'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorContentSource' => 'Phobject', 'PhabricatorContentSourceModule' => 'PhabricatorConfigModule', 'PhabricatorContentSourceView' => 'AphrontView', @@ -9816,6 +9848,7 @@ phutil_register_library_map(array( 'PhabricatorPholioApplication' => 'PhabricatorApplication', 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', 'PhabricatorPhoneNumber' => 'Phobject', + 'PhabricatorPhoneNumberTestCase' => 'PhabricatorTestCase', 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', 'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource', 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 2c36e935ee..20547d8ca3 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -104,6 +104,12 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'PhabricatorAuthMessageViewController', ), + 'contact/' => array( + $this->getEditRoutePattern('edit/') => + 'PhabricatorAuthContactNumberEditController', + '(?P[1-9]\d*)/' => + 'PhabricatorAuthContactNumberViewController', + ), ), '/oauth/(?P\w+)/login/' diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php new file mode 100644 index 0000000000..a713f48a3b --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php @@ -0,0 +1,16 @@ +addTextCrumb( + pht('Contact Numbers'), + pht('/settings/panel/contact/')); + + return $crumbs; + } + +} diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php new file mode 100644 index 0000000000..95764496da --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php @@ -0,0 +1,12 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php new file mode 100644 index 0000000000..5423d93dcd --- /dev/null +++ b/src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php @@ -0,0 +1,98 @@ +getViewer(); + + $number = id(new PhabricatorAuthContactNumberQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->executeOne(); + if (!$number) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($number->getObjectName()) + ->setBorder(true); + + $header = $this->buildHeaderView($number); + $properties = $this->buildPropertiesView($number); + $curtain = $this->buildCurtain($number); + + $timeline = $this->buildTransactionTimeline( + $number, + new PhabricatorAuthContactNumberTransactionQuery()); + $timeline->setShouldTerminate(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $timeline, + )) + ->addPropertySection(pht('Details'), $properties); + + return $this->newPage() + ->setTitle($number->getDisplayName()) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs( + array( + $number->getPHID(), + )) + ->appendChild($view); + } + + private function buildHeaderView(PhabricatorAuthContactNumber $number) { + $viewer = $this->getViewer(); + + $view = id(new PHUIHeaderView()) + ->setViewer($viewer) + ->setHeader($number->getObjectName()) + ->setPolicyObject($number); + + return $view; + } + + private function buildPropertiesView( + PhabricatorAuthContactNumber $number) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer); + + $view->addProperty( + pht('Owner'), + $viewer->renderHandle($number->getObjectPHID())); + + $view->addProperty(pht('Contact Number'), $number->getDisplayName()); + + return $view; + } + + private function buildCurtain(PhabricatorAuthContactNumber $number) { + $viewer = $this->getViewer(); + $id = $number->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $number, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($number); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Contact Number')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("contact/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $curtain; + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php b/src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php new file mode 100644 index 0000000000..5b1a059b2f --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php @@ -0,0 +1,86 @@ +getViewer(); + return PhabricatorAuthContactNumber::initializeNewContactNumber($viewer); + } + + protected function newObjectQuery() { + return new PhabricatorAuthContactNumberQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create Contact Number'); + } + + protected function getObjectCreateButtonText($object) { + return pht('Create Contact Number'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Contact Number'); + } + + protected function getObjectEditShortText($object) { + return $object->getObjectName(); + } + + protected function getObjectCreateShortText() { + return pht('Create Contact Number'); + } + + protected function getObjectName() { + return pht('Contact Number'); + } + + protected function getEditorURI() { + return '/auth/contact/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/settings/panel/contact/'; + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('contactNumber') + ->setTransactionType( + PhabricatorAuthContactNumberNumberTransaction::TRANSACTIONTYPE) + ->setLabel(pht('Contact Number')) + ->setDescription(pht('The contact number.')) + ->setValue($object->getContactNumber()) + ->setIsRequired(true), + ); + } + +} diff --git a/src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php b/src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php new file mode 100644 index 0000000000..9dfb569e89 --- /dev/null +++ b/src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php @@ -0,0 +1,38 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $contact_number = $objects[$phid]; + } + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php b/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php new file mode 100644 index 0000000000..10cfba7a65 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthContactNumberQuery.php @@ -0,0 +1,90 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + + public function withUniqueKeys(array $unique_keys) { + $this->uniqueKeys = $unique_keys; + return $this; + } + + public function newResultObject() { + return new PhabricatorAuthContactNumber(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->objectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'status IN (%Ls)', + $this->statuses); + } + + if ($this->uniqueKeys !== null) { + $where[] = qsprintf( + $conn, + 'uniqueKey IN (%Ls)', + $this->uniqueKeys); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorAuthApplication'; + } + +} diff --git a/src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php b/src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php new file mode 100644 index 0000000000..a443cbab42 --- /dev/null +++ b/src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php @@ -0,0 +1,10 @@ + array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_AUX_PHID => true, + self::CONFIG_COLUMN_SCHEMA => array( + 'contactNumber' => 'text255', + 'status' => 'text32', + 'uniqueKey' => 'bytes12?', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_object' => array( + 'columns' => array('objectPHID'), + ), + 'key_unique' => array( + 'columns' => array('uniqueKey'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public static function initializeNewContactNumber($object) { + return id(new self()) + ->setStatus(self::STATUS_ACTIVE) + ->setObjectPHID($object->getPHID()); + } + + public function getPHIDType() { + return PhabricatorAuthContactNumberPHIDType::TYPECONST; + } + + public function getURI() { + return urisprintf('/auth/contact/%s/', $this->getID()); + } + + public function getObjectName() { + return pht('Contact Number %d', $this->getID()); + } + + public function getDisplayName() { + return $this->getContactNumber(); + } + + public function isDisabled() { + return ($this->getStatus() === self::STATUS_DISABLED); + } + + public function newIconView() { + if ($this->isDisabled()) { + return id(new PHUIIconView()) + ->setIcon('fa-ban', 'grey') + ->setTooltip(pht('Disabled')); + } + + return id(new PHUIIconView()) + ->setIcon('fa-mobile', 'green') + ->setTooltip(pht('Active Phone Number')); + } + + public function newUniqueKey() { + $parts = array( + // This is future-proofing for a world where we have multiple types + // of contact numbers, so we might be able to avoid re-hashing + // everything. + 'phone', + $this->getContactNumber(), + ); + + $parts = implode("\0", $parts); + + return PhabricatorHash::digestForIndex($parts); + } + + public function save() { + $this->uniqueKey = $this->newUniqueKey(); + return parent::save(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getObjectPHID(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorAuthContactNumberEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorAuthContactNumberTransaction(); + } + + +} diff --git a/src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php b/src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php new file mode 100644 index 0000000000..d6faccf497 --- /dev/null +++ b/src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php @@ -0,0 +1,18 @@ +getContactNumber(); + } + + public function generateNewValue($object, $value) { + $number = new PhabricatorPhoneNumber($value); + return $number->toE164(); + } + + public function applyInternalEffects($object, $value) { + $object->setContactNumber($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + return pht( + '%s changed this contact number from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $current_value = $object->getContactNumber(); + if ($this->isEmptyTextTransaction($current_value, $xactions)) { + $errors[] = $this->newRequiredError( + pht('Contact numbers must have a contact number.')); + return $errors; + } + + $max_length = $object->getColumnMaximumByteLength('contactNumber'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Contact numbers can not be longer than %s characters.', + new PhutilNumber($max_length)), + $xaction); + continue; + } + + try { + new PhabricatorPhoneNumber($new_value); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + pht( + 'Contact number is invalid: %s', + $ex->getMessage()), + $xaction); + continue; + } + + $new_value = $this->generateNewValue($object, $new_value); + + $unique_key = id(clone $object) + ->setContactNumber($new_value) + ->newUniqueKey(); + + $other = id(new PhabricatorAuthContactNumberQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withUniqueKeys(array($unique_key)) + ->executeOne(); + + if ($other) { + if ($other->getID() !== $object->getID()) { + $errors[] = $this->newInvalidError( + pht('Contact number is already in use.'), + $xaction); + continue; + } + } + + } + + return $errors; + } + +} diff --git a/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php new file mode 100644 index 0000000000..c32fbe6a30 --- /dev/null +++ b/src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php @@ -0,0 +1,4 @@ +number = $number; } diff --git a/src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php b/src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php new file mode 100644 index 0000000000..4a5da3bcc5 --- /dev/null +++ b/src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php @@ -0,0 +1,37 @@ + '+15555555555', + '+1 (555) 555-5555' => '+15555555555', + '(555) 555-5555' => '+15555555555', + + '' => false, + '1-800-CALL-SAUL' => false, + ); + + foreach ($map as $input => $expect) { + $caught = null; + try { + $actual = id(new PhabricatorPhoneNumber($input)) + ->toE164(); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->assertEqual( + (bool)$caught, + ($expect === false), + pht('Exception raised by: %s', $input)); + + if ($expect !== false) { + $this->assertEqual($expect, $actual, pht('E164 of: %s', $input)); + } + } + + } + +} diff --git a/src/applications/search/view/PhabricatorSearchResultView.php b/src/applications/search/view/PhabricatorSearchResultView.php index 6c527733e8..b209b4422a 100644 --- a/src/applications/search/view/PhabricatorSearchResultView.php +++ b/src/applications/search/view/PhabricatorSearchResultView.php @@ -126,7 +126,7 @@ final class PhabricatorSearchResultView extends AphrontView { } // Go through the string one display glyph at a time. If a glyph starts - // on a highlighted byte position, turn on highlighting for the nubmer + // on a highlighted byte position, turn on highlighting for the number // of matching bytes. If a query searches for "e" and the document contains // an "e" followed by a bunch of combining marks, this will correctly // highlight the entire glyph. diff --git a/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php new file mode 100644 index 0000000000..0bfe747ec3 --- /dev/null +++ b/src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php @@ -0,0 +1,69 @@ +getUser(); + $viewer = $request->getUser(); + + $numbers = id(new PhabricatorAuthContactNumberQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($user->getPHID())) + ->execute(); + + $rows = array(); + foreach ($numbers as $number) { + $rows[] = array( + $number->newIconView(), + phutil_tag( + 'a', + array( + 'href' => $number->getURI(), + ), + $number->getDisplayName()), + phabricator_datetime($number->getDateCreated(), $viewer), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString( + pht("You haven't added any contact numbers to your account.")) + ->setHeaders( + array( + null, + pht('Number'), + pht('Created'), + )) + ->setColumnClasses( + array( + null, + 'wide pri', + 'right', + )); + + $buttons = array(); + + $buttons[] = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-plus') + ->setText(pht('Add Contact Number')) + ->setHref('/auth/contact/edit/') + ->setColor(PHUIButtonView::GREY); + + return $this->newBox(pht('Contact Numbers'), $table, $buttons); + } + +}