diff options
-rw-r--r-- | apps/provisioning_api/lib/Controller/AUserData.php | 18 | ||||
-rw-r--r-- | apps/provisioning_api/lib/Controller/UsersController.php | 66 | ||||
-rw-r--r-- | lib/private/Setup.php | 2 | ||||
-rw-r--r-- | lib/private/User/Manager.php | 1 | ||||
-rw-r--r-- | lib/private/User/User.php | 77 | ||||
-rw-r--r-- | lib/public/IUser.php | 63 | ||||
-rw-r--r-- | lib/public/IUserManager.php | 2 | ||||
-rw-r--r-- | tests/lib/User/UserTest.php | 5 |
8 files changed, 193 insertions, 41 deletions
diff --git a/apps/provisioning_api/lib/Controller/AUserData.php b/apps/provisioning_api/lib/Controller/AUserData.php index e358d282061..83ad887be77 100644 --- a/apps/provisioning_api/lib/Controller/AUserData.php +++ b/apps/provisioning_api/lib/Controller/AUserData.php @@ -54,6 +54,13 @@ use OCP\User\Backend\ISetPasswordBackend; abstract class AUserData extends OCSController { public const SCOPE_SUFFIX = 'Scope'; + public const USER_FIELD_DISPLAYNAME = 'display'; + public const USER_FIELD_LANGUAGE = 'language'; + public const USER_FIELD_LOCALE = 'locale'; + public const USER_FIELD_PASSWORD = 'password'; + public const USER_FIELD_QUOTA = 'quota'; + public const USER_FIELD_NOTIFICATION_EMAIL = 'notify_email'; + /** @var IUserManager */ protected $userManager; /** @var IConfig */ @@ -139,7 +146,7 @@ abstract class AUserData extends OCSController { $data['lastLogin'] = $targetUserObject->getLastLogin() * 1000; $data['backend'] = $targetUserObject->getBackendClassName(); $data['subadmin'] = $this->getUserSubAdminGroupsData($targetUserObject->getUID()); - $data['quota'] = $this->fillStorageInfo($targetUserObject->getUID()); + $data[self::USER_FIELD_QUOTA] = $this->fillStorageInfo($targetUserObject->getUID()); try { if ($includeScopes) { @@ -187,8 +194,9 @@ abstract class AUserData extends OCSController { } $data['groups'] = $gids; - $data['language'] = $this->l10nFactory->getUserLanguage($targetUserObject); - $data['locale'] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'locale'); + $data[self::USER_FIELD_LANGUAGE] = $this->l10nFactory->getUserLanguage($targetUserObject); + $data[self::USER_FIELD_LOCALE] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'locale'); + $data[self::USER_FIELD_NOTIFICATION_EMAIL] = $targetUserObject->getPrimaryEMailAddress(); $backend = $targetUserObject->getBackend(); $data['backendCapabilities'] = [ @@ -238,7 +246,7 @@ abstract class AUserData extends OCSController { 'used' => $storage['used'], 'total' => $storage['total'], 'relative' => $storage['relative'], - 'quota' => $storage['quota'], + self::USER_FIELD_QUOTA => $storage['quota'], ]; } catch (NotFoundException $ex) { // User fs is not setup yet @@ -251,7 +259,7 @@ abstract class AUserData extends OCSController { $quota = OC_Helper::computerFileSize($quota); } $data = [ - 'quota' => $quota !== false ? $quota : 'none', + self::USER_FIELD_QUOTA => $quota !== false ? $quota : 'none', 'used' => 0 ]; } diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php index aae34975c25..e91d234809c 100644 --- a/apps/provisioning_api/lib/Controller/UsersController.php +++ b/apps/provisioning_api/lib/Controller/UsersController.php @@ -42,6 +42,7 @@ declare(strict_types=1); */ namespace OCA\Provisioning_API\Controller; +use InvalidArgumentException; use libphonenumber\NumberParseException; use libphonenumber\PhoneNumber; use libphonenumber\PhoneNumberFormat; @@ -418,15 +419,15 @@ class UsersController extends AUserData { } if ($displayName !== '') { - $this->editUser($userid, 'display', $displayName); + $this->editUser($userid, self::USER_FIELD_DISPLAYNAME, $displayName); } if ($quota !== '') { - $this->editUser($userid, 'quota', $quota); + $this->editUser($userid, self::USER_FIELD_QUOTA, $quota); } if ($language !== '') { - $this->editUser($userid, 'language', $language); + $this->editUser($userid, self::USER_FIELD_LANGUAGE, $language); } // Send new user mail only if a mail is set @@ -466,7 +467,7 @@ class UsersController extends AUserData { ] ); throw $e; - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { $this->logger->error('Failed addUser attempt with invalid argument exeption.', [ 'app' => 'ocs_api', @@ -676,7 +677,7 @@ class UsersController extends AUserData { try { $targetProperty->setScope($value); $this->accountManager->updateAccount($userAccount); - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { throw new OCSException('', 102); } } else { @@ -717,7 +718,7 @@ class UsersController extends AUserData { if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) { if ($targetUser->getBackend() instanceof ISetDisplayNameBackend || $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)) { - $permittedFields[] = 'display'; + $permittedFields[] = self::USER_FIELD_DISPLAYNAME; $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME; } $permittedFields[] = IAccountManager::PROPERTY_EMAIL; @@ -728,15 +729,16 @@ class UsersController extends AUserData { $permittedFields[] = IAccountManager::COLLECTION_EMAIL; - $permittedFields[] = 'password'; + $permittedFields[] = self::USER_FIELD_PASSWORD; + $permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL; if ($this->config->getSystemValue('force_language', false) === false || $this->groupManager->isAdmin($currentLoggedInUser->getUID())) { - $permittedFields[] = 'language'; + $permittedFields[] = self::USER_FIELD_LANGUAGE; } if ($this->config->getSystemValue('force_locale', false) === false || $this->groupManager->isAdmin($currentLoggedInUser->getUID())) { - $permittedFields[] = 'locale'; + $permittedFields[] = self::USER_FIELD_LOCALE; } $permittedFields[] = IAccountManager::PROPERTY_PHONE; @@ -752,7 +754,7 @@ class UsersController extends AUserData { // If admin they can edit their own quota if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) { - $permittedFields[] = 'quota'; + $permittedFields[] = self::USER_FIELD_QUOTA; } } else { // Check if admin / subadmin @@ -762,19 +764,19 @@ class UsersController extends AUserData { // They have permissions over the user if ($targetUser->getBackend() instanceof ISetDisplayNameBackend || $targetUser->getBackend()->implementsActions(Backend::SET_DISPLAYNAME)) { - $permittedFields[] = 'display'; + $permittedFields[] = self::USER_FIELD_DISPLAYNAME; $permittedFields[] = IAccountManager::PROPERTY_DISPLAYNAME; } $permittedFields[] = IAccountManager::PROPERTY_EMAIL; $permittedFields[] = IAccountManager::COLLECTION_EMAIL; - $permittedFields[] = 'password'; - $permittedFields[] = 'language'; - $permittedFields[] = 'locale'; + $permittedFields[] = self::USER_FIELD_PASSWORD; + $permittedFields[] = self::USER_FIELD_LANGUAGE; + $permittedFields[] = self::USER_FIELD_LOCALE; $permittedFields[] = IAccountManager::PROPERTY_PHONE; $permittedFields[] = IAccountManager::PROPERTY_ADDRESS; $permittedFields[] = IAccountManager::PROPERTY_WEBSITE; $permittedFields[] = IAccountManager::PROPERTY_TWITTER; - $permittedFields[] = 'quota'; + $permittedFields[] = self::USER_FIELD_QUOTA; } else { // No rights throw new OCSException('', OCSController::RESPOND_NOT_FOUND); @@ -786,11 +788,11 @@ class UsersController extends AUserData { } // Process the edit switch ($key) { - case 'display': + case self::USER_FIELD_DISPLAYNAME: case IAccountManager::PROPERTY_DISPLAYNAME: $targetUser->setDisplayName($value); break; - case 'quota': + case self::USER_FIELD_QUOTA: $quota = $value; if ($quota !== 'none' && $quota !== 'default') { if (is_numeric($quota)) { @@ -820,7 +822,7 @@ class UsersController extends AUserData { } $targetUser->setQuota($quota); break; - case 'password': + case self::USER_FIELD_PASSWORD: try { if (!$targetUser->canChangePassword()) { throw new OCSException('Setting the password is not supported by the users backend', 103); @@ -830,19 +832,39 @@ class UsersController extends AUserData { throw new OCSException($e->getMessage(), 103); } break; - case 'language': + case self::USER_FIELD_LANGUAGE: $languagesCodes = $this->l10nFactory->findAvailableLanguages(); if (!in_array($value, $languagesCodes, true) && $value !== 'en') { throw new OCSException('Invalid language', 102); } $this->config->setUserValue($targetUser->getUID(), 'core', 'lang', $value); break; - case 'locale': + case self::USER_FIELD_LOCALE: if (!$this->l10nFactory->localeExists($value)) { throw new OCSException('Invalid locale', 102); } $this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value); break; + case self::USER_FIELD_NOTIFICATION_EMAIL: + $success = false; + if ($value === '' || filter_var($value, FILTER_VALIDATE_EMAIL)) { + try { + $targetUser->setPrimaryEMailAddress($value); + $success = true; + } catch (InvalidArgumentException $e) { + $this->logger->info( + 'Cannot set primary email, because provided address is not verified', + [ + 'app' => 'provisioning_api', + 'exception' => $e, + ] + ); + } + } + if (!$success) { + throw new OCSException('', 102); + } + break; case IAccountManager::PROPERTY_EMAIL: if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') { $targetUser->setEMailAddress($value); @@ -878,7 +900,7 @@ class UsersController extends AUserData { if ($userProperty->getName() === IAccountManager::PROPERTY_PHONE) { $this->knownUserService->deleteByContactUserId($targetUser->getUID()); } - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { throw new OCSException('Invalid ' . $e->getMessage(), 102); } } @@ -901,7 +923,7 @@ class UsersController extends AUserData { try { $userProperty->setScope($value); $this->accountManager->updateAccount($userAccount); - } catch (\InvalidArgumentException $e) { + } catch (InvalidArgumentException $e) { throw new OCSException('Invalid ' . $e->getMessage(), 102); } } diff --git a/lib/private/Setup.php b/lib/private/Setup.php index a4873e63aa9..c24d417f8cf 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -439,7 +439,7 @@ class Setup { // Set email for admin if (!empty($options['adminemail'])) { - $config->setUserValue($user->getUID(), 'settings', 'email', $options['adminemail']); + $user->setSystemEMailAddress($options['adminemail']); } } diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 1827be61a7a..3e30861f2a4 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -700,6 +700,7 @@ class Manager extends PublicEmitter implements IUserManager { * @since 9.1.0 */ public function getByEmail($email) { + // looking for 'email' only (and not primary_mail) is intentional $userIds = $this->config->getUsersForUserValueCaseInsensitive('settings', 'email', $email); $users = array_map(function ($uid) { diff --git a/lib/private/User/User.php b/lib/private/User/User.php index f17824f51b9..5fa1272f95c 100644 --- a/lib/private/User/User.php +++ b/lib/private/User/User.php @@ -34,10 +34,12 @@ */ namespace OC\User; +use InvalidArgumentException; use OC\Accounts\AccountManager; use OC\Avatar\AvatarManager; use OC\Hooks\Emitter; use OC_Helper; +use OCP\Accounts\IAccountManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\Group\Events\BeforeUserRemovedEvent; use OCP\Group\Events\UserRemovedEvent; @@ -55,6 +57,8 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; class User implements IUser { + /** @var IAccountManager */ + protected $accountManager; /** @var string */ private $uid; @@ -165,25 +169,62 @@ class User implements IUser { } /** - * set the email address of the user - * - * @param string|null $mailAddress - * @return void - * @since 9.0.0 + * @inheritDoc */ public function setEMailAddress($mailAddress) { - $oldMailAddress = $this->getEMailAddress(); + $this->setSystemEMailAddress($mailAddress); + } + + /** + * @inheritDoc + */ + public function setSystemEMailAddress(string $mailAddress): void { + $oldMailAddress = $this->getSystemEMailAddress(); + + if ($mailAddress === '') { + $this->config->deleteUserValue($this->uid, 'settings', 'email'); + } else { + $this->config->setUserValue($this->uid, 'settings', 'email', $mailAddress); + } + + $primaryAddress = $this->getPrimaryEMailAddress(); + if ($primaryAddress === $mailAddress) { + // on match no dedicated primary settings is necessary + $this->setPrimaryEMailAddress(''); + } + if ($oldMailAddress !== $mailAddress) { - if ($mailAddress === '') { - $this->config->deleteUserValue($this->uid, 'settings', 'email'); - } else { - $this->config->setUserValue($this->uid, 'settings', 'email', $mailAddress); - } $this->triggerChange('eMailAddress', $mailAddress, $oldMailAddress); } } /** + * @inheritDoc + */ + public function setPrimaryEMailAddress(string $mailAddress): void { + if ($mailAddress === '') { + $this->config->deleteUserValue($this->uid, 'settings', 'primary_email'); + return; + } + + $this->ensureAccountManager(); + $account = $this->accountManager->getAccount($this); + $property = $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL) + ->getPropertyByValue($mailAddress); + + if ($property === null || $property->getLocallyVerified() !== IAccountManager::VERIFIED) { + throw new InvalidArgumentException('Only verified emails can be set as primary'); + } + $this->config->setUserValue($this->uid, 'settings', 'primary_email', $mailAddress); + } + + private function ensureAccountManager() { + if (!$this->accountManager instanceof IAccountManager) { + $this->accountManager = \OC::$server->get(IAccountManager::class); + } + } + + /** * returns the timestamp of the user's last login or 0 if the user did never * login * @@ -390,10 +431,24 @@ class User implements IUser { * @since 9.0.0 */ public function getEMailAddress() { + return $this->getPrimaryEMailAddress() ?? $this->getSystemEMailAddress(); + } + + /** + * @inheritDoc + */ + public function getSystemEMailAddress(): ?string { return $this->config->getUserValue($this->uid, 'settings', 'email', null); } /** + * @inheritDoc + */ + public function getPrimaryEMailAddress(): ?string { + return $this->config->getUserValue($this->uid, 'settings', 'primary_email', null); + } + + /** * get the users' quota * * @return string diff --git a/lib/public/IUser.php b/lib/public/IUser.php index 7e75704ed5b..1a1d1e44d8a 100644 --- a/lib/public/IUser.php +++ b/lib/public/IUser.php @@ -27,6 +27,8 @@ */ namespace OCP; +use InvalidArgumentException; + /** * Interface IUser * @@ -157,7 +159,7 @@ interface IUser { public function setEnabled(bool $enabled = true); /** - * get the users email address + * get the user's email address * * @return string|null * @since 9.0.0 @@ -165,6 +167,35 @@ interface IUser { public function getEMailAddress(); /** + * get the user's system email address + * + * The system mail address may be read only and may be set from different + * sources like LDAP, SAML or simply the admin. + * + * Use this getter only when the system address is needed. For picking the + * proper address to e.g. send a mail to, use getEMailAddress(). + * + * @return string|null + * @since 23.0.0 + */ + public function getSystemEMailAddress(): ?string; + + /** + * get the user's preferred email address + * + * The primary mail address may be set be the user to specify a different + * email address where mails by Nextcloud are sent to. It is not necessarily + * set. + * + * Use this getter only when the primary address is needed. For picking the + * proper address to e.g. send a mail to, use getEMailAddress(). + * + * @return string|null + * @since 23.0.0 + */ + public function getPrimaryEMailAddress(): ?string; + + /** * get the avatar image if it exists * * @param int $size @@ -184,13 +215,43 @@ interface IUser { /** * set the email address of the user * + * It is an alias to setSystemEMailAddress() + * * @param string|null $mailAddress * @return void * @since 9.0.0 + * @deprecated 23.0.0 use setSystemEMailAddress() or setPrimaryEMailAddress() */ public function setEMailAddress($mailAddress); /** + * Set the system email address of the user + * + * This is supposed to be used when the email is set from different sources + * (i.e. other user backends, admin). + * + * @since 23.0.0 + */ + public function setSystemEMailAddress(string $mailAddress): void; + + /** + * Set the primary email address of the user. + * + * This method should be typically called when the user is changing their + * own primary address and is not allowed to change their system email. + * + * The mail address provided here must be already registered as an + * additional mail in the user account and also be verified locally. Also + * an empty string is allowed to delete this preference. + * + * @throws InvalidArgumentException when the provided email address does not + * satisfy constraints. + * + * @since 23.0.0 + */ + public function setPrimaryEMailAddress(string $mailAddress): void; + + /** * get the users' quota in human readable form. If a specific quota is not * set for the user, the default value is returned. If a default setting * was not set otherwise, it is return as 'none', i.e. quota is not limited. diff --git a/lib/public/IUserManager.php b/lib/public/IUserManager.php index c6cad6f0549..e5c220af40c 100644 --- a/lib/public/IUserManager.php +++ b/lib/public/IUserManager.php @@ -196,6 +196,8 @@ interface IUserManager { public function callForSeenUsers(\Closure $callback); /** + * returns all users having the provided email set as system email address + * * @param string $email * @return IUser[] * @since 9.1.0 diff --git a/tests/lib/User/UserTest.php b/tests/lib/User/UserTest.php index 2366bf45321..ad8b01555ea 100644 --- a/tests/lib/User/UserTest.php +++ b/tests/lib/User/UserTest.php @@ -676,11 +676,14 @@ class UserTest extends TestCase { $emitter->expects($this->never()) ->method('emit'); + $this->dispatcher->expects($this->never()) + ->method('dispatch'); + $config = $this->createMock(IConfig::class); $config->expects($this->any()) ->method('getUserValue') ->willReturn('foo@bar.com'); - $config->expects($this->never()) + $config->expects($this->any()) ->method('setUserValue'); $user = new User('foo', $backend, $this->dispatcher, $emitter, $config); |