diff --git a/3rdparty b/3rdparty index e287243b8eeaf..27a56c5bb9d0e 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit e287243b8eeaf72443b841ace3b1788e85a5aacd +Subproject commit 27a56c5bb9d0ec514a8fb22044fd5f03a51ea2a5 diff --git a/apps/dav/lib/CardDAV/Converter.php b/apps/dav/lib/CardDAV/Converter.php index dd7ceb77c55f6..8602d3b4ed901 100644 --- a/apps/dav/lib/CardDAV/Converter.php +++ b/apps/dav/lib/CardDAV/Converter.php @@ -26,6 +26,7 @@ namespace OCA\DAV\CardDAV; use OC\Accounts\AccountManager; +use OCP\Accounts\IAccountManager; use OCP\IImage; use OCP\IUser; use Sabre\VObject\Component\VCard; @@ -62,8 +63,8 @@ public function createCardFromUser(IUser $user) { $publish = false; - if ($image !== null && isset($userData[AccountManager::PROPERTY_AVATAR])) { - $userData[AccountManager::PROPERTY_AVATAR]['value'] = true; + if ($image !== null && isset($userData[IAccountManager::PROPERTY_AVATAR])) { + $userData[IAccountManager::PROPERTY_AVATAR]['value'] = true; } foreach ($userData as $property => $value) { @@ -76,28 +77,28 @@ public function createCardFromUser(IUser $user) { if ($shareWithTrustedServers && !$emptyValue) { $publish = true; switch ($property) { - case AccountManager::PROPERTY_DISPLAYNAME: + case IAccountManager::PROPERTY_DISPLAYNAME: $vCard->add(new Text($vCard, 'FN', $value['value'])); $vCard->add(new Text($vCard, 'N', $this->splitFullName($value['value']))); break; - case AccountManager::PROPERTY_AVATAR: + case IAccountManager::PROPERTY_AVATAR: if ($image !== null) { $vCard->add('PHOTO', $image->data(), ['ENCODING' => 'b', 'TYPE' => $image->mimeType()]); } break; - case AccountManager::PROPERTY_EMAIL: + case IAccountManager::PROPERTY_EMAIL: $vCard->add(new Text($vCard, 'EMAIL', $value['value'], ['TYPE' => 'OTHER'])); break; - case AccountManager::PROPERTY_WEBSITE: + case IAccountManager::PROPERTY_WEBSITE: $vCard->add(new Text($vCard, 'URL', $value['value'])); break; - case AccountManager::PROPERTY_PHONE: + case IAccountManager::PROPERTY_PHONE: $vCard->add(new Text($vCard, 'TEL', $value['value'], ['TYPE' => 'OTHER'])); break; - case AccountManager::PROPERTY_ADDRESS: + case IAccountManager::PROPERTY_ADDRESS: $vCard->add(new Text($vCard, 'ADR', $value['value'], ['TYPE' => 'OTHER'])); break; - case AccountManager::PROPERTY_TWITTER: + case IAccountManager::PROPERTY_TWITTER: $vCard->add(new Text($vCard, 'X-SOCIALPROFILE', $value['value'], ['TYPE' => 'TWITTER'])); break; } diff --git a/apps/dav/tests/unit/CardDAV/ConverterTest.php b/apps/dav/tests/unit/CardDAV/ConverterTest.php index 8b4125c6c6844..aef5cf8ef1c42 100644 --- a/apps/dav/tests/unit/CardDAV/ConverterTest.php +++ b/apps/dav/tests/unit/CardDAV/ConverterTest.php @@ -29,13 +29,14 @@ use OC\Accounts\AccountManager; use OCA\DAV\CardDAV\Converter; +use OCP\Accounts\IAccountManager; use OCP\IImage; use OCP\IUser; use Test\TestCase; class ConverterTest extends TestCase { - /** @var AccountManager | \PHPUnit\Framework\MockObject\MockObject */ + /** @var AccountManager|\PHPUnit\Framework\MockObject\MockObject */ private $accountManager; protected function setUp(): void { @@ -49,36 +50,36 @@ public function getAccountManager(IUser $user) { ->disableOriginalConstructor()->getMock(); $accountManager->expects($this->any())->method('getUser')->willReturn( [ - AccountManager::PROPERTY_DISPLAYNAME => + IAccountManager::PROPERTY_DISPLAYNAME => [ 'value' => $user->getDisplayName(), 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, ], - AccountManager::PROPERTY_ADDRESS => + IAccountManager::PROPERTY_ADDRESS => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, ], - AccountManager::PROPERTY_WEBSITE => + IAccountManager::PROPERTY_WEBSITE => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, ], - AccountManager::PROPERTY_EMAIL => + IAccountManager::PROPERTY_EMAIL => [ 'value' => $user->getEMailAddress(), 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, ], - AccountManager::PROPERTY_AVATAR => + IAccountManager::PROPERTY_AVATAR => [ 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY ], - AccountManager::PROPERTY_PHONE => + IAccountManager::PROPERTY_PHONE => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, ], - AccountManager::PROPERTY_TWITTER => + IAccountManager::PROPERTY_TWITTER => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php index 0a474d649bc23..eb8186807c68b 100644 --- a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php +++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php @@ -31,6 +31,7 @@ use OC\Accounts\AccountManager; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\SyncService; +use OCP\Accounts\IAccountManager; use OCP\ILogger; use OCP\IUser; use OCP\IUserManager; @@ -132,36 +133,36 @@ public function testUpdateAndDeleteUser($activated, $createCalls, $updateCalls, $accountManager = $this->getMockBuilder(AccountManager::class)->disableOriginalConstructor()->getMock(); $accountManager->expects($this->any())->method('getUser') ->willReturn([ - AccountManager::PROPERTY_DISPLAYNAME => + IAccountManager::PROPERTY_DISPLAYNAME => [ 'value' => $user->getDisplayName(), 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, ], - AccountManager::PROPERTY_ADDRESS => + IAccountManager::PROPERTY_ADDRESS => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, ], - AccountManager::PROPERTY_WEBSITE => + IAccountManager::PROPERTY_WEBSITE => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, ], - AccountManager::PROPERTY_EMAIL => + IAccountManager::PROPERTY_EMAIL => [ 'value' => $user->getEMailAddress(), 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, ], - AccountManager::PROPERTY_AVATAR => + IAccountManager::PROPERTY_AVATAR => [ 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY ], - AccountManager::PROPERTY_PHONE => + IAccountManager::PROPERTY_PHONE => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, ], - AccountManager::PROPERTY_TWITTER => + IAccountManager::PROPERTY_TWITTER => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, diff --git a/apps/provisioning_api/appinfo/routes.php b/apps/provisioning_api/appinfo/routes.php index fd1579ca8437c..912dd82e85355 100644 --- a/apps/provisioning_api/appinfo/routes.php +++ b/apps/provisioning_api/appinfo/routes.php @@ -48,6 +48,7 @@ // Users ['root' => '/cloud', 'name' => 'Users#getUsers', 'url' => '/users', 'verb' => 'GET'], ['root' => '/cloud', 'name' => 'Users#getUsersDetails', 'url' => '/users/details', 'verb' => 'GET'], + ['root' => '/cloud', 'name' => 'Users#searchByPhoneNumbers', 'url' => '/users/search/by-phone', 'verb' => 'POST'], ['root' => '/cloud', 'name' => 'Users#addUser', 'url' => '/users', 'verb' => 'POST'], ['root' => '/cloud', 'name' => 'Users#getUser', 'url' => '/users/{userId}', 'verb' => 'GET'], ['root' => '/cloud', 'name' => 'Users#getCurrentUser', 'url' => '/user', 'verb' => 'GET'], diff --git a/apps/provisioning_api/lib/Controller/AUserData.php b/apps/provisioning_api/lib/Controller/AUserData.php index 131db91add912..b7b31b18b539a 100644 --- a/apps/provisioning_api/lib/Controller/AUserData.php +++ b/apps/provisioning_api/lib/Controller/AUserData.php @@ -35,6 +35,7 @@ use OC\User\Backend; use OC\User\NoUserException; use OC_Helper; +use OCP\Accounts\IAccountManager; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; @@ -135,12 +136,12 @@ protected function getUserData(string $userId): array { $data['backend'] = $targetUserObject->getBackendClassName(); $data['subadmin'] = $this->getUserSubAdminGroupsData($targetUserObject->getUID()); $data['quota'] = $this->fillStorageInfo($targetUserObject->getUID()); - $data[AccountManager::PROPERTY_EMAIL] = $targetUserObject->getEMailAddress(); - $data[AccountManager::PROPERTY_DISPLAYNAME] = $targetUserObject->getDisplayName(); - $data[AccountManager::PROPERTY_PHONE] = $userAccount[AccountManager::PROPERTY_PHONE]['value']; - $data[AccountManager::PROPERTY_ADDRESS] = $userAccount[AccountManager::PROPERTY_ADDRESS]['value']; - $data[AccountManager::PROPERTY_WEBSITE] = $userAccount[AccountManager::PROPERTY_WEBSITE]['value']; - $data[AccountManager::PROPERTY_TWITTER] = $userAccount[AccountManager::PROPERTY_TWITTER]['value']; + $data[IAccountManager::PROPERTY_EMAIL] = $targetUserObject->getEMailAddress(); + $data[IAccountManager::PROPERTY_DISPLAYNAME] = $targetUserObject->getDisplayName(); + $data[IAccountManager::PROPERTY_PHONE] = $userAccount[IAccountManager::PROPERTY_PHONE]['value']; + $data[IAccountManager::PROPERTY_ADDRESS] = $userAccount[IAccountManager::PROPERTY_ADDRESS]['value']; + $data[IAccountManager::PROPERTY_WEBSITE] = $userAccount[IAccountManager::PROPERTY_WEBSITE]['value']; + $data[IAccountManager::PROPERTY_TWITTER] = $userAccount[IAccountManager::PROPERTY_TWITTER]['value']; $data['groups'] = $gids; $data['language'] = $this->l10nFactory->getUserLanguage($targetUserObject); $data['locale'] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'locale'); diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php index fd143a0e46663..735d394796b10 100644 --- a/apps/provisioning_api/lib/Controller/UsersController.php +++ b/apps/provisioning_api/lib/Controller/UsersController.php @@ -41,12 +41,18 @@ namespace OCA\Provisioning_API\Controller; +use libphonenumber\NumberParseException; +use libphonenumber\PhoneNumber; +use libphonenumber\PhoneNumberFormat; +use libphonenumber\PhoneNumberUtil; use OC\Accounts\AccountManager; use OC\Authentication\Token\RemoteWipe; use OC\HintException; use OCA\Provisioning_API\FederatedShareProviderFactory; use OCA\Settings\Mailer\NewUserMailHelper; +use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSForbiddenException; @@ -55,6 +61,7 @@ use OCP\IGroupManager; use OCP\ILogger; use OCP\IRequest; +use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; @@ -67,6 +74,8 @@ class UsersController extends AUserData { /** @var IAppManager */ private $appManager; + /** @var IURLGenerator */ + protected $urlGenerator; /** @var ILogger */ private $logger; /** @var IFactory */ @@ -90,6 +99,7 @@ public function __construct(string $appName, IGroupManager $groupManager, IUserSession $userSession, AccountManager $accountManager, + IURLGenerator $urlGenerator, ILogger $logger, IFactory $l10nFactory, NewUserMailHelper $newUserMailHelper, @@ -107,6 +117,7 @@ public function __construct(string $appName, $l10nFactory); $this->appManager = $appManager; + $this->urlGenerator = $urlGenerator; $this->logger = $logger; $this->l10nFactory = $l10nFactory; $this->newUserMailHelper = $newUserMailHelper; @@ -201,6 +212,65 @@ public function getUsersDetails(string $search = '', int $limit = null, int $off ]); } + + /** + * @NoAdminRequired + * @NoSubAdminRequired + * + * @param string $location + * @param array $search + * @return DataResponse + */ + public function searchByPhoneNumbers(string $location, array $search): DataResponse { + $phoneUtil = PhoneNumberUtil::getInstance(); + + if ($phoneUtil->getCountryCodeForRegion($location) === 0) { + // Not a valid region code + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + $normalizedNumberToKey = []; + foreach ($search as $key => $phoneNumbers) { + foreach ($phoneNumbers as $phone) { + try { + $phoneNumber = $phoneUtil->parse($phone, $location); + if ($phoneNumber instanceof PhoneNumber && $phoneUtil->isValidNumber($phoneNumber)) { + $normalizedNumber = $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164); + $normalizedNumberToKey[$normalizedNumber] = (string) $key; + } + } catch (NumberParseException $e) { + } + } + } + + $phoneNumbers = array_keys($normalizedNumberToKey); + + if (empty($phoneNumbers)) { + return new DataResponse(); + } + + $userMatches = $this->accountManager->searchUsers(IAccountManager::PROPERTY_PHONE, $phoneNumbers); + + if (empty($userMatches)) { + return new DataResponse(); + } + + $cloudUrl = rtrim($this->urlGenerator->getAbsoluteURL('/'), '/'); + if (strpos($cloudUrl, 'http://') === 0) { + $cloudUrl = substr($cloudUrl, strlen('http://')); + } elseif (strpos($cloudUrl, 'https://') === 0) { + $cloudUrl = substr($cloudUrl, strlen('https://')); + } + + $matches = []; + foreach ($userMatches as $phone => $userId) { + // Not using the ICloudIdManager as that would run a search for each contact to find the display name in the address book + $matches[$normalizedNumberToKey[$phone]] = $userId . '@' . $cloudUrl; + } + + return new DataResponse($matches); + } + /** * @throws OCSException */ @@ -431,17 +501,17 @@ public function getEditableFields(): DataResponse { // Editing self (display, email) if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) { - $permittedFields[] = AccountManager::PROPERTY_DISPLAYNAME; - $permittedFields[] = AccountManager::PROPERTY_EMAIL; + $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME; + $permittedFields[] = IAccountManager::PROPERTY_EMAIL; } if ($this->appManager->isEnabledForUser('federatedfilesharing')) { $shareProvider = $this->federatedShareProviderFactory->get(); if ($shareProvider->isLookupServerUploadEnabled()) { - $permittedFields[] = AccountManager::PROPERTY_PHONE; - $permittedFields[] = AccountManager::PROPERTY_ADDRESS; - $permittedFields[] = AccountManager::PROPERTY_WEBSITE; - $permittedFields[] = AccountManager::PROPERTY_TWITTER; + $permittedFields[] = IAccountManager::PROPERTY_PHONE; + $permittedFields[] = IAccountManager::PROPERTY_ADDRESS; + $permittedFields[] = IAccountManager::PROPERTY_WEBSITE; + $permittedFields[] = IAccountManager::PROPERTY_TWITTER; } } @@ -474,8 +544,8 @@ public function editUser(string $userId, string $key, string $value): DataRespon // Editing self (display, email) if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) { $permittedFields[] = 'display'; - $permittedFields[] = AccountManager::PROPERTY_DISPLAYNAME; - $permittedFields[] = AccountManager::PROPERTY_EMAIL; + $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME; + $permittedFields[] = IAccountManager::PROPERTY_EMAIL; } $permittedFields[] = 'password'; @@ -492,10 +562,10 @@ public function editUser(string $userId, string $key, string $value): DataRespon if ($this->appManager->isEnabledForUser('federatedfilesharing')) { $shareProvider = $this->federatedShareProviderFactory->get(); if ($shareProvider->isLookupServerUploadEnabled()) { - $permittedFields[] = AccountManager::PROPERTY_PHONE; - $permittedFields[] = AccountManager::PROPERTY_ADDRESS; - $permittedFields[] = AccountManager::PROPERTY_WEBSITE; - $permittedFields[] = AccountManager::PROPERTY_TWITTER; + $permittedFields[] = IAccountManager::PROPERTY_PHONE; + $permittedFields[] = IAccountManager::PROPERTY_ADDRESS; + $permittedFields[] = IAccountManager::PROPERTY_WEBSITE; + $permittedFields[] = IAccountManager::PROPERTY_TWITTER; } } @@ -510,15 +580,15 @@ public function editUser(string $userId, string $key, string $value): DataRespon || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) { // They have permissions over the user $permittedFields[] = 'display'; - $permittedFields[] = AccountManager::PROPERTY_DISPLAYNAME; - $permittedFields[] = AccountManager::PROPERTY_EMAIL; + $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME; + $permittedFields[] = IAccountManager::PROPERTY_EMAIL; $permittedFields[] = 'password'; $permittedFields[] = 'language'; $permittedFields[] = 'locale'; - $permittedFields[] = AccountManager::PROPERTY_PHONE; - $permittedFields[] = AccountManager::PROPERTY_ADDRESS; - $permittedFields[] = AccountManager::PROPERTY_WEBSITE; - $permittedFields[] = AccountManager::PROPERTY_TWITTER; + $permittedFields[] = IAccountManager::PROPERTY_PHONE; + $permittedFields[] = IAccountManager::PROPERTY_ADDRESS; + $permittedFields[] = IAccountManager::PROPERTY_WEBSITE; + $permittedFields[] = IAccountManager::PROPERTY_TWITTER; $permittedFields[] = 'quota'; } else { // No rights @@ -532,7 +602,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon // Process the edit switch ($key) { case 'display': - case AccountManager::PROPERTY_DISPLAYNAME: + case IAccountManager::PROPERTY_DISPLAYNAME: $targetUser->setDisplayName($value); break; case 'quota': @@ -577,21 +647,25 @@ public function editUser(string $userId, string $key, string $value): DataRespon } $this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value); break; - case AccountManager::PROPERTY_EMAIL: + case IAccountManager::PROPERTY_EMAIL: if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') { $targetUser->setEMailAddress($value); } else { throw new OCSException('', 102); } break; - case AccountManager::PROPERTY_PHONE: - case AccountManager::PROPERTY_ADDRESS: - case AccountManager::PROPERTY_WEBSITE: - case AccountManager::PROPERTY_TWITTER: + case IAccountManager::PROPERTY_PHONE: + case IAccountManager::PROPERTY_ADDRESS: + case IAccountManager::PROPERTY_WEBSITE: + case IAccountManager::PROPERTY_TWITTER: $userAccount = $this->accountManager->getUser($targetUser); if ($userAccount[$key]['value'] !== $value) { $userAccount[$key]['value'] = $value; - $this->accountManager->updateUser($targetUser, $userAccount); + try { + $this->accountManager->updateUser($targetUser, $userAccount, true); + } catch (\InvalidArgumentException $e) { + throw new OCSException('Invalid ' . $e->getMessage(), 102); + } } break; default: diff --git a/apps/provisioning_api/tests/Controller/GroupsControllerTest.php b/apps/provisioning_api/tests/Controller/GroupsControllerTest.php index 1973e6f8b6afc..bb8ec85439095 100644 --- a/apps/provisioning_api/tests/Controller/GroupsControllerTest.php +++ b/apps/provisioning_api/tests/Controller/GroupsControllerTest.php @@ -35,6 +35,7 @@ use OC\SubAdmin; use OC\User\NoUserException; use OCA\Provisioning_API\Controller\GroupsController; +use OCP\Accounts\IAccountManager; use OCP\IConfig; use OCP\ILogger; use OCP\IRequest; @@ -185,10 +186,10 @@ private function useAccountManager() { ->method('getUser') ->willReturnCallback(function (IUser $user) { return [ - AccountManager::PROPERTY_PHONE => ['value' => '0800-call-' . $user->getUID()], - AccountManager::PROPERTY_ADDRESS => ['value' => 'Holzweg 99, 0601 Herrera, Panama'], - AccountManager::PROPERTY_WEBSITE => ['value' => 'https://' . $user->getUid() . '.pa'], - AccountManager::PROPERTY_TWITTER => ['value' => '@' . $user->getUID()], + IAccountManager::PROPERTY_PHONE => ['value' => '0800-call-' . $user->getUID()], + IAccountManager::PROPERTY_ADDRESS => ['value' => 'Holzweg 99, 0601 Herrera, Panama'], + IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://' . $user->getUid() . '.pa'], + IAccountManager::PROPERTY_TWITTER => ['value' => '@' . $user->getUID()], ]; }); } diff --git a/apps/provisioning_api/tests/Controller/UsersControllerTest.php b/apps/provisioning_api/tests/Controller/UsersControllerTest.php index d508670d4f6ed..b6f28cc4a04c9 100644 --- a/apps/provisioning_api/tests/Controller/UsersControllerTest.php +++ b/apps/provisioning_api/tests/Controller/UsersControllerTest.php @@ -48,6 +48,7 @@ use OCA\Provisioning_API\Controller\UsersController; use OCA\Provisioning_API\FederatedShareProviderFactory; use OCA\Settings\Mailer\NewUserMailHelper; +use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\AppFramework\Http\DataResponse; use OCP\EventDispatcher\IEventDispatcher; @@ -56,6 +57,7 @@ use OCP\IL10N; use OCP\ILogger; use OCP\IRequest; +use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; @@ -85,6 +87,8 @@ class UsersControllerTest extends TestCase { protected $api; /** @var AccountManager|MockObject */ protected $accountManager; + /** @var IURLGenerator|MockObject */ + protected $urlGenerator; /** @var IRequest|MockObject */ protected $request; /** @var IFactory|MockObject */ @@ -111,6 +115,7 @@ protected function setUp(): void { $this->logger = $this->createMock(ILogger::class); $this->request = $this->createMock(IRequest::class); $this->accountManager = $this->createMock(AccountManager::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->l10nFactory = $this->createMock(IFactory::class); $this->newUserMailHelper = $this->createMock(NewUserMailHelper::class); $this->federatedShareProviderFactory = $this->createMock(FederatedShareProviderFactory::class); @@ -128,6 +133,7 @@ protected function setUp(): void { $this->groupManager, $this->userSession, $this->accountManager, + $this->urlGenerator, $this->logger, $this->l10nFactory, $this->newUserMailHelper, @@ -381,7 +387,7 @@ public function testAddUserSuccessful() { } public function testAddUserSuccessfulWithDisplayName() { - $api = $this->getMockBuilder('OCA\Provisioning_API\Controller\UsersController') + $api = $this->getMockBuilder(UsersController::class) ->setConstructorArgs([ 'provisioning_api', $this->request, @@ -391,6 +397,7 @@ public function testAddUserSuccessfulWithDisplayName() { $this->groupManager, $this->userSession, $this->accountManager, + $this->urlGenerator, $this->logger, $this->l10nFactory, $this->newUserMailHelper, @@ -993,10 +1000,10 @@ public function testGetUserDataAsAdmin() { ->with($targetUser) ->willReturn( [ - AccountManager::PROPERTY_ADDRESS => ['value' => 'address'], - AccountManager::PROPERTY_PHONE => ['value' => 'phone'], - AccountManager::PROPERTY_TWITTER => ['value' => 'twitter'], - AccountManager::PROPERTY_WEBSITE => ['value' => 'website'], + IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'], + IAccountManager::PROPERTY_PHONE => ['value' => 'phone'], + IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'], + IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'], ] ); $this->config @@ -1162,10 +1169,10 @@ public function testGetUserDataAsSubAdminAndUserIsAccessible() { ->with($targetUser) ->willReturn( [ - AccountManager::PROPERTY_ADDRESS => ['value' => 'address'], - AccountManager::PROPERTY_PHONE => ['value' => 'phone'], - AccountManager::PROPERTY_TWITTER => ['value' => 'twitter'], - AccountManager::PROPERTY_WEBSITE => ['value' => 'website'], + IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'], + IAccountManager::PROPERTY_PHONE => ['value' => 'phone'], + IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'], + IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'], ] ); @@ -1333,10 +1340,10 @@ public function testGetUserDataAsSubAdminSelfLookup() { ->with($targetUser) ->willReturn( [ - AccountManager::PROPERTY_ADDRESS => ['value' => 'address'], - AccountManager::PROPERTY_PHONE => ['value' => 'phone'], - AccountManager::PROPERTY_TWITTER => ['value' => 'twitter'], - AccountManager::PROPERTY_WEBSITE => ['value' => 'website'], + IAccountManager::PROPERTY_ADDRESS => ['value' => 'address'], + IAccountManager::PROPERTY_PHONE => ['value' => 'phone'], + IAccountManager::PROPERTY_TWITTER => ['value' => 'twitter'], + IAccountManager::PROPERTY_WEBSITE => ['value' => 'website'], ] ); @@ -1370,6 +1377,47 @@ public function testGetUserDataAsSubAdminSelfLookup() { $this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID'])); } + public function dataSearchByPhoneNumbers(): array { + return [ + 'Invalid country' => ['Not a country code', ['12345' => ['NaN']], 400, null, null, []], + 'No number to search' => ['DE', ['12345' => ['NaN']], 200, null, null, []], + 'Valid number but no match' => ['DE', ['12345' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], [], []], + 'Invalid number' => ['FR', ['12345' => ['0711 / 25 24 28-90']], 200, null, null, []], + 'Invalid and valid number' => ['DE', ['12345' => ['NaN', '0711 / 25 24 28-90']], 200, ['+4971125242890'], [], []], + 'Valid and invalid number' => ['DE', ['12345' => ['0711 / 25 24 28-90', 'NaN']], 200, ['+4971125242890'], [], []], + 'Valid number and a match' => ['DE', ['12345' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], ['+4971125242890' => 'admin'], ['12345' => 'admin@localhost']], + 'Same number twice, later hits' => ['DE', ['12345' => ['0711 / 25 24 28-90'], '23456' => ['0711 / 25 24 28-90']], 200, ['+4971125242890'], ['+4971125242890' => 'admin'], ['23456' => 'admin@localhost']], + ]; + } + + /** + * @dataProvider dataSearchByPhoneNumbers + * @param string $location + * @param array $search + * @param int $status + * @param array $expected + */ + public function testSearchByPhoneNumbers(string $location, array $search, int $status, ?array $searchUsers, ?array $userMatches, array $expected) { + if ($searchUsers === null) { + $this->accountManager->expects($this->never()) + ->method('searchUsers'); + } else { + $this->accountManager->expects($this->once()) + ->method('searchUsers') + ->with(IAccountManager::PROPERTY_PHONE, $searchUsers) + ->willReturn($userMatches); + } + + $this->urlGenerator->method('getAbsoluteURL') + ->with('/') + ->willReturn('https://localhost/'); + + $response = $this->api->searchByPhoneNumbers($location, $search); + + self::assertEquals($status, $response->getStatus()); + self::assertEquals($expected, $response->getData()); + } + public function testEditUserRegularUserSelfEditChangeDisplayName() { $loggedInUser = $this->getMockBuilder(IUser::class) ->disableOriginalConstructor() @@ -3162,7 +3210,7 @@ public function testGetCurrentUserLoggedIn() { ->willReturn($user); /** @var UsersController | MockObject $api */ - $api = $this->getMockBuilder('OCA\Provisioning_API\Controller\UsersController') + $api = $this->getMockBuilder(UsersController::class) ->setConstructorArgs([ 'provisioning_api', $this->request, @@ -3172,6 +3220,7 @@ public function testGetCurrentUserLoggedIn() { $this->groupManager, $this->userSession, $this->accountManager, + $this->urlGenerator, $this->logger, $this->l10nFactory, $this->newUserMailHelper, @@ -3227,7 +3276,7 @@ public function testGetCurrentUserNotLoggedIn() { public function testGetUser() { /** @var UsersController | MockObject $api */ - $api = $this->getMockBuilder('OCA\Provisioning_API\Controller\UsersController') + $api = $this->getMockBuilder(UsersController::class) ->setConstructorArgs([ 'provisioning_api', $this->request, @@ -3237,6 +3286,7 @@ public function testGetUser() { $this->groupManager, $this->userSession, $this->accountManager, + $this->urlGenerator, $this->logger, $this->l10nFactory, $this->newUserMailHelper, @@ -3566,22 +3616,22 @@ public function dataGetEditableFields() { return [ [false, false, []], [false, true, [ - AccountManager::PROPERTY_PHONE, - AccountManager::PROPERTY_ADDRESS, - AccountManager::PROPERTY_WEBSITE, - AccountManager::PROPERTY_TWITTER, + IAccountManager::PROPERTY_PHONE, + IAccountManager::PROPERTY_ADDRESS, + IAccountManager::PROPERTY_WEBSITE, + IAccountManager::PROPERTY_TWITTER, ]], [ true, false, [ - AccountManager::PROPERTY_DISPLAYNAME, - AccountManager::PROPERTY_EMAIL, + IAccountManager::PROPERTY_DISPLAYNAME, + IAccountManager::PROPERTY_EMAIL, ]], [ true, true ,[ - AccountManager::PROPERTY_DISPLAYNAME, - AccountManager::PROPERTY_EMAIL, - AccountManager::PROPERTY_PHONE, - AccountManager::PROPERTY_ADDRESS, - AccountManager::PROPERTY_WEBSITE, - AccountManager::PROPERTY_TWITTER, + IAccountManager::PROPERTY_DISPLAYNAME, + IAccountManager::PROPERTY_EMAIL, + IAccountManager::PROPERTY_PHONE, + IAccountManager::PROPERTY_ADDRESS, + IAccountManager::PROPERTY_WEBSITE, + IAccountManager::PROPERTY_TWITTER, ]] ]; } diff --git a/apps/settings/js/usersettings.js b/apps/settings/js/usersettings.js index fcfe556b1d9a6..a02aae6fb6ac4 100644 --- a/apps/settings/js/usersettings.js +++ b/apps/settings/js/usersettings.js @@ -24,6 +24,11 @@ if (_.isUndefined(data)) { return null; } + + if (data.status && data.status === 'error') { + OC.Notification.show(data.data.message, { type: 'error' }); + } + if (_.isUndefined(data.data)) { return null; } @@ -47,4 +52,4 @@ OC.Settings = OC.Settings || {}; OC.Settings.UserSettings = UserSettings; -})(); \ No newline at end of file +})(); diff --git a/apps/settings/lib/BackgroundJobs/VerifyUserData.php b/apps/settings/lib/BackgroundJobs/VerifyUserData.php index 0faa9b56e8294..d1b6d835fa4b7 100644 --- a/apps/settings/lib/BackgroundJobs/VerifyUserData.php +++ b/apps/settings/lib/BackgroundJobs/VerifyUserData.php @@ -30,6 +30,7 @@ namespace OCA\Settings\BackgroundJobs; use OC\Accounts\AccountManager; +use OCP\Accounts\IAccountManager; use OCP\AppFramework\Http; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\Job; @@ -114,11 +115,11 @@ protected function run($argument) { $try = (int)$argument['try'] + 1; switch ($argument['type']) { - case AccountManager::PROPERTY_WEBSITE: + case IAccountManager::PROPERTY_WEBSITE: $result = $this->verifyWebsite($argument); break; - case AccountManager::PROPERTY_TWITTER: - case AccountManager::PROPERTY_EMAIL: + case IAccountManager::PROPERTY_TWITTER: + case IAccountManager::PROPERTY_EMAIL: $result = $this->verifyViaLookupServer($argument, $argument['type']); break; default: @@ -164,9 +165,9 @@ protected function verifyWebsite(array $argument) { $userData = $this->accountManager->getUser($user); if ($publishedCodeSanitized === $argument['verificationCode']) { - $userData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::VERIFIED; + $userData[IAccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::VERIFIED; } else { - $userData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::NOT_VERIFIED; + $userData[IAccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::NOT_VERIFIED; } $this->accountManager->updateUser($user, $userData); diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index 7929e9c396208..1ebeb41adfbfc 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -752,6 +752,7 @@ public function check() { PhpOutputBuffering::class => ['pass' => $phpOutputBuffering->run(), 'description' => $phpOutputBuffering->description(), 'severity' => $phpOutputBuffering->severity()], LegacySSEKeyFormat::class => ['pass' => $legacySSEKeyFormat->run(), 'description' => $legacySSEKeyFormat->description(), 'severity' => $legacySSEKeyFormat->severity(), 'linkToDocumentation' => $legacySSEKeyFormat->linkToDocumentation()], CheckUserCertificates::class => ['pass' => $checkUserCertificates->run(), 'description' => $checkUserCertificates->description(), 'severity' => $checkUserCertificates->severity(), 'elements' => $checkUserCertificates->elements()], + 'isDefaultPhoneRegionSet' => $this->config->getSystemValueString('default_phone_region', '') !== '', ] ); } diff --git a/apps/settings/lib/Controller/UsersController.php b/apps/settings/lib/Controller/UsersController.php index cad21c5f3b34f..dba5ec69b2b8f 100644 --- a/apps/settings/lib/Controller/UsersController.php +++ b/apps/settings/lib/Controller/UsersController.php @@ -1,4 +1,6 @@ usersList(); } @@ -152,7 +154,7 @@ public function usersListByGroup() { * * @return TemplateResponse */ - public function usersList() { + public function usersList(): TemplateResponse { $user = $this->userSession->getUser(); $uid = $user->getUID(); @@ -309,7 +311,7 @@ protected function parseQuotaPreset(string $quotaPreset): array { * * @return bool */ - protected function canAdminChangeUserPasswords() { + protected function canAdminChangeUserPasswords(): bool { $isEncryptionEnabled = $this->encryptionManager->isEnabled(); try { $noUserSpecificEncryptionKeys = !$this->encryptionManager->getEncryptionModule()->needDetailedAccessList(); @@ -344,19 +346,19 @@ protected function canAdminChangeUserPasswords() { * @param string $twitterScope * @return DataResponse */ - public function setUserSettings($avatarScope, - $displayname, - $displaynameScope, - $phone, - $phoneScope, - $email, - $emailScope, - $website, - $websiteScope, - $address, - $addressScope, - $twitter, - $twitterScope + public function setUserSettings(string $avatarScope, + string $displayname, + string $displaynameScope, + string $phone, + string $phoneScope, + string $email, + string $emailScope, + string $website, + string $websiteScope, + string $address, + string $addressScope, + string $twitter, + string $twitterScope ) { $email = strtolower($email); if (!empty($email) && !$this->mailer->validateMailAddress($email)) { @@ -372,36 +374,40 @@ public function setUserSettings($avatarScope, } $user = $this->userSession->getUser(); $data = $this->accountManager->getUser($user); - $data[AccountManager::PROPERTY_AVATAR] = ['scope' => $avatarScope]; + $data[IAccountManager::PROPERTY_AVATAR] = ['scope' => $avatarScope]; if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) { - $data[AccountManager::PROPERTY_DISPLAYNAME] = ['value' => $displayname, 'scope' => $displaynameScope]; - $data[AccountManager::PROPERTY_EMAIL] = ['value' => $email, 'scope' => $emailScope]; + $data[IAccountManager::PROPERTY_DISPLAYNAME] = ['value' => $displayname, 'scope' => $displaynameScope]; + $data[IAccountManager::PROPERTY_EMAIL] = ['value' => $email, 'scope' => $emailScope]; } if ($this->appManager->isEnabledForUser('federatedfilesharing')) { $shareProvider = \OC::$server->query(FederatedShareProvider::class); if ($shareProvider->isLookupServerUploadEnabled()) { - $data[AccountManager::PROPERTY_WEBSITE] = ['value' => $website, 'scope' => $websiteScope]; - $data[AccountManager::PROPERTY_ADDRESS] = ['value' => $address, 'scope' => $addressScope]; - $data[AccountManager::PROPERTY_PHONE] = ['value' => $phone, 'scope' => $phoneScope]; - $data[AccountManager::PROPERTY_TWITTER] = ['value' => $twitter, 'scope' => $twitterScope]; + $data[IAccountManager::PROPERTY_WEBSITE] = ['value' => $website, 'scope' => $websiteScope]; + $data[IAccountManager::PROPERTY_ADDRESS] = ['value' => $address, 'scope' => $addressScope]; + $data[IAccountManager::PROPERTY_PHONE] = ['value' => $phone, 'scope' => $phoneScope]; + $data[IAccountManager::PROPERTY_TWITTER] = ['value' => $twitter, 'scope' => $twitterScope]; } } try { - $this->saveUserSettings($user, $data); + $data = $this->saveUserSettings($user, $data); return new DataResponse( [ 'status' => 'success', 'data' => [ 'userId' => $user->getUID(), - 'avatarScope' => $data[AccountManager::PROPERTY_AVATAR]['scope'], - 'displayname' => $data[AccountManager::PROPERTY_DISPLAYNAME]['value'], - 'displaynameScope' => $data[AccountManager::PROPERTY_DISPLAYNAME]['scope'], - 'email' => $data[AccountManager::PROPERTY_EMAIL]['value'], - 'emailScope' => $data[AccountManager::PROPERTY_EMAIL]['scope'], - 'website' => $data[AccountManager::PROPERTY_WEBSITE]['value'], - 'websiteScope' => $data[AccountManager::PROPERTY_WEBSITE]['scope'], - 'address' => $data[AccountManager::PROPERTY_ADDRESS]['value'], - 'addressScope' => $data[AccountManager::PROPERTY_ADDRESS]['scope'], + 'avatarScope' => $data[IAccountManager::PROPERTY_AVATAR]['scope'], + 'displayname' => $data[IAccountManager::PROPERTY_DISPLAYNAME]['value'], + 'displaynameScope' => $data[IAccountManager::PROPERTY_DISPLAYNAME]['scope'], + 'phone' => $data[IAccountManager::PROPERTY_PHONE]['value'], + 'phoneScope' => $data[IAccountManager::PROPERTY_PHONE]['scope'], + 'email' => $data[IAccountManager::PROPERTY_EMAIL]['value'], + 'emailScope' => $data[IAccountManager::PROPERTY_EMAIL]['scope'], + 'website' => $data[IAccountManager::PROPERTY_WEBSITE]['value'], + 'websiteScope' => $data[IAccountManager::PROPERTY_WEBSITE]['scope'], + 'address' => $data[IAccountManager::PROPERTY_ADDRESS]['value'], + 'addressScope' => $data[IAccountManager::PROPERTY_ADDRESS]['scope'], + 'twitter' => $data[IAccountManager::PROPERTY_TWITTER]['value'], + 'twitterScope' => $data[IAccountManager::PROPERTY_TWITTER]['scope'], 'message' => $this->l10n->t('Settings saved') ] ], @@ -414,6 +420,13 @@ public function setUserSettings($avatarScope, 'message' => $e->getMessage() ], ]); + } catch (\InvalidArgumentException $e) { + return new DataResponse([ + 'status' => 'error', + 'data' => [ + 'message' => $e->getMessage() + ], + ]); } } /** @@ -421,34 +434,45 @@ public function setUserSettings($avatarScope, * * @param IUser $user * @param array $data + * @return array * @throws ForbiddenException + * @throws \InvalidArgumentException */ - protected function saveUserSettings(IUser $user, array $data) { + protected function saveUserSettings(IUser $user, array $data): array { // keep the user back-end up-to-date with the latest display name and email // address $oldDisplayName = $user->getDisplayName(); $oldDisplayName = is_null($oldDisplayName) ? '' : $oldDisplayName; - if (isset($data[AccountManager::PROPERTY_DISPLAYNAME]['value']) - && $oldDisplayName !== $data[AccountManager::PROPERTY_DISPLAYNAME]['value'] + if (isset($data[IAccountManager::PROPERTY_DISPLAYNAME]['value']) + && $oldDisplayName !== $data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] ) { - $result = $user->setDisplayName($data[AccountManager::PROPERTY_DISPLAYNAME]['value']); + $result = $user->setDisplayName($data[IAccountManager::PROPERTY_DISPLAYNAME]['value']); if ($result === false) { throw new ForbiddenException($this->l10n->t('Unable to change full name')); } } + $oldEmailAddress = $user->getEMailAddress(); $oldEmailAddress = is_null($oldEmailAddress) ? '' : strtolower($oldEmailAddress); - if (isset($data[AccountManager::PROPERTY_EMAIL]['value']) - && $oldEmailAddress !== $data[AccountManager::PROPERTY_EMAIL]['value'] + if (isset($data[IAccountManager::PROPERTY_EMAIL]['value']) + && $oldEmailAddress !== $data[IAccountManager::PROPERTY_EMAIL]['value'] ) { // this is the only permission a backend provides and is also used // for the permission of setting a email address if (!$user->canChangeDisplayName()) { throw new ForbiddenException($this->l10n->t('Unable to change email address')); } - $user->setEMailAddress($data[AccountManager::PROPERTY_EMAIL]['value']); + $user->setEMailAddress($data[IAccountManager::PROPERTY_EMAIL]['value']); + } + + try { + return $this->accountManager->updateUser($user, $data, true); + } catch (\InvalidArgumentException $e) { + if ($e->getMessage() === IAccountManager::PROPERTY_PHONE) { + throw new \InvalidArgumentException($this->l10n->t('Unable to set invalid phone number')); + } + throw new \InvalidArgumentException($this->l10n->t('Some account data was invalid')); } - $this->accountManager->updateUser($user, $data); } /** @@ -479,26 +503,25 @@ public function getVerificationCode(string $account, bool $onlyVerificationCode) switch ($account) { case 'verify-twitter': - $accountData[AccountManager::PROPERTY_TWITTER]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS; + $accountData[IAccountManager::PROPERTY_TWITTER]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS; $msg = $this->l10n->t('In order to verify your Twitter account, post the following tweet on Twitter (please make sure to post it without any line breaks):'); $code = $codeMd5; - $type = AccountManager::PROPERTY_TWITTER; - $data = $accountData[AccountManager::PROPERTY_TWITTER]['value']; - $accountData[AccountManager::PROPERTY_TWITTER]['signature'] = $signature; + $type = IAccountManager::PROPERTY_TWITTER; + $accountData[IAccountManager::PROPERTY_TWITTER]['signature'] = $signature; break; case 'verify-website': - $accountData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS; + $accountData[IAccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS; $msg = $this->l10n->t('In order to verify your Website, store the following content in your web-root at \'.well-known/CloudIdVerificationCode.txt\' (please make sure that the complete text is in one line):'); - $type = AccountManager::PROPERTY_WEBSITE; - $data = $accountData[AccountManager::PROPERTY_WEBSITE]['value']; - $accountData[AccountManager::PROPERTY_WEBSITE]['signature'] = $signature; + $type = IAccountManager::PROPERTY_WEBSITE; + $accountData[IAccountManager::PROPERTY_WEBSITE]['signature'] = $signature; break; default: return new DataResponse([], Http::STATUS_BAD_REQUEST); } if ($onlyVerificationCode === false) { - $this->accountManager->updateUser($user, $accountData); + $accountData = $this->accountManager->updateUser($user, $accountData); + $data = $accountData[$type]['value']; $this->jobList->add(VerifyUserData::class, [ diff --git a/apps/settings/lib/Settings/Personal/PersonalInfo.php b/apps/settings/lib/Settings/Personal/PersonalInfo.php index 06ea440afabe1..d9f9c2b3a7d04 100644 --- a/apps/settings/lib/Settings/Personal/PersonalInfo.php +++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php @@ -1,4 +1,6 @@ * @@ -33,6 +35,7 @@ use OC\Accounts\AccountManager; use OCA\FederatedFileSharing\FederatedShareProvider; +use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\Files\FileInfo; @@ -62,14 +65,6 @@ class PersonalInfo implements ISettings { /** @var IL10N */ private $l; - /** - * @param IConfig $config - * @param IUserManager $userManager - * @param IGroupManager $groupManager - * @param AccountManager $accountManager - * @param IFactory $l10nFactory - * @param IL10N $l - */ public function __construct( IConfig $config, IUserManager $userManager, @@ -88,11 +83,7 @@ public function __construct( $this->l = $l; } - /** - * @return TemplateResponse returns the instance with all parameters set, ready to be rendered - * @since 9.1 - */ - public function getForm() { + public function getForm(): TemplateResponse { $federatedFileSharingEnabled = $this->appManager->isEnabledForUser('federatedfilesharing'); $lookupServerUploadEnabled = false; if ($federatedFileSharingEnabled) { @@ -126,23 +117,23 @@ public function getForm() { 'quota' => $storageInfo['quota'], 'avatarChangeSupported' => $user->canChangeAvatar(), 'lookupServerUploadEnabled' => $lookupServerUploadEnabled, - 'avatarScope' => $userData[AccountManager::PROPERTY_AVATAR]['scope'], + 'avatarScope' => $userData[IAccountManager::PROPERTY_AVATAR]['scope'], 'displayNameChangeSupported' => $user->canChangeDisplayName(), - 'displayName' => $userData[AccountManager::PROPERTY_DISPLAYNAME]['value'], - 'displayNameScope' => $userData[AccountManager::PROPERTY_DISPLAYNAME]['scope'], - 'email' => $userData[AccountManager::PROPERTY_EMAIL]['value'], - 'emailScope' => $userData[AccountManager::PROPERTY_EMAIL]['scope'], - 'emailVerification' => $userData[AccountManager::PROPERTY_EMAIL]['verified'], - 'phone' => $userData[AccountManager::PROPERTY_PHONE]['value'], - 'phoneScope' => $userData[AccountManager::PROPERTY_PHONE]['scope'], - 'address' => $userData[AccountManager::PROPERTY_ADDRESS]['value'], - 'addressScope' => $userData[AccountManager::PROPERTY_ADDRESS]['scope'], - 'website' => $userData[AccountManager::PROPERTY_WEBSITE]['value'], - 'websiteScope' => $userData[AccountManager::PROPERTY_WEBSITE]['scope'], - 'websiteVerification' => $userData[AccountManager::PROPERTY_WEBSITE]['verified'], - 'twitter' => $userData[AccountManager::PROPERTY_TWITTER]['value'], - 'twitterScope' => $userData[AccountManager::PROPERTY_TWITTER]['scope'], - 'twitterVerification' => $userData[AccountManager::PROPERTY_TWITTER]['verified'], + 'displayName' => $userData[IAccountManager::PROPERTY_DISPLAYNAME]['value'], + 'displayNameScope' => $userData[IAccountManager::PROPERTY_DISPLAYNAME]['scope'], + 'email' => $userData[IAccountManager::PROPERTY_EMAIL]['value'], + 'emailScope' => $userData[IAccountManager::PROPERTY_EMAIL]['scope'], + 'emailVerification' => $userData[IAccountManager::PROPERTY_EMAIL]['verified'], + 'phone' => $userData[IAccountManager::PROPERTY_PHONE]['value'], + 'phoneScope' => $userData[IAccountManager::PROPERTY_PHONE]['scope'], + 'address' => $userData[IAccountManager::PROPERTY_ADDRESS]['value'], + 'addressScope' => $userData[IAccountManager::PROPERTY_ADDRESS]['scope'], + 'website' => $userData[IAccountManager::PROPERTY_WEBSITE]['value'], + 'websiteScope' => $userData[IAccountManager::PROPERTY_WEBSITE]['scope'], + 'websiteVerification' => $userData[IAccountManager::PROPERTY_WEBSITE]['verified'], + 'twitter' => $userData[IAccountManager::PROPERTY_TWITTER]['value'], + 'twitterScope' => $userData[IAccountManager::PROPERTY_TWITTER]['scope'], + 'twitterVerification' => $userData[IAccountManager::PROPERTY_TWITTER]['verified'], 'groups' => $this->getGroups($user), ] + $messageParameters + $languageParameters + $localeParameters; @@ -154,7 +145,7 @@ public function getForm() { * @return string the section ID, e.g. 'sharing' * @since 9.1 */ - public function getSection() { + public function getSection(): string { return 'personal-info'; } @@ -166,7 +157,7 @@ public function getSection() { * E.g.: 70 * @since 9.1 */ - public function getPriority() { + public function getPriority(): int { return 10; } @@ -176,9 +167,9 @@ public function getPriority() { * @param IUser $user * @return array */ - private function getGroups(IUser $user) { + private function getGroups(IUser $user): array { $groups = array_map( - function (IGroup $group) { + static function (IGroup $group) { return $group->getDisplayName(); }, $this->groupManager->getUserGroups($user) @@ -195,7 +186,7 @@ function (IGroup $group) { * @param IUser $user * @return array */ - private function getLanguages(IUser $user) { + private function getLanguages(IUser $user): array { $forceLanguage = $this->config->getSystemValue('force_language', false); if ($forceLanguage !== false) { return []; @@ -228,7 +219,7 @@ private function getLanguages(IUser $user) { ); } - private function getLocales(IUser $user) { + private function getLocales(IUser $user): array { $forceLanguage = $this->config->getSystemValue('force_locale', false); if ($forceLanguage !== false) { return []; @@ -273,8 +264,8 @@ private function getLocales(IUser $user) { * @param array $userData * @return array */ - private function getMessageParameters(array $userData) { - $needVerifyMessage = [AccountManager::PROPERTY_EMAIL, AccountManager::PROPERTY_WEBSITE, AccountManager::PROPERTY_TWITTER]; + private function getMessageParameters(array $userData): array { + $needVerifyMessage = [IAccountManager::PROPERTY_EMAIL, IAccountManager::PROPERTY_WEBSITE, IAccountManager::PROPERTY_TWITTER]; $messageParameters = []; foreach ($needVerifyMessage as $property) { switch ($userData[$property]['verified']) { diff --git a/apps/settings/templates/settings/personal/personal.info.php b/apps/settings/templates/settings/personal/personal.info.php index b78c162c6c9e0..84198b3c0c43e 100644 --- a/apps/settings/templates/settings/personal/personal.info.php +++ b/apps/settings/templates/settings/personal/personal.info.php @@ -67,6 +67,7 @@ + @@ -161,7 +162,7 @@ } ?> placeholder="t('Your email address')); ?>" autocomplete="on" autocapitalize="none" autocorrect="off" /> - + t('Your phone number')); ?>" autocomplete="on" autocapitalize="none" autocorrect="off" /> + @@ -220,6 +222,7 @@ value="" autocomplete="on" autocapitalize="none" autocorrect="off" /> + @@ -275,6 +278,7 @@ } ?> /> + @@ -330,6 +334,7 @@ } ?> /> + diff --git a/apps/settings/tests/Controller/CheckSetupControllerTest.php b/apps/settings/tests/Controller/CheckSetupControllerTest.php index 43ec984041c8a..965d758634382 100644 --- a/apps/settings/tests/Controller/CheckSetupControllerTest.php +++ b/apps/settings/tests/Controller/CheckSetupControllerTest.php @@ -605,6 +605,7 @@ public function testCheck() { 'OCA\Settings\SetupChecks\LegacySSEKeyFormat' => ['pass' => true, 'description' => 'The old server-side-encryption format is enabled. We recommend disabling this.', 'severity' => 'warning', 'linkToDocumentation' => ''], 'OCA\Settings\SetupChecks\CheckUserCertificates' => ['pass' => false, 'description' => 'There are some user imported SSL certificates present, that are not used anymore with Nextcloud 21. They can be imported on the command line via "occ security:certificates:import" command. Their paths inside the data directory are shown below.', 'severity' => 'warning', 'elements' => ['a', 'b']], 'imageMagickLacksSVGSupport' => false, + 'isDefaultPhoneRegionSet' => false, ] ); $this->assertEquals($expected, $this->checkSetupController->check()); diff --git a/apps/settings/tests/Controller/UsersControllerTest.php b/apps/settings/tests/Controller/UsersControllerTest.php index 4679fd8f7ba0f..23e3ef5ec0117 100644 --- a/apps/settings/tests/Controller/UsersControllerTest.php +++ b/apps/settings/tests/Controller/UsersControllerTest.php @@ -31,6 +31,7 @@ use OC\Encryption\Exceptions\ModuleDoesNotExistsException; use OC\Group\Manager; use OCA\Settings\Controller\UsersController; +use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\BackgroundJob\IJobList; @@ -196,41 +197,41 @@ public function testSetUserSettings($email, $validEmail, $expectedStatus) { ->method('getUser') ->with($user) ->willReturn([ - AccountManager::PROPERTY_DISPLAYNAME => + IAccountManager::PROPERTY_DISPLAYNAME => [ 'value' => 'Display name', 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, 'verified' => AccountManager::NOT_VERIFIED, ], - AccountManager::PROPERTY_ADDRESS => + IAccountManager::PROPERTY_ADDRESS => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, 'verified' => AccountManager::NOT_VERIFIED, ], - AccountManager::PROPERTY_WEBSITE => + IAccountManager::PROPERTY_WEBSITE => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, 'verified' => AccountManager::NOT_VERIFIED, ], - AccountManager::PROPERTY_EMAIL => + IAccountManager::PROPERTY_EMAIL => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY, 'verified' => AccountManager::NOT_VERIFIED, ], - AccountManager::PROPERTY_AVATAR => + IAccountManager::PROPERTY_AVATAR => [ 'scope' => AccountManager::VISIBILITY_CONTACTS_ONLY ], - AccountManager::PROPERTY_PHONE => + IAccountManager::PROPERTY_PHONE => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, 'verified' => AccountManager::NOT_VERIFIED, ], - AccountManager::PROPERTY_TWITTER => + IAccountManager::PROPERTY_TWITTER => [ 'value' => '', 'scope' => AccountManager::VISIBILITY_PRIVATE, @@ -238,12 +239,14 @@ public function testSetUserSettings($email, $validEmail, $expectedStatus) { ], ]); - $controller->expects($this->once())->method('saveUserSettings'); + $controller->expects($this->once()) + ->method('saveUserSettings') + ->willReturnArgument(1); } else { $controller->expects($this->never())->method('saveUserSettings'); } - $result = $controller->setUserSettings( + $result = $controller->setUserSettings(// AccountManager::VISIBILITY_CONTACTS_ONLY, 'displayName', AccountManager::VISIBILITY_CONTACTS_ONLY, @@ -289,21 +292,21 @@ public function testSaveUserSettings($data, $user->method('getEMailAddress')->willReturn($oldEmailAddress); $user->method('canChangeDisplayName')->willReturn(true); - if ($data[AccountManager::PROPERTY_EMAIL]['value'] === $oldEmailAddress || - ($oldEmailAddress === null && $data[AccountManager::PROPERTY_EMAIL]['value'] === '')) { + if ($data[IAccountManager::PROPERTY_EMAIL]['value'] === $oldEmailAddress || + ($oldEmailAddress === null && $data[IAccountManager::PROPERTY_EMAIL]['value'] === '')) { $user->expects($this->never())->method('setEMailAddress'); } else { $user->expects($this->once())->method('setEMailAddress') - ->with($data[AccountManager::PROPERTY_EMAIL]['value']) + ->with($data[IAccountManager::PROPERTY_EMAIL]['value']) ->willReturn(true); } - if ($data[AccountManager::PROPERTY_DISPLAYNAME]['value'] === $oldDisplayName || - ($oldDisplayName === null && $data[AccountManager::PROPERTY_DISPLAYNAME]['value'] === '')) { + if ($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] === $oldDisplayName || + ($oldDisplayName === null && $data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] === '')) { $user->expects($this->never())->method('setDisplayName'); } else { $user->expects($this->once())->method('setDisplayName') - ->with($data[AccountManager::PROPERTY_DISPLAYNAME]['value']) + ->with($data[IAccountManager::PROPERTY_DISPLAYNAME]['value']) ->willReturn(true); } @@ -317,48 +320,48 @@ public function dataTestSaveUserSettings() { return [ [ [ - AccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], + IAccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], ], 'john@example.com', 'john doe' ], [ [ - AccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], + IAccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], ], 'johnNew@example.com', 'john New doe' ], [ [ - AccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], + IAccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], ], 'johnNew@example.com', 'john doe' ], [ [ - AccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], + IAccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], ], 'john@example.com', 'john New doe' ], [ [ - AccountManager::PROPERTY_EMAIL => ['value' => ''], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], + IAccountManager::PROPERTY_EMAIL => ['value' => ''], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], ], null, 'john New doe' ], [ [ - AccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], + IAccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], ], 'john@example.com', null @@ -391,14 +394,14 @@ public function testSaveUserSettingsException($data, $user->method('getDisplayName')->willReturn($oldDisplayName); $user->method('getEMailAddress')->willReturn($oldEmailAddress); - if ($data[AccountManager::PROPERTY_EMAIL]['value'] !== $oldEmailAddress) { + if ($data[IAccountManager::PROPERTY_EMAIL]['value'] !== $oldEmailAddress) { $user->method('canChangeDisplayName') ->willReturn($canChangeEmail); } - if ($data[AccountManager::PROPERTY_DISPLAYNAME]['value'] !== $oldDisplayName) { + if ($data[IAccountManager::PROPERTY_DISPLAYNAME]['value'] !== $oldDisplayName) { $user->method('setDisplayName') - ->with($data[AccountManager::PROPERTY_DISPLAYNAME]['value']) + ->with($data[IAccountManager::PROPERTY_DISPLAYNAME]['value']) ->willReturn($setDisplayNameResult); } @@ -410,8 +413,8 @@ public function dataTestSaveUserSettingsException() { return [ [ [ - AccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], + IAccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], ], 'johnNew@example.com', 'john New doe', @@ -420,8 +423,8 @@ public function dataTestSaveUserSettingsException() { ], [ [ - AccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], + IAccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], ], 'johnNew@example.com', 'john New doe', @@ -430,8 +433,8 @@ public function dataTestSaveUserSettingsException() { ], [ [ - AccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], + IAccountManager::PROPERTY_EMAIL => ['value' => 'john@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'john doe'], ], 'johnNew@example.com', 'john New doe', @@ -455,7 +458,7 @@ public function testGetVerificationCode($account, $type, $dataBefore, $expectedD $signature = 'theSignature'; $code = $message . ' ' . $signature; - if ($type === AccountManager::PROPERTY_TWITTER) { + if ($type === IAccountManager::PROPERTY_TWITTER) { $code = $message . ' ' . md5($signature); } @@ -470,7 +473,7 @@ public function testGetVerificationCode($account, $type, $dataBefore, $expectedD $controller->expects($this->any())->method('getCurrentTime')->willReturn(1234567); if ($onlyVerificationCode === false) { - $this->accountManager->expects($this->once())->method('updateUser')->with($user, $expectedData); + $this->accountManager->expects($this->once())->method('updateUser')->with($user, $expectedData)->willReturnArgument(1); $this->jobList->expects($this->once())->method('add') ->with('OCA\Settings\BackgroundJobs\VerifyUserData', [ @@ -492,25 +495,25 @@ public function testGetVerificationCode($account, $type, $dataBefore, $expectedD public function dataTestGetVerificationCode() { $accountDataBefore = [ - AccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => AccountManager::NOT_VERIFIED], - AccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => AccountManager::NOT_VERIFIED, 'signature' => 'theSignature'], + IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => AccountManager::NOT_VERIFIED], + IAccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => AccountManager::NOT_VERIFIED, 'signature' => 'theSignature'], ]; $accountDataAfterWebsite = [ - AccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => AccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'], - AccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => AccountManager::NOT_VERIFIED, 'signature' => 'theSignature'], + IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => AccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'], + IAccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => AccountManager::NOT_VERIFIED, 'signature' => 'theSignature'], ]; $accountDataAfterTwitter = [ - AccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => AccountManager::NOT_VERIFIED], - AccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => AccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'], + IAccountManager::PROPERTY_WEBSITE => ['value' => 'https://nextcloud.com', 'verified' => AccountManager::NOT_VERIFIED], + IAccountManager::PROPERTY_TWITTER => ['value' => '@nextclouders', 'verified' => AccountManager::VERIFICATION_IN_PROGRESS, 'signature' => 'theSignature'], ]; return [ - ['verify-twitter', AccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, false], - ['verify-website', AccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, false], - ['verify-twitter', AccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, true], - ['verify-website', AccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, true], + ['verify-twitter', IAccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, false], + ['verify-website', IAccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, false], + ['verify-twitter', IAccountManager::PROPERTY_TWITTER, $accountDataBefore, $accountDataAfterTwitter, true], + ['verify-website', IAccountManager::PROPERTY_WEBSITE, $accountDataBefore, $accountDataAfterWebsite, true], ]; } diff --git a/build/integration/features/bootstrap/Provisioning.php b/build/integration/features/bootstrap/Provisioning.php index 56d75c058aadf..a856e47ef2b2a 100644 --- a/build/integration/features/bootstrap/Provisioning.php +++ b/build/integration/features/bootstrap/Provisioning.php @@ -173,6 +173,37 @@ public function userHasSetting($user, $settings) { } } + /** + * @Then /^search users by phone for region "([^"]*)" with$/ + * + * @param string $user + * @param \Behat\Gherkin\Node\TableNode|null $settings + */ + public function searchUserByPhone($region, \Behat\Gherkin\Node\TableNode $searchTable) { + $fullUrl = $this->baseUrl . "v{$this->apiVersion}.php/cloud/users/search/by-phone"; + $client = new Client(); + $options = []; + $options['auth'] = $this->adminUser; + $options['headers'] = [ + 'OCS-APIREQUEST' => 'true', + ]; + + $search = []; + foreach ($searchTable->getRows() as $row) { + if (!isset($search[$row[0]])) { + $search[$row[0]] = []; + } + $search[$row[0]][] = $row[1]; + } + + $options['form_params'] = [ + 'location' => $region, + 'search' => $search, + ]; + + $this->response = $client->post($fullUrl, $options); + } + public function createUser($user) { $previous_user = $this->currentUser; $this->currentUser = "admin"; @@ -560,6 +591,19 @@ public function theUsersShouldBe($usersList) { } } + /** + * @Then /^phone matches returned are$/ + * @param \Behat\Gherkin\Node\TableNode|null $usersList + */ + public function thePhoneUsersShouldBe($usersList) { + if ($usersList instanceof \Behat\Gherkin\Node\TableNode) { + $users = $usersList->getRowsHash(); + $listCheckedElements = simplexml_load_string($this->response->getBody())->data; + $respondedArray = json_decode(json_encode($listCheckedElements), true); + Assert::assertEquals($users, $respondedArray); + } + } + /** * @Then /^detailed users returned are$/ * @param \Behat\Gherkin\Node\TableNode|null $usersList diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index ecc33c657f41a..717aa04e4bdbc 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -71,12 +71,12 @@ Feature: provisioning And the HTTP status code should be "200" And sending "PUT" to "/cloud/users/brand-new-user" with | key | email | - | value | brand-new-user@gmail.com | + | value | no-reply@nextcloud.com | And the OCS status code should be "100" And the HTTP status code should be "200" And sending "PUT" to "/cloud/users/brand-new-user" with | key | phone | - | value | 0123 456 789 | + | value | +49 711 / 25 24 28-90 | And the OCS status code should be "100" And the HTTP status code should be "200" And sending "PUT" to "/cloud/users/brand-new-user" with @@ -97,12 +97,29 @@ Feature: provisioning Then user "brand-new-user" has | id | brand-new-user | | displayname | Brand New User | - | email | brand-new-user@gmail.com | - | phone | 0123 456 789 | + | email | no-reply@nextcloud.com | + | phone | +4971125242890 | | address | Foo Bar Town | | website | https://nextcloud.com | | twitter | Nextcloud | + Scenario: Search by phone number + Given As an "admin" + And user "phone-user" exists + And sending "PUT" to "/cloud/users/phone-user" with + | key | phone | + | value | +49 711 / 25 24 28-90 | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then search users by phone for region "DE" with + | random-string1 | 0711 / 123 456 78 | + | random-string1 | 0711 / 252 428-90 | + | random-string2 | 0711 / 90-824 252 | + And the OCS status code should be "100" + And the HTTP status code should be "200" + Then phone matches returned are + | random-string1 | phone-user@localhost:8080 | + Scenario: Create a group Given As an "admin" And group "new-group" does not exist diff --git a/config/config.sample.php b/config/config.sample.php index 2710fbf5fdb5b..8adb5bf2168f8 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -194,6 +194,16 @@ */ 'default_locale' => 'en_US', +/** + * This sets the default region for phone numbers on your Nextcloud server, + * using ISO 3166-1 country codes such as ``DE`` for Germany, ``FR`` for France, … + * It is required to allow inserting phone numbers in the user profiles starting + * without the country code (e.g. +49 for Germany). + * + * No default value! + */ +'default_phone_region' => 'EN', + /** * With this setting a locale can be forced for all users. If a locale is * forced, the users are also unable to change their locale in the personal diff --git a/core/Migrations/Version21000Date20201202095923.php b/core/Migrations/Version21000Date20201202095923.php new file mode 100644 index 0000000000000..6433d8c9b7a4f --- /dev/null +++ b/core/Migrations/Version21000Date20201202095923.php @@ -0,0 +1,75 @@ + + * + * @author Joas Schilling + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Core\Migrations; + +use Closure; +use Doctrine\DBAL\Types\Types; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version21000Date20201202095923 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('accounts_data')) { + $table = $schema->createTable('accounts_data'); + $table->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 20, + ]); + $table->addColumn('uid', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('name', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->addColumn('value', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + 'default' => '', + ]); + $table->setPrimaryKey(['id']); + $table->addIndex(['uid'], 'accounts_data_uid'); + $table->addIndex(['name'], 'accounts_data_name'); + $table->addIndex(['value'], 'accounts_data_value'); + + return $schema; + } + + return null; + } +} diff --git a/core/js/setupchecks.js b/core/js/setupchecks.js index 214f148fa943c..22c8589f73b7a 100644 --- a/core/js/setupchecks.js +++ b/core/js/setupchecks.js @@ -216,6 +216,12 @@ type: OC.SetupChecks.MESSAGE_TYPE_WARNING }); } + if (!data.isDefaultPhoneRegionSet) { + messages.push({ + msg: t('core', 'Your installation has no default phone region set. This is required to be able to validate phone numbers in the profile settings without a country code. To allow numbers without a country code, please add "default_phone_region" with the respective ISO 3166-1 code of the wished region.'), + type: OC.SetupChecks.MESSAGE_TYPE_INFO + }); + } if (data.cronErrors.length > 0) { var listOfCronErrors = ""; data.cronErrors.forEach(function(element){ diff --git a/core/js/tests/specs/setupchecksSpec.js b/core/js/tests/specs/setupchecksSpec.js index a0a3c2a4ba9a1..c3cddb88a9d75 100644 --- a/core/js/tests/specs/setupchecksSpec.js +++ b/core/js/tests/specs/setupchecksSpec.js @@ -251,6 +251,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -306,6 +307,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -362,6 +364,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -416,6 +419,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -468,6 +472,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -522,6 +527,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -574,6 +580,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -626,6 +633,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -678,6 +686,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -751,6 +760,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -804,6 +814,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -857,6 +868,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -910,6 +922,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -962,6 +975,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: true, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyGeneratedURL: 'https://server', }) @@ -1014,6 +1028,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, reverseProxyDocs: 'https://docs.nextcloud.com/foo/bar.html', reverseProxyGeneratedURL: 'http://server', @@ -1067,6 +1082,7 @@ describe('OC.SetupChecks tests', function() { recommendedPHPModules: [], pendingBigIntConversionColumns: [], isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: true, isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: false, reverseProxyGeneratedURL: 'https://server', }) @@ -1080,6 +1096,59 @@ describe('OC.SetupChecks tests', function() { done(); }); }); + + it('should return an info if there is no default phone region', function(done) { + var async = OC.SetupChecks.checkSetup(); + + suite.server.requests[0].respond( + 200, + { + 'Content-Type': 'application/json', + }, + JSON.stringify({ + hasFileinfoInstalled: true, + isGetenvServerWorking: true, + isReadOnlyConfig: false, + hasWorkingFileLocking: true, + hasValidTransactionIsolationLevel: true, + suggestedOverwriteCliURL: '', + isRandomnessSecure: true, + securityDocs: 'https://docs.owncloud.org/myDocs.html', + serverHasInternetConnectionProblems: false, + isMemcacheConfigured: true, + forwardedForHeadersWorking: true, + isCorrectMemcachedPHPModuleInstalled: true, + hasPassedCodeIntegrityCheck: true, + isOpcacheProperlySetup: true, + hasOpcacheLoaded: true, + isSettimelimitAvailable: true, + hasFreeTypeSupport: true, + missingIndexes: [], + missingPrimaryKeys: [], + missingColumns: [], + cronErrors: [], + cronInfo: { + diffInSeconds: 0 + }, + isMemoryLimitSufficient: true, + appDirsWithDifferentOwner: [], + recommendedPHPModules: [], + pendingBigIntConversionColumns: [], + isMysqlUsedWithoutUTF8MB4: false, + isDefaultPhoneRegionSet: false, + isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed: true, + reverseProxyGeneratedURL: 'https://server', + }) + ); + + async.done(function( data, s, x ){ + expect(data).toEqual([{ + msg: 'Your installation has no default phone region set. This is required to be able to validate phone numbers in the profile settings without a country code. To allow numbers without a country code, please add "default_phone_region" with the respective ISO 3166-1 code of the wished region.', + type: OC.SetupChecks.MESSAGE_TYPE_INFO + }]); + done(); + }); + }); }); describe('checkGeneric', function() { diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 0400e68109056..b7dbc6675d237 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -930,6 +930,7 @@ 'OC\\Core\\Migrations\\Version20000Date20201109081918' => $baseDir . '/core/Migrations/Version20000Date20201109081918.php', 'OC\\Core\\Migrations\\Version20000Date20201109081919' => $baseDir . '/core/Migrations/Version20000Date20201109081919.php', 'OC\\Core\\Migrations\\Version20000Date20201111081915' => $baseDir . '/core/Migrations/Version20000Date20201111081915.php', + 'OC\\Core\\Migrations\\Version21000Date20201202095923' => $baseDir . '/core/Migrations/Version21000Date20201202095923.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', @@ -1266,6 +1267,7 @@ 'OC\\Repair\\NC20\\EncryptionMigration' => $baseDir . '/lib/private/Repair/NC20/EncryptionMigration.php', 'OC\\Repair\\NC20\\ShippedDashboardEnable' => $baseDir . '/lib/private/Repair/NC20/ShippedDashboardEnable.php', 'OC\\Repair\\NC21\\AddCheckForUserCertificatesJob' => $baseDir . '/lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php', + 'OC\\Repair\\NC21\\ValidatePhoneNumber' => $baseDir . '/lib/private/Repair/NC21/ValidatePhoneNumber.php', 'OC\\Repair\\OldGroupMembershipShares' => $baseDir . '/lib/private/Repair/OldGroupMembershipShares.php', 'OC\\Repair\\Owncloud\\DropAccountTermsTable' => $baseDir . '/lib/private/Repair/Owncloud/DropAccountTermsTable.php', 'OC\\Repair\\Owncloud\\SaveAccountsTableData' => $baseDir . '/lib/private/Repair/Owncloud/SaveAccountsTableData.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index b9b4f2f307b8c..a8984b486f3cb 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -959,6 +959,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version20000Date20201109081918' => __DIR__ . '/../../..' . '/core/Migrations/Version20000Date20201109081918.php', 'OC\\Core\\Migrations\\Version20000Date20201109081919' => __DIR__ . '/../../..' . '/core/Migrations/Version20000Date20201109081919.php', 'OC\\Core\\Migrations\\Version20000Date20201111081915' => __DIR__ . '/../../..' . '/core/Migrations/Version20000Date20201111081915.php', + 'OC\\Core\\Migrations\\Version21000Date20201202095923' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20201202095923.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', @@ -1295,6 +1296,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Repair\\NC20\\EncryptionMigration' => __DIR__ . '/../../..' . '/lib/private/Repair/NC20/EncryptionMigration.php', 'OC\\Repair\\NC20\\ShippedDashboardEnable' => __DIR__ . '/../../..' . '/lib/private/Repair/NC20/ShippedDashboardEnable.php', 'OC\\Repair\\NC21\\AddCheckForUserCertificatesJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php', + 'OC\\Repair\\NC21\\ValidatePhoneNumber' => __DIR__ . '/../../..' . '/lib/private/Repair/NC21/ValidatePhoneNumber.php', 'OC\\Repair\\OldGroupMembershipShares' => __DIR__ . '/../../..' . '/lib/private/Repair/OldGroupMembershipShares.php', 'OC\\Repair\\Owncloud\\DropAccountTermsTable' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/DropAccountTermsTable.php', 'OC\\Repair\\Owncloud\\SaveAccountsTableData' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/SaveAccountsTableData.php', diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index d18555d296c12..05feaf87b8ff7 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -30,10 +30,16 @@ namespace OC\Accounts; +use libphonenumber\NumberParseException; +use libphonenumber\PhoneNumber; +use libphonenumber\PhoneNumberFormat; +use libphonenumber\PhoneNumberUtil; use OCA\Settings\BackgroundJobs\VerifyUserData; use OCP\Accounts\IAccount; use OCP\Accounts\IAccountManager; use OCP\BackgroundJob\IJobList; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IUser; use Psr\Log\LoggerInterface; @@ -55,9 +61,15 @@ class AccountManager implements IAccountManager { /** @var IDBConnection database connection */ private $connection; + /** @var IConfig */ + private $config; + /** @var string table name */ private $table = 'accounts'; + /** @var string table name */ + private $dataTable = 'accounts_data'; + /** @var EventDispatcherInterface */ private $eventDispatcher; @@ -68,24 +80,70 @@ class AccountManager implements IAccountManager { private $logger; public function __construct(IDBConnection $connection, + IConfig $config, EventDispatcherInterface $eventDispatcher, IJobList $jobList, LoggerInterface $logger) { $this->connection = $connection; + $this->config = $config; $this->eventDispatcher = $eventDispatcher; $this->jobList = $jobList; $this->logger = $logger; } + /** + * @param string $input + * @return string Provided phone number in E.164 format when it was a valid number + * @throws \InvalidArgumentException When the phone number was invalid or no default region is set and the number doesn't start with a country code + */ + protected function parsePhoneNumber(string $input): string { + $defaultRegion = $this->config->getSystemValueString('default_phone_region', ''); + + if ($defaultRegion === '') { + // When no default region is set, only +49… numbers are valid + if (strpos($input, '+') !== 0) { + throw new \InvalidArgumentException(self::PROPERTY_PHONE); + } + + $defaultRegion = 'EN'; + } + + $phoneUtil = PhoneNumberUtil::getInstance(); + try { + $phoneNumber = $phoneUtil->parse($input, $defaultRegion); + if ($phoneNumber instanceof PhoneNumber && $phoneUtil->isValidNumber($phoneNumber)) { + return $phoneUtil->format($phoneNumber, PhoneNumberFormat::E164); + } + } catch (NumberParseException $e) { + } + + throw new \InvalidArgumentException(self::PROPERTY_PHONE); + } + /** * update user record * * @param IUser $user - * @param $data + * @param array $data + * @param bool $throwOnData Set to true if you can inform the user about invalid data + * @return array The potentially modified data (e.g. phone numbers are converted to E.164 format) + * @throws \InvalidArgumentException Message is the property that was invalid */ - public function updateUser(IUser $user, $data) { + public function updateUser(IUser $user, array $data, bool $throwOnData = false): array { $userData = $this->getUser($user); $updated = true; + + if (isset($data[self::PROPERTY_PHONE]) && $data[self::PROPERTY_PHONE]['value'] !== '') { + try { + $data[self::PROPERTY_PHONE]['value'] = $this->parsePhoneNumber($data[self::PROPERTY_PHONE]['value']); + } catch (\InvalidArgumentException $e) { + if ($throwOnData) { + throw $e; + } + $data[self::PROPERTY_PHONE]['value'] = ''; + } + } + if (empty($userData)) { $this->insertNewUser($user, $data); } elseif ($userData !== $data) { @@ -103,6 +161,8 @@ public function updateUser(IUser $user, $data) { new GenericEvent($user, $data) ); } + + return $data; } /** @@ -116,6 +176,21 @@ public function deleteUser(IUser $user) { $query->delete($this->table) ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))) ->execute(); + + $this->deleteUserData($user); + } + + /** + * delete user from accounts table + * + * @param IUser $user + */ + public function deleteUserData(IUser $user): void { + $uid = $user->getUID(); + $query = $this->connection->getQueryBuilder(); + $query->delete($this->dataTable) + ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))) + ->execute(); } /** @@ -153,6 +228,24 @@ public function getUser(IUser $user) { return $userDataArray; } + public function searchUsers(string $property, array $values): array { + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from($this->dataTable) + ->where($query->expr()->eq('name', $query->createNamedParameter($property))) + ->andWhere($query->expr()->in('value', $query->createNamedParameter($values, IQueryBuilder::PARAM_STR_ARRAY))); + + $result = $query->execute(); + $matches = []; + + while ($row = $result->fetch()) { + $matches[$row['value']] = $row['uid']; + } + $result->closeCursor(); + + return $matches; + } + /** * check if we need to ask the server for email verification, if yes we create a cronjob * @@ -173,7 +266,7 @@ protected function checkEmailVerification($oldData, $newData, IUser $user) { 'lastRun' => time() ] ); - $newData[AccountManager::PROPERTY_EMAIL]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS; + $newData[self::PROPERTY_EMAIL]['verified'] = self::VERIFICATION_IN_PROGRESS; } return $newData; @@ -256,7 +349,7 @@ protected function updateVerifyStatus($oldData, $newData) { * @param IUser $user * @param array $data */ - protected function insertNewUser(IUser $user, $data) { + protected function insertNewUser(IUser $user, array $data): void { $uid = $user->getUID(); $jsonEncodedData = json_encode($data); $query = $this->connection->getQueryBuilder(); @@ -268,6 +361,9 @@ protected function insertNewUser(IUser $user, $data) { ] ) ->execute(); + + $this->deleteUserData($user); + $this->writeUserData($user, $data); } /** @@ -276,7 +372,7 @@ protected function insertNewUser(IUser $user, $data) { * @param IUser $user * @param array $data */ - protected function updateExistingUser(IUser $user, $data) { + protected function updateExistingUser(IUser $user, array $data): void { $uid = $user->getUID(); $jsonEncodedData = json_encode($data); $query = $this->connection->getQueryBuilder(); @@ -284,6 +380,30 @@ protected function updateExistingUser(IUser $user, $data) { ->set('data', $query->createNamedParameter($jsonEncodedData)) ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))) ->execute(); + + $this->deleteUserData($user); + $this->writeUserData($user, $data); + } + + protected function writeUserData(IUser $user, array $data): void { + $query = $this->connection->getQueryBuilder(); + $query->insert($this->dataTable) + ->values( + [ + 'uid' => $query->createNamedParameter($user->getUID()), + 'name' => $query->createParameter('name'), + 'value' => $query->createParameter('value'), + ] + ); + foreach ($data as $propertyName => $property) { + if ($propertyName === self::PROPERTY_AVATAR) { + continue; + } + + $query->setParameter('name', $propertyName) + ->setParameter('value', $property['value']); + $query->execute(); + } } /** diff --git a/lib/private/Accounts/Hooks.php b/lib/private/Accounts/Hooks.php index 82f55f5a9d1a2..c5e7c34deaf54 100644 --- a/lib/private/Accounts/Hooks.php +++ b/lib/private/Accounts/Hooks.php @@ -24,6 +24,7 @@ namespace OC\Accounts; +use OCP\Accounts\IAccountManager; use OCP\IUser; use Psr\Log\LoggerInterface; @@ -61,14 +62,14 @@ public function changeUserHook($params) { switch ($feature) { case 'eMailAddress': - if ($accountData[AccountManager::PROPERTY_EMAIL]['value'] !== $newValue) { - $accountData[AccountManager::PROPERTY_EMAIL]['value'] = $newValue; + if ($accountData[IAccountManager::PROPERTY_EMAIL]['value'] !== $newValue) { + $accountData[IAccountManager::PROPERTY_EMAIL]['value'] = $newValue; $accountManager->updateUser($user, $accountData); } break; case 'displayName': - if ($accountData[AccountManager::PROPERTY_DISPLAYNAME]['value'] !== $newValue) { - $accountData[AccountManager::PROPERTY_DISPLAYNAME]['value'] = $newValue; + if ($accountData[IAccountManager::PROPERTY_DISPLAYNAME]['value'] !== $newValue) { + $accountData[IAccountManager::PROPERTY_DISPLAYNAME]['value'] = $newValue; $accountManager->updateUser($user, $accountData); } break; diff --git a/lib/private/Repair.php b/lib/private/Repair.php index ec7483555672b..847a41aeb254f 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -53,6 +53,7 @@ use OC\Repair\NC20\EncryptionMigration; use OC\Repair\NC20\ShippedDashboardEnable; use OC\Repair\NC21\AddCheckForUserCertificatesJob; +use OC\Repair\NC21\ValidatePhoneNumber; use OC\Repair\OldGroupMembershipShares; use OC\Repair\Owncloud\DropAccountTermsTable; use OC\Repair\Owncloud\SaveAccountsTableData; @@ -177,7 +178,8 @@ public static function getRepairSteps() { */ public static function getExpensiveRepairSteps() { return [ - new OldGroupMembershipShares(\OC::$server->getDatabaseConnection(), \OC::$server->getGroupManager()) + new OldGroupMembershipShares(\OC::$server->getDatabaseConnection(), \OC::$server->getGroupManager()), + \OC::$server->get(ValidatePhoneNumber::class), ]; } diff --git a/lib/private/Repair/NC21/ValidatePhoneNumber.php b/lib/private/Repair/NC21/ValidatePhoneNumber.php new file mode 100644 index 0000000000000..6e25ff26b7efa --- /dev/null +++ b/lib/private/Repair/NC21/ValidatePhoneNumber.php @@ -0,0 +1,89 @@ + + * + * @author Joas Schilling + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Repair\NC21; + +use OC\Accounts\AccountManager; +use OCP\Accounts\IAccountManager; +use OCP\IConfig; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class ValidatePhoneNumber implements IRepairStep { + + /** @var IConfig */ + protected $config; + /** @var IUserManager */ + protected $userManager; + /** @var AccountManager */ + private $accountManager; + + public function __construct(IUserManager $userManager, + AccountManager $accountManager, + IConfig $config) { + $this->config = $config; + $this->userManager = $userManager; + $this->accountManager = $accountManager; + } + + public function getName(): string { + return 'Validate the phone number and store it in a known format for search'; + } + + private function shouldRun(): bool { + return true; + } + + public function run(IOutput $output): void { + if ($this->config->getSystemValueString('default_phone_region', '') === '') { + throw new \Exception('Can not validate phone numbers without `default_phone_region` being set in the config file'); + } + + $numUpdated = 0; + $numRemoved = 0; + + $this->userManager->callForSeenUsers(function (IUser $user) use (&$numUpdated, &$numRemoved) { + $account = $this->accountManager->getUser($user); + + if ($account[IAccountManager::PROPERTY_PHONE]['value'] !== '') { + $updated = $this->accountManager->updateUser($user, $account); + + if ($account[IAccountManager::PROPERTY_PHONE]['value'] !== $updated[IAccountManager::PROPERTY_PHONE]['value']) { + if ($updated[IAccountManager::PROPERTY_PHONE]['value'] === '') { + $numRemoved++; + } else { + $numUpdated++; + } + } + } + }); + + if ($numRemoved > 0 || $numUpdated > 0) { + $output->info('Updated ' . $numUpdated . ' entries and cleaned ' . $numRemoved . ' invalid phone numbers'); + } + } +} diff --git a/lib/public/Accounts/IAccountManager.php b/lib/public/Accounts/IAccountManager.php index 3306abc55f174..63824ed94b93e 100644 --- a/lib/public/Accounts/IAccountManager.php +++ b/lib/public/Accounts/IAccountManager.php @@ -65,4 +65,15 @@ interface IAccountManager { * @return IAccount */ public function getAccount(IUser $user): IAccount; + + /** + * Search for users based on account data + * + * @param string $property + * @param string[] $values + * @return array + * + * @since 21.0.0 + */ + public function searchUsers(string $property, array $values): array; } diff --git a/tests/lib/Accounts/AccountsManagerTest.php b/tests/lib/Accounts/AccountsManagerTest.php index ff75b51d008eb..d13d5f4186a5a 100644 --- a/tests/lib/Accounts/AccountsManagerTest.php +++ b/tests/lib/Accounts/AccountsManagerTest.php @@ -25,6 +25,7 @@ use OC\Accounts\AccountManager; use OCP\Accounts\IAccountManager; use OCP\BackgroundJob\IJobList; +use OCP\IConfig; use OCP\IUser; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; @@ -43,6 +44,9 @@ class AccountsManagerTest extends TestCase { /** @var \OCP\IDBConnection */ private $connection; + /** @var IConfig|MockObject */ + private $config; + /** @var EventDispatcherInterface|MockObject */ private $eventDispatcher; @@ -59,6 +63,7 @@ protected function setUp(): void { parent::setUp(); $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); $this->connection = \OC::$server->getDatabaseConnection(); + $this->config = $this->createMock(IConfig::class); $this->jobList = $this->createMock(IJobList::class); $this->logger = $this->createMock(LoggerInterface::class); } @@ -77,7 +82,13 @@ protected function tearDown(): void { */ public function getInstance($mockedMethods = null) { return $this->getMockBuilder(AccountManager::class) - ->setConstructorArgs([$this->connection, $this->eventDispatcher, $this->jobList, $this->logger]) + ->setConstructorArgs([ + $this->connection, + $this->config, + $this->eventDispatcher, + $this->jobList, + $this->logger, + ]) ->setMethods($mockedMethods) ->getMock(); } @@ -187,9 +198,9 @@ public function dataTestGetUser() { public function testUpdateExistingUser() { $user = $this->getMockBuilder(IUser::class)->getMock(); - $user->expects($this->once())->method('getUID')->willReturn('uid'); - $oldData = ['key' => 'value']; - $newData = ['newKey' => 'newValue']; + $user->expects($this->atLeastOnce())->method('getUID')->willReturn('uid'); + $oldData = ['key' => ['value' => 'value']]; + $newData = ['newKey' => ['value' => 'newValue']]; $accountManager = $this->getInstance(); $this->addDummyValuesToTable('uid', $oldData); @@ -201,10 +212,10 @@ public function testUpdateExistingUser() { public function testInsertNewUser() { $user = $this->getMockBuilder(IUser::class)->getMock(); $uid = 'uid'; - $data = ['key' => 'value']; + $data = ['key' => ['value' => 'value']]; $accountManager = $this->getInstance(); - $user->expects($this->once())->method('getUID')->willReturn($uid); + $user->expects($this->atLeastOnce())->method('getUID')->willReturn($uid); $this->assertNull($this->getDataFromTable($uid)); $this->invokePrivate($accountManager, 'insertNewUser', [$user, $data]); @@ -293,4 +304,32 @@ public function testGetAccount() { ->willReturn($data); $this->assertEquals($expected, $accountManager->getAccount($user)); } + + public function dataParsePhoneNumber(): array { + return [ + ['0711 / 25 24 28-90', 'DE', '+4971125242890'], + ['0711 / 25 24 28-90', '', null], + ['+49 711 / 25 24 28-90', '', '+4971125242890'], + ]; + } + + /** + * @dataProvider dataParsePhoneNumber + * @param string $phoneInput + * @param string $defaultRegion + * @param string|null $phoneNumber + */ + public function testParsePhoneNumber(string $phoneInput, string $defaultRegion, ?string $phoneNumber): void { + $this->config->method('getSystemValueString') + ->willReturn($defaultRegion); + + $instance = $this->getInstance(); + + if ($phoneNumber === null) { + $this->expectException(\InvalidArgumentException::class); + self::invokePrivate($instance, 'parsePhoneNumber', [$phoneInput]); + } else { + self::assertEquals($phoneNumber, self::invokePrivate($instance, 'parsePhoneNumber', [$phoneInput])); + } + } } diff --git a/tests/lib/Accounts/HooksTest.php b/tests/lib/Accounts/HooksTest.php index 39003b2ef2350..8af9e209034df 100644 --- a/tests/lib/Accounts/HooksTest.php +++ b/tests/lib/Accounts/HooksTest.php @@ -23,6 +23,7 @@ use OC\Accounts\AccountManager; use OC\Accounts\Hooks; +use OCP\Accounts\IAccountManager; use OCP\IUser; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; @@ -77,11 +78,11 @@ public function testChangeUserHook($params, $data, $setEmail, $setDisplayName, $ $this->accountManager->expects($this->once())->method('getUser')->willReturn($data); $newData = $data; if ($setEmail) { - $newData[AccountManager::PROPERTY_EMAIL]['value'] = $params['value']; + $newData[IAccountManager::PROPERTY_EMAIL]['value'] = $params['value']; $this->accountManager->expects($this->once())->method('updateUser') ->with($params['user'], $newData); } elseif ($setDisplayName) { - $newData[AccountManager::PROPERTY_DISPLAYNAME]['value'] = $params['value']; + $newData[IAccountManager::PROPERTY_DISPLAYNAME]['value'] = $params['value']; $this->accountManager->expects($this->once())->method('updateUser') ->with($params['user'], $newData); } else { @@ -98,48 +99,48 @@ public function dataTestChangeUserHook() { [ ['feature' => '', 'value' => ''], [ - AccountManager::PROPERTY_EMAIL => ['value' => ''], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => ''] + IAccountManager::PROPERTY_EMAIL => ['value' => ''], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => ''] ], false, false, true ], [ ['user' => $user, 'value' => ''], [ - AccountManager::PROPERTY_EMAIL => ['value' => ''], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => ''] + IAccountManager::PROPERTY_EMAIL => ['value' => ''], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => ''] ], false, false, true ], [ ['user' => $user, 'feature' => ''], [ - AccountManager::PROPERTY_EMAIL => ['value' => ''], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => ''] + IAccountManager::PROPERTY_EMAIL => ['value' => ''], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => ''] ], false, false, true ], [ ['user' => $user, 'feature' => 'foo', 'value' => 'bar'], [ - AccountManager::PROPERTY_EMAIL => ['value' => 'oldMail@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'oldDisplayName'] + IAccountManager::PROPERTY_EMAIL => ['value' => 'oldMail@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'oldDisplayName'] ], false, false, false ], [ ['user' => $user, 'feature' => 'eMailAddress', 'value' => 'newMail@example.com'], [ - AccountManager::PROPERTY_EMAIL => ['value' => 'oldMail@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'oldDisplayName'] + IAccountManager::PROPERTY_EMAIL => ['value' => 'oldMail@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'oldDisplayName'] ], true, false, false ], [ ['user' => $user, 'feature' => 'displayName', 'value' => 'newDisplayName'], [ - AccountManager::PROPERTY_EMAIL => ['value' => 'oldMail@example.com'], - AccountManager::PROPERTY_DISPLAYNAME => ['value' => 'oldDisplayName'] + IAccountManager::PROPERTY_EMAIL => ['value' => 'oldMail@example.com'], + IAccountManager::PROPERTY_DISPLAYNAME => ['value' => 'oldDisplayName'] ], false, true, false ], diff --git a/version.php b/version.php index 24e3e8db6aeba..299855816a9fd 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = [21, 0, 0, 7]; +$OC_Version = [21, 0, 0, 8]; // The human readable string $OC_VersionString = '21.0.0 alpha';