@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 "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

+830 -3
+11
resources/sql/autopatches/20190116.contact.01.number.sql
··· 1 + CREATE TABLE {$NAMESPACE}_auth.auth_contactnumber ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARBINARY(64) NOT NULL, 4 + objectPHID VARBINARY(64) NOT NULL, 5 + contactNumber VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, 6 + status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, 7 + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, 8 + uniqueKey BINARY(12), 9 + dateCreated INT UNSIGNED NOT NULL, 10 + dateModified INT UNSIGNED NOT NULL 11 + ) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
+19
resources/sql/autopatches/20190116.contact.02.xaction.sql
··· 1 + CREATE TABLE {$NAMESPACE}_auth.auth_contactnumbertransaction ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + phid VARBINARY(64) NOT NULL, 4 + authorPHID VARBINARY(64) NOT NULL, 5 + objectPHID VARBINARY(64) NOT NULL, 6 + viewPolicy VARBINARY(64) NOT NULL, 7 + editPolicy VARBINARY(64) NOT NULL, 8 + commentPHID VARBINARY(64) DEFAULT NULL, 9 + commentVersion INT UNSIGNED NOT NULL, 10 + transactionType VARCHAR(32) NOT NULL, 11 + oldValue LONGTEXT NOT NULL, 12 + newValue LONGTEXT NOT NULL, 13 + contentSource LONGTEXT NOT NULL, 14 + metadata LONGTEXT NOT NULL, 15 + dateCreated INT UNSIGNED NOT NULL, 16 + dateModified INT UNSIGNED NOT NULL, 17 + UNIQUE KEY `key_phid` (`phid`), 18 + KEY `key_object` (`objectPHID`) 19 + ) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
+33
src/__phutil_library_map__.php
··· 2200 2200 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 2201 2201 'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php', 2202 2202 'PhabricatorAuthConfirmLinkController' => 'applications/auth/controller/PhabricatorAuthConfirmLinkController.php', 2203 + 'PhabricatorAuthContactNumber' => 'applications/auth/storage/PhabricatorAuthContactNumber.php', 2204 + 'PhabricatorAuthContactNumberController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberController.php', 2205 + 'PhabricatorAuthContactNumberEditController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php', 2206 + 'PhabricatorAuthContactNumberEditEngine' => 'applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php', 2207 + 'PhabricatorAuthContactNumberEditor' => 'applications/auth/editor/PhabricatorAuthContactNumberEditor.php', 2208 + 'PhabricatorAuthContactNumberNumberTransaction' => 'applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php', 2209 + 'PhabricatorAuthContactNumberPHIDType' => 'applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php', 2210 + 'PhabricatorAuthContactNumberQuery' => 'applications/auth/query/PhabricatorAuthContactNumberQuery.php', 2211 + 'PhabricatorAuthContactNumberTransaction' => 'applications/auth/storage/PhabricatorAuthContactNumberTransaction.php', 2212 + 'PhabricatorAuthContactNumberTransactionQuery' => 'applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php', 2213 + 'PhabricatorAuthContactNumberTransactionType' => 'applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php', 2214 + 'PhabricatorAuthContactNumberViewController' => 'applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php', 2203 2215 'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php', 2204 2216 'PhabricatorAuthDAO' => 'applications/auth/storage/PhabricatorAuthDAO.php', 2205 2217 'PhabricatorAuthDisableController' => 'applications/auth/controller/config/PhabricatorAuthDisableController.php', ··· 2739 2751 'PhabricatorConpherenceWidgetVisibleSetting' => 'applications/settings/setting/PhabricatorConpherenceWidgetVisibleSetting.php', 2740 2752 'PhabricatorConsoleApplication' => 'applications/console/application/PhabricatorConsoleApplication.php', 2741 2753 'PhabricatorConsoleContentSource' => 'infrastructure/contentsource/PhabricatorConsoleContentSource.php', 2754 + 'PhabricatorContactNumbersSettingsPanel' => 'applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php', 2742 2755 'PhabricatorContentSource' => 'infrastructure/contentsource/PhabricatorContentSource.php', 2743 2756 'PhabricatorContentSourceModule' => 'infrastructure/contentsource/PhabricatorContentSourceModule.php', 2744 2757 'PhabricatorContentSourceView' => 'infrastructure/contentsource/PhabricatorContentSourceView.php', ··· 3870 3883 'PhabricatorPholioApplication' => 'applications/pholio/application/PhabricatorPholioApplication.php', 3871 3884 'PhabricatorPholioMockTestDataGenerator' => 'applications/pholio/lipsum/PhabricatorPholioMockTestDataGenerator.php', 3872 3885 'PhabricatorPhoneNumber' => 'applications/metamta/message/PhabricatorPhoneNumber.php', 3886 + 'PhabricatorPhoneNumberTestCase' => 'applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php', 3873 3887 'PhabricatorPhortuneApplication' => 'applications/phortune/application/PhabricatorPhortuneApplication.php', 3874 3888 'PhabricatorPhortuneContentSource' => 'applications/phortune/contentsource/PhabricatorPhortuneContentSource.php', 3875 3889 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'applications/phortune/management/PhabricatorPhortuneManagementInvoiceWorkflow.php', ··· 7884 7898 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 7885 7899 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker', 7886 7900 'PhabricatorAuthConfirmLinkController' => 'PhabricatorAuthController', 7901 + 'PhabricatorAuthContactNumber' => array( 7902 + 'PhabricatorAuthDAO', 7903 + 'PhabricatorApplicationTransactionInterface', 7904 + 'PhabricatorPolicyInterface', 7905 + 'PhabricatorDestructibleInterface', 7906 + ), 7907 + 'PhabricatorAuthContactNumberController' => 'PhabricatorAuthController', 7908 + 'PhabricatorAuthContactNumberEditController' => 'PhabricatorAuthContactNumberController', 7909 + 'PhabricatorAuthContactNumberEditEngine' => 'PhabricatorEditEngine', 7910 + 'PhabricatorAuthContactNumberEditor' => 'PhabricatorApplicationTransactionEditor', 7911 + 'PhabricatorAuthContactNumberNumberTransaction' => 'PhabricatorAuthContactNumberTransactionType', 7912 + 'PhabricatorAuthContactNumberPHIDType' => 'PhabricatorPHIDType', 7913 + 'PhabricatorAuthContactNumberQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 7914 + 'PhabricatorAuthContactNumberTransaction' => 'PhabricatorModularTransaction', 7915 + 'PhabricatorAuthContactNumberTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 7916 + 'PhabricatorAuthContactNumberTransactionType' => 'PhabricatorModularTransactionType', 7917 + 'PhabricatorAuthContactNumberViewController' => 'PhabricatorAuthContactNumberController', 7887 7918 'PhabricatorAuthController' => 'PhabricatorController', 7888 7919 'PhabricatorAuthDAO' => 'PhabricatorLiskDAO', 7889 7920 'PhabricatorAuthDisableController' => 'PhabricatorAuthProviderConfigController', ··· 8524 8555 'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting', 8525 8556 'PhabricatorConsoleApplication' => 'PhabricatorApplication', 8526 8557 'PhabricatorConsoleContentSource' => 'PhabricatorContentSource', 8558 + 'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel', 8527 8559 'PhabricatorContentSource' => 'Phobject', 8528 8560 'PhabricatorContentSourceModule' => 'PhabricatorConfigModule', 8529 8561 'PhabricatorContentSourceView' => 'AphrontView', ··· 9816 9848 'PhabricatorPholioApplication' => 'PhabricatorApplication', 9817 9849 'PhabricatorPholioMockTestDataGenerator' => 'PhabricatorTestDataGenerator', 9818 9850 'PhabricatorPhoneNumber' => 'Phobject', 9851 + 'PhabricatorPhoneNumberTestCase' => 'PhabricatorTestCase', 9819 9852 'PhabricatorPhortuneApplication' => 'PhabricatorApplication', 9820 9853 'PhabricatorPhortuneContentSource' => 'PhabricatorContentSource', 9821 9854 'PhabricatorPhortuneManagementInvoiceWorkflow' => 'PhabricatorPhortuneManagementWorkflow',
+6
src/applications/auth/application/PhabricatorAuthApplication.php
··· 104 104 'PhabricatorAuthMessageViewController', 105 105 ), 106 106 107 + 'contact/' => array( 108 + $this->getEditRoutePattern('edit/') => 109 + 'PhabricatorAuthContactNumberEditController', 110 + '(?P<id>[1-9]\d*)/' => 111 + 'PhabricatorAuthContactNumberViewController', 112 + ), 107 113 ), 108 114 109 115 '/oauth/(?P<provider>\w+)/login/'
+16
src/applications/auth/controller/contact/PhabricatorAuthContactNumberController.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorAuthContactNumberController 4 + extends PhabricatorAuthController { 5 + 6 + protected function buildApplicationCrumbs() { 7 + $crumbs = parent::buildApplicationCrumbs(); 8 + 9 + $crumbs->addTextCrumb( 10 + pht('Contact Numbers'), 11 + pht('/settings/panel/contact/')); 12 + 13 + return $crumbs; 14 + } 15 + 16 + }
+12
src/applications/auth/controller/contact/PhabricatorAuthContactNumberEditController.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthContactNumberEditController 4 + extends PhabricatorAuthContactNumberController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + return id(new PhabricatorAuthContactNumberEditEngine()) 8 + ->setController($this) 9 + ->buildResponse(); 10 + } 11 + 12 + }
+98
src/applications/auth/controller/contact/PhabricatorAuthContactNumberViewController.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthContactNumberViewController 4 + extends PhabricatorAuthContactNumberController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $this->getViewer(); 8 + 9 + $number = id(new PhabricatorAuthContactNumberQuery()) 10 + ->setViewer($viewer) 11 + ->withIDs(array($request->getURIData('id'))) 12 + ->executeOne(); 13 + if (!$number) { 14 + return new Aphront404Response(); 15 + } 16 + 17 + $crumbs = $this->buildApplicationCrumbs() 18 + ->addTextCrumb($number->getObjectName()) 19 + ->setBorder(true); 20 + 21 + $header = $this->buildHeaderView($number); 22 + $properties = $this->buildPropertiesView($number); 23 + $curtain = $this->buildCurtain($number); 24 + 25 + $timeline = $this->buildTransactionTimeline( 26 + $number, 27 + new PhabricatorAuthContactNumberTransactionQuery()); 28 + $timeline->setShouldTerminate(true); 29 + 30 + $view = id(new PHUITwoColumnView()) 31 + ->setHeader($header) 32 + ->setCurtain($curtain) 33 + ->setMainColumn( 34 + array( 35 + $timeline, 36 + )) 37 + ->addPropertySection(pht('Details'), $properties); 38 + 39 + return $this->newPage() 40 + ->setTitle($number->getDisplayName()) 41 + ->setCrumbs($crumbs) 42 + ->setPageObjectPHIDs( 43 + array( 44 + $number->getPHID(), 45 + )) 46 + ->appendChild($view); 47 + } 48 + 49 + private function buildHeaderView(PhabricatorAuthContactNumber $number) { 50 + $viewer = $this->getViewer(); 51 + 52 + $view = id(new PHUIHeaderView()) 53 + ->setViewer($viewer) 54 + ->setHeader($number->getObjectName()) 55 + ->setPolicyObject($number); 56 + 57 + return $view; 58 + } 59 + 60 + private function buildPropertiesView( 61 + PhabricatorAuthContactNumber $number) { 62 + $viewer = $this->getViewer(); 63 + 64 + $view = id(new PHUIPropertyListView()) 65 + ->setViewer($viewer); 66 + 67 + $view->addProperty( 68 + pht('Owner'), 69 + $viewer->renderHandle($number->getObjectPHID())); 70 + 71 + $view->addProperty(pht('Contact Number'), $number->getDisplayName()); 72 + 73 + return $view; 74 + } 75 + 76 + private function buildCurtain(PhabricatorAuthContactNumber $number) { 77 + $viewer = $this->getViewer(); 78 + $id = $number->getID(); 79 + 80 + $can_edit = PhabricatorPolicyFilter::hasCapability( 81 + $viewer, 82 + $number, 83 + PhabricatorPolicyCapability::CAN_EDIT); 84 + 85 + $curtain = $this->newCurtainView($number); 86 + 87 + $curtain->addAction( 88 + id(new PhabricatorActionView()) 89 + ->setName(pht('Edit Contact Number')) 90 + ->setIcon('fa-pencil') 91 + ->setHref($this->getApplicationURI("contact/edit/{$id}/")) 92 + ->setDisabled(!$can_edit) 93 + ->setWorkflow(!$can_edit)); 94 + 95 + return $curtain; 96 + } 97 + 98 + }
+86
src/applications/auth/editor/PhabricatorAuthContactNumberEditEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthContactNumberEditEngine 4 + extends PhabricatorEditEngine { 5 + 6 + const ENGINECONST = 'auth.contact'; 7 + 8 + public function isEngineConfigurable() { 9 + return false; 10 + } 11 + 12 + public function getEngineName() { 13 + return pht('Contact Numbers'); 14 + } 15 + 16 + public function getSummaryHeader() { 17 + return pht('Edit Contact Numbers'); 18 + } 19 + 20 + public function getSummaryText() { 21 + return pht('This engine is used to edit contact numbers.'); 22 + } 23 + 24 + public function getEngineApplicationClass() { 25 + return 'PhabricatorAuthApplication'; 26 + } 27 + 28 + protected function newEditableObject() { 29 + $viewer = $this->getViewer(); 30 + return PhabricatorAuthContactNumber::initializeNewContactNumber($viewer); 31 + } 32 + 33 + protected function newObjectQuery() { 34 + return new PhabricatorAuthContactNumberQuery(); 35 + } 36 + 37 + protected function getObjectCreateTitleText($object) { 38 + return pht('Create Contact Number'); 39 + } 40 + 41 + protected function getObjectCreateButtonText($object) { 42 + return pht('Create Contact Number'); 43 + } 44 + 45 + protected function getObjectEditTitleText($object) { 46 + return pht('Edit Contact Number'); 47 + } 48 + 49 + protected function getObjectEditShortText($object) { 50 + return $object->getObjectName(); 51 + } 52 + 53 + protected function getObjectCreateShortText() { 54 + return pht('Create Contact Number'); 55 + } 56 + 57 + protected function getObjectName() { 58 + return pht('Contact Number'); 59 + } 60 + 61 + protected function getEditorURI() { 62 + return '/auth/contact/edit/'; 63 + } 64 + 65 + protected function getObjectCreateCancelURI($object) { 66 + return '/settings/panel/contact/'; 67 + } 68 + 69 + protected function getObjectViewURI($object) { 70 + return $object->getURI(); 71 + } 72 + 73 + protected function buildCustomEditFields($object) { 74 + return array( 75 + id(new PhabricatorTextEditField()) 76 + ->setKey('contactNumber') 77 + ->setTransactionType( 78 + PhabricatorAuthContactNumberNumberTransaction::TRANSACTIONTYPE) 79 + ->setLabel(pht('Contact Number')) 80 + ->setDescription(pht('The contact number.')) 81 + ->setValue($object->getContactNumber()) 82 + ->setIsRequired(true), 83 + ); 84 + } 85 + 86 + }
+38
src/applications/auth/editor/PhabricatorAuthContactNumberEditor.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthContactNumberEditor 4 + extends PhabricatorApplicationTransactionEditor { 5 + 6 + public function getEditorApplicationClass() { 7 + return 'PhabricatorAuthApplication'; 8 + } 9 + 10 + public function getEditorObjectsDescription() { 11 + return pht('Contact Numbers'); 12 + } 13 + 14 + public function getCreateObjectTitle($author, $object) { 15 + return pht('%s created this contact number.', $author); 16 + } 17 + 18 + public function getCreateObjectTitleForFeed($author, $object) { 19 + return pht('%s created %s.', $author, $object); 20 + } 21 + 22 + protected function didCatchDuplicateKeyException( 23 + PhabricatorLiskDAO $object, 24 + array $xactions, 25 + Exception $ex) { 26 + 27 + $errors = array(); 28 + $errors[] = new PhabricatorApplicationTransactionValidationError( 29 + PhabricatorAuthContactNumberNumberTransaction::TRANSACTIONTYPE, 30 + pht('Duplicate'), 31 + pht('This contact number is already in use.'), 32 + null); 33 + 34 + throw new PhabricatorApplicationTransactionValidationException($errors); 35 + } 36 + 37 + 38 + }
+38
src/applications/auth/phid/PhabricatorAuthContactNumberPHIDType.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthContactNumberPHIDType 4 + extends PhabricatorPHIDType { 5 + 6 + const TYPECONST = 'CTNM'; 7 + 8 + public function getTypeName() { 9 + return pht('Contact Number'); 10 + } 11 + 12 + public function newObject() { 13 + return new PhabricatorAuthContactNumber(); 14 + } 15 + 16 + public function getPHIDTypeApplicationClass() { 17 + return 'PhabricatorAuthApplication'; 18 + } 19 + 20 + protected function buildQueryForObjects( 21 + PhabricatorObjectQuery $query, 22 + array $phids) { 23 + 24 + return id(new PhabricatorAuthContactNumberQuery()) 25 + ->withPHIDs($phids); 26 + } 27 + 28 + public function loadHandles( 29 + PhabricatorHandleQuery $query, 30 + array $handles, 31 + array $objects) { 32 + 33 + foreach ($handles as $phid => $handle) { 34 + $contact_number = $objects[$phid]; 35 + } 36 + } 37 + 38 + }
+90
src/applications/auth/query/PhabricatorAuthContactNumberQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthContactNumberQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $phids; 8 + private $objectPHIDs; 9 + private $statuses; 10 + private $uniqueKeys; 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 withObjectPHIDs(array $object_phids) { 23 + $this->objectPHIDs = $object_phids; 24 + return $this; 25 + } 26 + 27 + public function withStatuses(array $statuses) { 28 + $this->statuses = $statuses; 29 + return $this; 30 + } 31 + 32 + public function withUniqueKeys(array $unique_keys) { 33 + $this->uniqueKeys = $unique_keys; 34 + return $this; 35 + } 36 + 37 + public function newResultObject() { 38 + return new PhabricatorAuthContactNumber(); 39 + } 40 + 41 + protected function loadPage() { 42 + return $this->loadStandardPage($this->newResultObject()); 43 + } 44 + 45 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 46 + $where = parent::buildWhereClauseParts($conn); 47 + 48 + if ($this->ids !== null) { 49 + $where[] = qsprintf( 50 + $conn, 51 + 'id IN (%Ld)', 52 + $this->ids); 53 + } 54 + 55 + if ($this->phids !== null) { 56 + $where[] = qsprintf( 57 + $conn, 58 + 'phid IN (%Ls)', 59 + $this->phids); 60 + } 61 + 62 + if ($this->objectPHIDs !== null) { 63 + $where[] = qsprintf( 64 + $conn, 65 + 'objectPHID IN (%Ls)', 66 + $this->objectPHIDs); 67 + } 68 + 69 + if ($this->statuses !== null) { 70 + $where[] = qsprintf( 71 + $conn, 72 + 'status IN (%Ls)', 73 + $this->statuses); 74 + } 75 + 76 + if ($this->uniqueKeys !== null) { 77 + $where[] = qsprintf( 78 + $conn, 79 + 'uniqueKey IN (%Ls)', 80 + $this->uniqueKeys); 81 + } 82 + 83 + return $where; 84 + } 85 + 86 + public function getQueryApplicationClass() { 87 + return 'PhabricatorAuthApplication'; 88 + } 89 + 90 + }
+10
src/applications/auth/query/PhabricatorAuthContactNumberTransactionQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthContactNumberTransactionQuery 4 + extends PhabricatorApplicationTransactionQuery { 5 + 6 + public function getTemplateApplicationTransaction() { 7 + return new PhabricatorAuthContactNumberTransaction(); 8 + } 9 + 10 + }
+141
src/applications/auth/storage/PhabricatorAuthContactNumber.php
··· 1 + <?php 2 + 3 + 4 + final class PhabricatorAuthContactNumber 5 + extends PhabricatorAuthDAO 6 + implements 7 + PhabricatorApplicationTransactionInterface, 8 + PhabricatorPolicyInterface, 9 + PhabricatorDestructibleInterface { 10 + 11 + protected $objectPHID; 12 + protected $contactNumber; 13 + protected $uniqueKey; 14 + protected $status; 15 + protected $properties = array(); 16 + 17 + const STATUS_ACTIVE = 'active'; 18 + const STATUS_DISABLED = 'disabled'; 19 + 20 + protected function getConfiguration() { 21 + return array( 22 + self::CONFIG_SERIALIZATION => array( 23 + 'properties' => self::SERIALIZATION_JSON, 24 + ), 25 + self::CONFIG_AUX_PHID => true, 26 + self::CONFIG_COLUMN_SCHEMA => array( 27 + 'contactNumber' => 'text255', 28 + 'status' => 'text32', 29 + 'uniqueKey' => 'bytes12?', 30 + ), 31 + self::CONFIG_KEY_SCHEMA => array( 32 + 'key_object' => array( 33 + 'columns' => array('objectPHID'), 34 + ), 35 + 'key_unique' => array( 36 + 'columns' => array('uniqueKey'), 37 + 'unique' => true, 38 + ), 39 + ), 40 + ) + parent::getConfiguration(); 41 + } 42 + 43 + public static function initializeNewContactNumber($object) { 44 + return id(new self()) 45 + ->setStatus(self::STATUS_ACTIVE) 46 + ->setObjectPHID($object->getPHID()); 47 + } 48 + 49 + public function getPHIDType() { 50 + return PhabricatorAuthContactNumberPHIDType::TYPECONST; 51 + } 52 + 53 + public function getURI() { 54 + return urisprintf('/auth/contact/%s/', $this->getID()); 55 + } 56 + 57 + public function getObjectName() { 58 + return pht('Contact Number %d', $this->getID()); 59 + } 60 + 61 + public function getDisplayName() { 62 + return $this->getContactNumber(); 63 + } 64 + 65 + public function isDisabled() { 66 + return ($this->getStatus() === self::STATUS_DISABLED); 67 + } 68 + 69 + public function newIconView() { 70 + if ($this->isDisabled()) { 71 + return id(new PHUIIconView()) 72 + ->setIcon('fa-ban', 'grey') 73 + ->setTooltip(pht('Disabled')); 74 + } 75 + 76 + return id(new PHUIIconView()) 77 + ->setIcon('fa-mobile', 'green') 78 + ->setTooltip(pht('Active Phone Number')); 79 + } 80 + 81 + public function newUniqueKey() { 82 + $parts = array( 83 + // This is future-proofing for a world where we have multiple types 84 + // of contact numbers, so we might be able to avoid re-hashing 85 + // everything. 86 + 'phone', 87 + $this->getContactNumber(), 88 + ); 89 + 90 + $parts = implode("\0", $parts); 91 + 92 + return PhabricatorHash::digestForIndex($parts); 93 + } 94 + 95 + public function save() { 96 + $this->uniqueKey = $this->newUniqueKey(); 97 + return parent::save(); 98 + } 99 + 100 + 101 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 102 + 103 + 104 + public function getCapabilities() { 105 + return array( 106 + PhabricatorPolicyCapability::CAN_VIEW, 107 + PhabricatorPolicyCapability::CAN_EDIT, 108 + ); 109 + } 110 + 111 + public function getPolicy($capability) { 112 + return $this->getObjectPHID(); 113 + } 114 + 115 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 116 + return false; 117 + } 118 + 119 + 120 + /* -( PhabricatorDestructibleInterface )----------------------------------- */ 121 + 122 + 123 + public function destroyObjectPermanently( 124 + PhabricatorDestructionEngine $engine) { 125 + $this->delete(); 126 + } 127 + 128 + 129 + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ 130 + 131 + 132 + public function getApplicationTransactionEditor() { 133 + return new PhabricatorAuthContactNumberEditor(); 134 + } 135 + 136 + public function getApplicationTransactionTemplate() { 137 + return new PhabricatorAuthContactNumberTransaction(); 138 + } 139 + 140 + 141 + }
+18
src/applications/auth/storage/PhabricatorAuthContactNumberTransaction.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthContactNumberTransaction 4 + extends PhabricatorModularTransaction { 5 + 6 + public function getApplicationName() { 7 + return 'auth'; 8 + } 9 + 10 + public function getApplicationTransactionType() { 11 + return PhabricatorAuthContactNumberPHIDType::TYPECONST; 12 + } 13 + 14 + public function getBaseTransactionClass() { 15 + return 'PhabricatorAuthContactNumberTransactionType'; 16 + } 17 + 18 + }
+91
src/applications/auth/xaction/PhabricatorAuthContactNumberNumberTransaction.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthContactNumberNumberTransaction 4 + extends PhabricatorAuthContactNumberTransactionType { 5 + 6 + const TRANSACTIONTYPE = 'number'; 7 + 8 + public function generateOldValue($object) { 9 + return $object->getContactNumber(); 10 + } 11 + 12 + public function generateNewValue($object, $value) { 13 + $number = new PhabricatorPhoneNumber($value); 14 + return $number->toE164(); 15 + } 16 + 17 + public function applyInternalEffects($object, $value) { 18 + $object->setContactNumber($value); 19 + } 20 + 21 + public function getTitle() { 22 + $old = $this->getOldValue(); 23 + $new = $this->getNewValue(); 24 + 25 + return pht( 26 + '%s changed this contact number from %s to %s.', 27 + $this->renderAuthor(), 28 + $this->renderOldValue(), 29 + $this->renderNewValue()); 30 + } 31 + 32 + public function validateTransactions($object, array $xactions) { 33 + $errors = array(); 34 + 35 + $current_value = $object->getContactNumber(); 36 + if ($this->isEmptyTextTransaction($current_value, $xactions)) { 37 + $errors[] = $this->newRequiredError( 38 + pht('Contact numbers must have a contact number.')); 39 + return $errors; 40 + } 41 + 42 + $max_length = $object->getColumnMaximumByteLength('contactNumber'); 43 + foreach ($xactions as $xaction) { 44 + $new_value = $xaction->getNewValue(); 45 + $new_length = strlen($new_value); 46 + if ($new_length > $max_length) { 47 + $errors[] = $this->newInvalidError( 48 + pht( 49 + 'Contact numbers can not be longer than %s characters.', 50 + new PhutilNumber($max_length)), 51 + $xaction); 52 + continue; 53 + } 54 + 55 + try { 56 + new PhabricatorPhoneNumber($new_value); 57 + } catch (Exception $ex) { 58 + $errors[] = $this->newInvalidError( 59 + pht( 60 + 'Contact number is invalid: %s', 61 + $ex->getMessage()), 62 + $xaction); 63 + continue; 64 + } 65 + 66 + $new_value = $this->generateNewValue($object, $new_value); 67 + 68 + $unique_key = id(clone $object) 69 + ->setContactNumber($new_value) 70 + ->newUniqueKey(); 71 + 72 + $other = id(new PhabricatorAuthContactNumberQuery()) 73 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 74 + ->withUniqueKeys(array($unique_key)) 75 + ->executeOne(); 76 + 77 + if ($other) { 78 + if ($other->getID() !== $object->getID()) { 79 + $errors[] = $this->newInvalidError( 80 + pht('Contact number is already in use.'), 81 + $xaction); 82 + continue; 83 + } 84 + } 85 + 86 + } 87 + 88 + return $errors; 89 + } 90 + 91 + }
+4
src/applications/auth/xaction/PhabricatorAuthContactNumberTransactionType.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorAuthContactNumberTransactionType 4 + extends PhabricatorModularTransactionType {}
+12 -2
src/applications/metamta/message/PhabricatorPhoneNumber.php
··· 8 8 public function __construct($raw_number) { 9 9 $number = preg_replace('/[^\d]+/', '', $raw_number); 10 10 11 - if (!preg_match('/^[1-9]\d{1,14}\z/', $number)) { 11 + if (!preg_match('/^[1-9]\d{9,14}\z/', $number)) { 12 12 throw new Exception( 13 13 pht( 14 - 'Phone number ("%s") is not in a recognized format.', 14 + 'Phone number ("%s") is not in a recognized format: expected a '. 15 + 'US number like "(555) 555-5555", or an international number '. 16 + 'like "+55 5555 555555".', 15 17 $raw_number)); 18 + } 19 + 20 + // If the number didn't start with "+" and has has 10 digits, assume it is 21 + // a US number with no country code prefix, like "(555) 555-5555". 22 + if (!preg_match('/^[+]/', $raw_number)) { 23 + if (strlen($number) === 10) { 24 + $number = '1'.$number; 25 + } 16 26 } 17 27 18 28 $this->number = $number;
+37
src/applications/metamta/message/__tests__/PhabricatorPhoneNumberTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorPhoneNumberTestCase 4 + extends PhabricatorTestCase { 5 + 6 + public function testNumberNormalization() { 7 + $map = array( 8 + '+15555555555' => '+15555555555', 9 + '+1 (555) 555-5555' => '+15555555555', 10 + '(555) 555-5555' => '+15555555555', 11 + 12 + '' => false, 13 + '1-800-CALL-SAUL' => false, 14 + ); 15 + 16 + foreach ($map as $input => $expect) { 17 + $caught = null; 18 + try { 19 + $actual = id(new PhabricatorPhoneNumber($input)) 20 + ->toE164(); 21 + } catch (Exception $ex) { 22 + $caught = $ex; 23 + } 24 + 25 + $this->assertEqual( 26 + (bool)$caught, 27 + ($expect === false), 28 + pht('Exception raised by: %s', $input)); 29 + 30 + if ($expect !== false) { 31 + $this->assertEqual($expect, $actual, pht('E164 of: %s', $input)); 32 + } 33 + } 34 + 35 + } 36 + 37 + }
+1 -1
src/applications/search/view/PhabricatorSearchResultView.php
··· 126 126 } 127 127 128 128 // Go through the string one display glyph at a time. If a glyph starts 129 - // on a highlighted byte position, turn on highlighting for the nubmer 129 + // on a highlighted byte position, turn on highlighting for the number 130 130 // of matching bytes. If a query searches for "e" and the document contains 131 131 // an "e" followed by a bunch of combining marks, this will correctly 132 132 // highlight the entire glyph.
+69
src/applications/settings/panel/PhabricatorContactNumbersSettingsPanel.php
··· 1 + <?php 2 + 3 + final class PhabricatorContactNumbersSettingsPanel 4 + extends PhabricatorSettingsPanel { 5 + 6 + public function getPanelKey() { 7 + return 'contact'; 8 + } 9 + 10 + public function getPanelName() { 11 + return pht('Contact Numbers'); 12 + } 13 + 14 + public function getPanelGroupKey() { 15 + return PhabricatorSettingsAuthenticationPanelGroup::PANELGROUPKEY; 16 + } 17 + 18 + public function processRequest(AphrontRequest $request) { 19 + $user = $this->getUser(); 20 + $viewer = $request->getUser(); 21 + 22 + $numbers = id(new PhabricatorAuthContactNumberQuery()) 23 + ->setViewer($viewer) 24 + ->withObjectPHIDs(array($user->getPHID())) 25 + ->execute(); 26 + 27 + $rows = array(); 28 + foreach ($numbers as $number) { 29 + $rows[] = array( 30 + $number->newIconView(), 31 + phutil_tag( 32 + 'a', 33 + array( 34 + 'href' => $number->getURI(), 35 + ), 36 + $number->getDisplayName()), 37 + phabricator_datetime($number->getDateCreated(), $viewer), 38 + ); 39 + } 40 + 41 + $table = id(new AphrontTableView($rows)) 42 + ->setNoDataString( 43 + pht("You haven't added any contact numbers to your account.")) 44 + ->setHeaders( 45 + array( 46 + null, 47 + pht('Number'), 48 + pht('Created'), 49 + )) 50 + ->setColumnClasses( 51 + array( 52 + null, 53 + 'wide pri', 54 + 'right', 55 + )); 56 + 57 + $buttons = array(); 58 + 59 + $buttons[] = id(new PHUIButtonView()) 60 + ->setTag('a') 61 + ->setIcon('fa-plus') 62 + ->setText(pht('Add Contact Number')) 63 + ->setHref('/auth/contact/edit/') 64 + ->setColor(PHUIButtonView::GREY); 65 + 66 + return $this->newBox(pht('Contact Numbers'), $table, $buttons); 67 + } 68 + 69 + }