diff options
-rw-r--r-- | apps/provisioning_api/appinfo/routes.php | 4 | ||||
-rw-r--r-- | apps/provisioning_api/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/provisioning_api/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/provisioning_api/composer/composer/installed.php | 4 | ||||
-rw-r--r-- | apps/provisioning_api/lib/Controller/UsersController.php | 14 | ||||
-rw-r--r-- | apps/provisioning_api/lib/Controller/VerificationController.php | 121 | ||||
-rw-r--r-- | core/templates/success.php | 13 | ||||
-rw-r--r-- | lib/private/Accounts/AccountManager.php | 131 | ||||
-rw-r--r-- | lib/private/Accounts/AccountProperty.php | 21 | ||||
-rw-r--r-- | lib/private/Accounts/AccountPropertyCollection.php | 9 | ||||
-rw-r--r-- | lib/private/Server.php | 4 | ||||
-rw-r--r-- | lib/public/Accounts/IAccountProperty.php | 20 | ||||
-rw-r--r-- | lib/public/Accounts/IAccountPropertyCollection.php | 9 | ||||
-rw-r--r-- | tests/lib/Accounts/AccountManagerTest.php | 36 |
14 files changed, 371 insertions, 17 deletions
diff --git a/apps/provisioning_api/appinfo/routes.php b/apps/provisioning_api/appinfo/routes.php index 2f981e0c924..81a5bb94e02 100644 --- a/apps/provisioning_api/appinfo/routes.php +++ b/apps/provisioning_api/appinfo/routes.php @@ -74,4 +74,8 @@ return [ ['name' => 'AppConfig#setValue', 'url' => '/api/v1/config/apps/{app}/{key}', 'verb' => 'POST'], ['name' => 'AppConfig#deleteKey', 'url' => '/api/v1/config/apps/{app}/{key}', 'verb' => 'DELETE'], ], + 'routes' => [ + // Verification + ['name' => 'Verification#verifyMail', 'url' => '/mailVerification/{key}/{token}/{userId}', 'verb' => 'GET'], + ] ]; diff --git a/apps/provisioning_api/composer/composer/autoload_classmap.php b/apps/provisioning_api/composer/composer/autoload_classmap.php index 22927806e65..447f92afc8d 100644 --- a/apps/provisioning_api/composer/composer/autoload_classmap.php +++ b/apps/provisioning_api/composer/composer/autoload_classmap.php @@ -14,6 +14,7 @@ return array( 'OCA\\Provisioning_API\\Controller\\AppsController' => $baseDir . '/../lib/Controller/AppsController.php', 'OCA\\Provisioning_API\\Controller\\GroupsController' => $baseDir . '/../lib/Controller/GroupsController.php', 'OCA\\Provisioning_API\\Controller\\UsersController' => $baseDir . '/../lib/Controller/UsersController.php', + 'OCA\\Provisioning_API\\Controller\\VerificationController' => $baseDir . '/../lib/Controller/VerificationController.php', 'OCA\\Provisioning_API\\FederatedShareProviderFactory' => $baseDir . '/../lib/FederatedShareProviderFactory.php', 'OCA\\Provisioning_API\\Listener\\UserDeletedListener' => $baseDir . '/../lib/Listener/UserDeletedListener.php', 'OCA\\Provisioning_API\\Middleware\\Exceptions\\NotSubAdminException' => $baseDir . '/../lib/Middleware/Exceptions/NotSubAdminException.php', diff --git a/apps/provisioning_api/composer/composer/autoload_static.php b/apps/provisioning_api/composer/composer/autoload_static.php index f5a4b73f4f8..6dbf6b45c79 100644 --- a/apps/provisioning_api/composer/composer/autoload_static.php +++ b/apps/provisioning_api/composer/composer/autoload_static.php @@ -29,6 +29,7 @@ class ComposerStaticInitProvisioning_API 'OCA\\Provisioning_API\\Controller\\AppsController' => __DIR__ . '/..' . '/../lib/Controller/AppsController.php', 'OCA\\Provisioning_API\\Controller\\GroupsController' => __DIR__ . '/..' . '/../lib/Controller/GroupsController.php', 'OCA\\Provisioning_API\\Controller\\UsersController' => __DIR__ . '/..' . '/../lib/Controller/UsersController.php', + 'OCA\\Provisioning_API\\Controller\\VerificationController' => __DIR__ . '/..' . '/../lib/Controller/VerificationController.php', 'OCA\\Provisioning_API\\FederatedShareProviderFactory' => __DIR__ . '/..' . '/../lib/FederatedShareProviderFactory.php', 'OCA\\Provisioning_API\\Listener\\UserDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/UserDeletedListener.php', 'OCA\\Provisioning_API\\Middleware\\Exceptions\\NotSubAdminException' => __DIR__ . '/..' . '/../lib/Middleware/Exceptions/NotSubAdminException.php', diff --git a/apps/provisioning_api/composer/composer/installed.php b/apps/provisioning_api/composer/composer/installed.php index 244245bc0cf..9f53826650b 100644 --- a/apps/provisioning_api/composer/composer/installed.php +++ b/apps/provisioning_api/composer/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), - 'reference' => 'fb5ee6087bfd1f4cc2f37cda7a7cab7072aaae86', + 'reference' => '13a9cd28a5a5d92e285df040d084d5d608e2f768', 'name' => '__root__', 'dev' => false, ), @@ -16,7 +16,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), - 'reference' => 'fb5ee6087bfd1f4cc2f37cda7a7cab7072aaae86', + 'reference' => '13a9cd28a5a5d92e285df040d084d5d608e2f768', 'dev_requirement' => false, ), ), diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php index 20472bebace..114dfcb074a 100644 --- a/apps/provisioning_api/lib/Controller/UsersController.php +++ b/apps/provisioning_api/lib/Controller/UsersController.php @@ -621,6 +621,10 @@ class UsersController extends AUserData { throw new OCSException('', OCSController::RESPOND_NOT_FOUND); } + $subAdminManager = $this->groupManager->getSubAdmin(); + $isAdminOrSubadmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID()) + || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser); + $permittedFields = []; if ($targetUser->getUID() === $currentLoggedInUser->getUID()) { // Editing self (display, email) @@ -628,11 +632,8 @@ class UsersController extends AUserData { $permittedFields[] = IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX; } else { // Check if admin / subadmin - $subAdminManager = $this->groupManager->getSubAdmin(); - if ($this->groupManager->isAdmin($currentLoggedInUser->getUID()) - || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) { + if ($isAdminOrSubadmin) { // They have permissions over the user - $permittedFields[] = IAccountManager::COLLECTION_EMAIL; } else { // No rights @@ -652,6 +653,11 @@ class UsersController extends AUserData { $mailCollection->removePropertyByValue($key); if ($value !== '') { $mailCollection->addPropertyWithDefaults($value); + $property = $mailCollection->getPropertyByValue($key); + if ($isAdminOrSubadmin && $property) { + // admin set mails are auto-verified + $property->setLocallyVerified(IAccountManager::VERIFIED); + } } $this->accountManager->updateAccount($userAccount); break; diff --git a/apps/provisioning_api/lib/Controller/VerificationController.php b/apps/provisioning_api/lib/Controller/VerificationController.php new file mode 100644 index 00000000000..b248d3e8285 --- /dev/null +++ b/apps/provisioning_api/lib/Controller/VerificationController.php @@ -0,0 +1,121 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @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 <https://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Provisioning_API\Controller; + +use InvalidArgumentException; +use OC\Security\Crypto; +use OCP\Accounts\IAccountManager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\Security\VerificationToken\InvalidTokenException; +use OCP\Security\VerificationToken\IVerificationToken; + +class VerificationController extends Controller { + + /** @var IVerificationToken */ + private $verificationToken; + /** @var IUserManager */ + private $userManager; + /** @var IL10N */ + private $l10n; + /** @var IUserSession */ + private $userSession; + /** @var IAccountManager */ + private $accountManager; + /** @var Crypto */ + private $crypto; + + public function __construct( + string $appName, + IRequest $request, + IVerificationToken $verificationToken, + IUserManager $userManager, + IL10N $l10n, + IUserSession $userSession, + IAccountManager $accountManager, + Crypto $crypto + ) { + parent::__construct($appName, $request); + $this->verificationToken = $verificationToken; + $this->userManager = $userManager; + $this->l10n = $l10n; + $this->userSession = $userSession; + $this->accountManager = $accountManager; + $this->crypto = $crypto; + } + + /** + * @NoCSRFRequired + */ + public function verifyMail(string $token, string $userId, string $key) { + try { + if ($this->userSession->getUser()->getUID() !== $userId) { + throw new InvalidArgumentException('Logged in user is not mail address owner'); + } + $email = $this->crypto->decrypt($key); + $ref = \substr(hash('sha256', $email), 0, 8); + + $user = $this->userManager->get($userId); + $this->verificationToken->check($token, $user, 'verifyMail' . $ref, $email); + + $userAccount = $this->accountManager->getAccount($user); + $emailProperty = $userAccount->getPropertyCollection(IAccountManager::COLLECTION_EMAIL) + ->getPropertyByValue($email); + + if ($emailProperty === null) { + throw new InvalidArgumentException($this->l10n->t('Email was already removed from account and cannot be confirmed anymore.')); + } + $emailProperty->setLocallyVerified(IAccountManager::VERIFIED); + $this->accountManager->updateAccount($userAccount); + } catch (InvalidTokenException $e) { + $error = $e->getCode() === InvalidTokenException::TOKEN_EXPIRED + ? $this->l10n->t('Could not verify mail because the token is expired.') + : $this->l10n->t('Could not verify mail because the token is invalid.'); + } catch (InvalidArgumentException $e) { + $error = $e->getMessage(); + } catch (\Exception $e) { + $error = $this->l10n->t('An unexpected error occurred. Please consult your sysadmin.'); + } + + if (isset($error)) { + return new TemplateResponse( + 'core', 'error', [ + 'errors' => [['error' => $error]] + ], 'guest'); + } + + return new TemplateResponse( + 'core', 'success', [ + 'title' => $this->l10n->t('Email confirmation successful'), + 'message' => $this->l10n->t('Email confirmation successful'), + ], 'guest'); + } +} diff --git a/core/templates/success.php b/core/templates/success.php new file mode 100644 index 00000000000..5ce8ff4f045 --- /dev/null +++ b/core/templates/success.php @@ -0,0 +1,13 @@ +<?php +/** @var array $_ */ +/** @var \OCP\IL10N $l */ +/** @var \OCP\Defaults $theme */ +?> + +<div class="update"> + <h2><?php p($_['title']) ?></h2> + <p><?php p($_['message']) ?></p> + <p><a class="button primary" href="<?php p(\OC::$server->get(\OCP\IURLGenerator::class)->linkTo('', 'index.php')) ?>"> + <?php p($l->t('Go to %s', [$theme->getName()])); ?> + </a></p> +</div> diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 9fc5accfa08..a3f971df6a1 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -32,6 +32,7 @@ */ namespace OC\Accounts; +use Exception; use InvalidArgumentException; use libphonenumber\NumberParseException; use libphonenumber\PhoneNumber; @@ -45,9 +46,17 @@ use OCP\Accounts\IAccountPropertyCollection; use OCP\Accounts\PropertyDoesNotExistException; use OCP\BackgroundJob\IJobList; use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Defaults; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IL10N; +use OCP\IURLGenerator; use OCP\IUser; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Security\ICrypto; +use OCP\Security\VerificationToken\IVerificationToken; +use OCP\Util; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; @@ -88,17 +97,46 @@ class AccountManager implements IAccountManager { /** @var LoggerInterface */ private $logger; - - public function __construct(IDBConnection $connection, - IConfig $config, - EventDispatcherInterface $eventDispatcher, - IJobList $jobList, - LoggerInterface $logger) { + /** @var IVerificationToken */ + private $verificationToken; + /** @var IMailer */ + private $mailer; + /** @var Defaults */ + private $defaults; + /** @var IL10N */ + private $l10n; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var ICrypto */ + private $crypto; + /** @var IFactory */ + private $l10nfactory; + + public function __construct( + IDBConnection $connection, + IConfig $config, + EventDispatcherInterface $eventDispatcher, + IJobList $jobList, + LoggerInterface $logger, + IVerificationToken $verificationToken, + IMailer $mailer, + Defaults $defaults, + IFactory $factory, + IURLGenerator $urlGenerator, + ICrypto $crypto + ) { $this->connection = $connection; $this->config = $config; $this->eventDispatcher = $eventDispatcher; $this->jobList = $jobList; $this->logger = $logger; + $this->verificationToken = $verificationToken; + $this->mailer = $mailer; + $this->defaults = $defaults; + $this->urlGenerator = $urlGenerator; + $this->crypto = $crypto; + // DIing IL10N results in a dependency loop + $this->l10nfactory = $factory; } /** @@ -337,7 +375,6 @@ class AccountManager implements IAccountManager { /** * check if we need to ask the server for email verification, if yes we create a cronjob - * */ protected function checkEmailVerification(IAccount $updatedAccount, array $oldData): void { try { @@ -358,11 +395,73 @@ class AccountManager implements IAccountManager { ] ); + $property->setVerified(self::VERIFICATION_IN_PROGRESS); + } + } + + protected function checkLocalEmailVerification(IAccount $updatedAccount, array $oldData): void { + $mailCollection = $updatedAccount->getPropertyCollection(self::COLLECTION_EMAIL); + foreach ($mailCollection->getProperties() as $property) { + if ($property->getLocallyVerified() !== self::NOT_VERIFIED) { + continue; + } + if ($this->sendEmailVerificationEmail($updatedAccount->getUser(), $property->getValue())) { + $property->setLocallyVerified(self::VERIFICATION_IN_PROGRESS); + } + } + } + + protected function sendEmailVerificationEmail(IUser $user, string $email): bool { + $ref = \substr(hash('sha256', $email), 0, 8); + $key = $this->crypto->encrypt($email); + $token = $this->verificationToken->create($user, 'verifyMail' . $ref, $email); + $link = $this->urlGenerator->linkToRouteAbsolute('provisioning_api.Verification.verifyMail', + [ + 'userId' => $user->getUID(), + 'token' => $token, + 'key' => $key + ]); + $emailTemplate = $this->mailer->createEMailTemplate('core.EmailVerification', [ + 'link' => $link, + ]); - $property->setVerified(self::VERIFICATION_IN_PROGRESS); + if (!$this->l10n) { + $this->l10n = $this->l10nfactory->get('core'); } + + $emailTemplate->setSubject($this->l10n->t('%s email verification', [$this->defaults->getName()])); + $emailTemplate->addHeader(); + $emailTemplate->addHeading($this->l10n->t('Email verification')); + + $emailTemplate->addBodyText( + htmlspecialchars($this->l10n->t('Click the following button to confirm your email.')), + $this->l10n->t('Click the following link to confirm your email.') + ); + + $emailTemplate->addBodyButton( + htmlspecialchars($this->l10n->t('Confirm your email')), + $link, + false + ); + $emailTemplate->addFooter(); + + try { + $message = $this->mailer->createMessage(); + $message->setTo([$email => $user->getDisplayName()]); + $message->setFrom([Util::getDefaultEmailAddress('verification-noreply') => $this->defaults->getName()]); + $message->useTemplate($emailTemplate); + $this->mailer->send($message); + } catch (Exception $e) { + // Log the exception and continue + $this->logger->info('Failed to send verification mail', [ + 'app' => 'core', + 'exception' => $e + ]); + return false; + } + return true; } /** @@ -406,7 +505,6 @@ class AccountManager implements IAccountManager { } } - /** * add new user to accounts table * @@ -435,6 +533,12 @@ class AccountManager implements IAccountManager { foreach ($data as $dataRow) { $propertyName = $dataRow['name']; unset($dataRow['name']); + + if (isset($dataRow['locallyVerified']) && $dataRow['locallyVerified'] === self::NOT_VERIFIED) { + // do not write default value, save DB space + unset($dataRow['locallyVerified']); + } + if (!$this->isCollection($propertyName)) { $preparedData[$propertyName] = $dataRow; continue; @@ -511,7 +615,6 @@ class AccountManager implements IAccountManager { continue; } - $query->setParameter('name', $property['name']) ->setParameter('value', $property['value'] ?? ''); $query->executeStatement(); @@ -587,6 +690,7 @@ class AccountManager implements IAccountManager { $data['verified'] ?? self::NOT_VERIFIED, '' ); + $p->setLocallyVerified($data['locallyVerified'] ?? self::NOT_VERIFIED); $collection->addProperty($p); return $collection; @@ -599,6 +703,10 @@ class AccountManager implements IAccountManager { $account->setPropertyCollection($this->arrayDataToCollection($account, $accountData)); } else { $account->setProperty($accountData['name'], $accountData['value'] ?? '', $accountData['scope'] ?? self::SCOPE_LOCAL, $accountData['verified'] ?? self::NOT_VERIFIED); + if (isset($accountData['locallyVerified'])) { + $property = $account->getProperty($accountData['name']); + $property->setLocallyVerified($accountData['locallyVerified']); + } } } return $account; @@ -640,14 +748,17 @@ class AccountManager implements IAccountManager { $oldData = $this->getUser($account->getUser(), false); $this->updateVerificationStatus($account, $oldData); $this->checkEmailVerification($account, $oldData); + $this->checkLocalEmailVerification($account, $oldData); $data = []; foreach ($account->getAllProperties() as $property) { + /** @var IAccountProperty $property */ $data[] = [ 'name' => $property->getName(), 'value' => $property->getValue(), 'scope' => $property->getScope(), 'verified' => $property->getVerified(), + 'locallyVerified' => $property->getLocallyVerified(), ]; } diff --git a/lib/private/Accounts/AccountProperty.php b/lib/private/Accounts/AccountProperty.php index 1a21baf9698..0e6356e9e92 100644 --- a/lib/private/Accounts/AccountProperty.php +++ b/lib/private/Accounts/AccountProperty.php @@ -27,6 +27,7 @@ declare(strict_types=1); */ namespace OC\Accounts; +use InvalidArgumentException; use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountProperty; @@ -42,6 +43,8 @@ class AccountProperty implements IAccountProperty { private $verified; /** @var string */ private $verificationData; + /** @var string */ + private $locallyVerified = IAccountManager::NOT_VERIFIED; public function __construct(string $name, string $value, string $scope, string $verified, string $verificationData) { $this->name = $name; @@ -90,7 +93,7 @@ class AccountProperty implements IAccountProperty { IAccountManager::SCOPE_PRIVATE, IAccountManager::SCOPE_PUBLISHED ])) { - throw new \InvalidArgumentException('Invalid scope'); + throw new InvalidArgumentException('Invalid scope'); } $this->scope = $newScope; return $this; @@ -178,4 +181,20 @@ class AccountProperty implements IAccountProperty { public function getVerificationData(): string { return $this->verificationData; } + + public function setLocallyVerified(string $verified): IAccountProperty { + if (!in_array($verified, [ + IAccountManager::NOT_VERIFIED, + IAccountManager::VERIFICATION_IN_PROGRESS, + IAccountManager::VERIFIED, + ])) { + throw new InvalidArgumentException('Provided verification value is invalid'); + } + $this->locallyVerified = $verified; + return $this; + } + + public function getLocallyVerified(): string { + return $this->locallyVerified; + } } diff --git a/lib/private/Accounts/AccountPropertyCollection.php b/lib/private/Accounts/AccountPropertyCollection.php index eb92536a6a0..3aed76d8746 100644 --- a/lib/private/Accounts/AccountPropertyCollection.php +++ b/lib/private/Accounts/AccountPropertyCollection.php @@ -84,6 +84,15 @@ class AccountPropertyCollection implements IAccountPropertyCollection { return $this; } + public function getPropertyByValue(string $value): ?IAccountProperty { + foreach ($this->properties as $i => $property) { + if ($property->getValue() === $value) { + return $property; + } + } + return null; + } + public function removePropertyByValue(string $value): IAccountPropertyCollection { foreach ($this->properties as $i => $property) { if ($property->getValue() === $value) { diff --git a/lib/private/Server.php b/lib/private/Server.php index 03d6a4146ed..883bf3f5c24 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -135,6 +135,7 @@ use OC\Security\CSRF\TokenStorage\SessionStorage; use OC\Security\Hasher; use OC\Security\SecureRandom; use OC\Security\TrustedDomainHelper; +use OC\Security\VerificationToken\VerificationToken; use OC\Session\CryptoWrapper; use OC\Share20\ProviderFactory; use OC\Share20\ShareHelper; @@ -224,6 +225,7 @@ use OCP\Security\ICredentialsManager; use OCP\Security\ICrypto; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; +use OCP\Security\VerificationToken\IVerificationToken; use OCP\Share\IShareHelper; use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; @@ -795,6 +797,8 @@ class Server extends ServerContainer implements IServerContainer { /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('SecureRandom', \OCP\Security\ISecureRandom::class); + $this->registerAlias(IVerificationToken::class, VerificationToken::class); + $this->registerAlias(ICrypto::class, Crypto::class); /** @deprecated 19.0.0 */ $this->registerDeprecatedAlias('Crypto', ICrypto::class); diff --git a/lib/public/Accounts/IAccountProperty.php b/lib/public/Accounts/IAccountProperty.php index 20505f299dd..94712e50f7d 100644 --- a/lib/public/Accounts/IAccountProperty.php +++ b/lib/public/Accounts/IAccountProperty.php @@ -115,4 +115,24 @@ interface IAccountProperty extends \JsonSerializable { * @since 22.0.0 */ public function getVerificationData(): string; + + /** + * Set the instance-based verification status of a property + * + * @since 22.2.0 + * + * @param string $verified must be one of the verification constants of IAccountManager + * @return IAccountProperty + * @throws InvalidArgumentException + */ + public function setLocallyVerified(string $verified): IAccountProperty; + + /** + * Get the instance-based verification status of a property + * + * @since 22.2.0 + * + * @return string + */ + public function getLocallyVerified(): string; } diff --git a/lib/public/Accounts/IAccountPropertyCollection.php b/lib/public/Accounts/IAccountPropertyCollection.php index 779fb1299b4..6ee3fed0acb 100644 --- a/lib/public/Accounts/IAccountPropertyCollection.php +++ b/lib/public/Accounts/IAccountPropertyCollection.php @@ -89,4 +89,13 @@ interface IAccountPropertyCollection extends JsonSerializable { * @since 22.0.0 */ public function removePropertyByValue(string $value): IAccountPropertyCollection; + + /** + * retrieves a property identified by its value. null, if none was found. + * + * Returns only the first property if there are more with the same value. + * + * @since 22.2.0 + */ + public function getPropertyByValue(string $value): ?IAccountProperty; } diff --git a/tests/lib/Accounts/AccountManagerTest.php b/tests/lib/Accounts/AccountManagerTest.php index 8ed0e29d7ce..bf79d233131 100644 --- a/tests/lib/Accounts/AccountManagerTest.php +++ b/tests/lib/Accounts/AccountManagerTest.php @@ -25,9 +25,15 @@ use OC\Accounts\Account; use OC\Accounts\AccountManager; use OCP\Accounts\IAccountManager; use OCP\BackgroundJob\IJobList; +use OCP\Defaults; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IURLGenerator; use OCP\IUser; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Security\ICrypto; +use OCP\Security\VerificationToken\IVerificationToken; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -41,6 +47,18 @@ use Test\TestCase; * @package Test\Accounts */ class AccountManagerTest extends TestCase { + /** @var IVerificationToken|MockObject */ + protected $verificationToken; + /** @var IMailer|MockObject */ + protected $mailer; + /** @var ICrypto|MockObject */ + protected $crypto; + /** @var IURLGenerator|MockObject */ + protected $urlGenerator; + /** @var Defaults|MockObject */ + protected $defaults; + /** @var IFactory|MockObject */ + protected $l10nFactory; /** @var \OCP\IDBConnection */ private $connection; @@ -70,6 +88,12 @@ class AccountManagerTest extends TestCase { $this->config = $this->createMock(IConfig::class); $this->jobList = $this->createMock(IJobList::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->verificationToken = $this->createMock(IVerificationToken::class); + $this->mailer = $this->createMock(IMailer::class); + $this->defaults = $this->createMock(Defaults::class); + $this->l10nFactory = $this->createMock(IFactory::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->crypto = $this->createMock(ICrypto::class); $this->accountManager = new AccountManager( $this->connection, @@ -77,6 +101,12 @@ class AccountManagerTest extends TestCase { $this->eventDispatcher, $this->jobList, $this->logger, + $this->verificationToken, + $this->mailer, + $this->defaults, + $this->l10nFactory, + $this->urlGenerator, + $this->crypto ); } @@ -310,6 +340,12 @@ class AccountManagerTest extends TestCase { $this->eventDispatcher, $this->jobList, $this->logger, + $this->verificationToken, + $this->mailer, + $this->defaults, + $this->l10nFactory, + $this->urlGenerator, + $this->crypto ]) ->setMethods($mockedMethods) ->getMock(); |