]> source.dussan.org Git - nextcloud-server.git/commitdiff
enable the user to set a primary (notification) email address (backend)
authorArthur Schiwon <blizzz@arthur-schiwon.de>
Wed, 1 Sep 2021 12:04:57 +0000 (14:04 +0200)
committerArthur Schiwon <blizzz@arthur-schiwon.de>
Fri, 10 Sep 2021 11:22:26 +0000 (13:22 +0200)
- specific getters and setters on IUser and implementation
- new notify_email field in provisioning API

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
apps/provisioning_api/lib/Controller/AUserData.php
apps/provisioning_api/lib/Controller/UsersController.php
lib/private/Setup.php
lib/private/User/Manager.php
lib/private/User/User.php
lib/public/IUser.php
lib/public/IUserManager.php
tests/lib/User/UserTest.php

index e358d2820618699b14af0be905754b39d76f4187..83ad887be77377d89d42965e863f04d4c4a4fd0c 100644 (file)
@@ -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
                        ];
                }
index 114dfcb074ab6ad8daac353ec635e05031906986..8479100433b1478c9dfd86f595c662ab1d5a9db7 100644 (file)
@@ -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);
                                        }
                                }
index 8e7e6ae6aea19e62f96ed91feea3ddea40d92208..cb0d812c8855e0ab1b23c7e74812db659c06321e 100644 (file)
@@ -436,7 +436,7 @@ class Setup {
 
                        // Set email for admin
                        if (!empty($options['adminemail'])) {
-                               $config->setUserValue($user->getUID(), 'settings', 'email', $options['adminemail']);
+                               $user->setSystemEMailAddress($options['adminemail']);
                        }
                }
 
index 0d340ad356e67109e6452769c9bf995dfee90c94..bebb9c8c72c244b232ff2c183c79f87c3ac905b5 100644 (file)
@@ -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) {
index f17824f51b9aa16adccbfee4ebcca9a01995ee56..5fa1272f95cc811e2693fa69143d1d59ab774444 100644 (file)
  */
 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,24 +169,61 @@ 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,9 +431,23 @@ 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
         *
index 7e75704ed5bc096f4633f9d4a06533e975aef9dc..614748f6e28e0fa32bd1270d633355ae6ce1cf90 100644 (file)
@@ -27,6 +27,8 @@
  */
 namespace OCP;
 
+use InvalidArgumentException;
+
 /**
  * Interface IUser
  *
@@ -157,13 +159,42 @@ 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
         */
        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 22.2.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 22.2.0
+        */
+       public function getPrimaryEMailAddress(): ?string;
+
        /**
         * get the avatar image if it exists
         *
@@ -184,12 +215,42 @@ 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 22.2.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 22.2.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 22.2.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
index c6cad6f0549db187b5e464bc696b2f3754b92eee..e5c220af40c60f9963dc5a11bc7ed5a94f0e6a95 100644 (file)
@@ -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
index 2366bf4532103af58654f3a1998e9cfc8630276a..ad8b01555eadca996320564d32bb9ba8b4c16a4c 100644 (file)
@@ -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);