diff options
Diffstat (limited to 'apps/user_ldap/lib/User')
-rw-r--r-- | apps/user_ldap/lib/User/DeletedUsersIndex.php | 84 | ||||
-rw-r--r-- | apps/user_ldap/lib/User/Manager.php | 162 | ||||
-rw-r--r-- | apps/user_ldap/lib/User/OfflineUser.php | 64 | ||||
-rw-r--r-- | apps/user_ldap/lib/User/User.php | 508 |
4 files changed, 424 insertions, 394 deletions
diff --git a/apps/user_ldap/lib/User/DeletedUsersIndex.php b/apps/user_ldap/lib/User/DeletedUsersIndex.php index 1e057987eef..f57f71a9d47 100644 --- a/apps/user_ldap/lib/User/DeletedUsersIndex.php +++ b/apps/user_ldap/lib/User/DeletedUsersIndex.php @@ -1,29 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\User_LDAP\User; use OCA\User_LDAP\Mapping\UserMapping; +use OCP\IConfig; +use OCP\PreConditionNotMetException; use OCP\Share\IManager; /** @@ -31,40 +16,30 @@ use OCP\Share\IManager; * @package OCA\User_LDAP */ class DeletedUsersIndex { - /** - * @var \OCP\IConfig $config - */ - protected $config; - - /** - * @var \OCA\User_LDAP\Mapping\UserMapping $mapping - */ - protected $mapping; + protected ?array $deletedUsers = null; - /** - * @var array $deletedUsers - */ - protected $deletedUsers; - /** @var IManager */ - private $shareManager; - - public function __construct(\OCP\IConfig $config, UserMapping $mapping, IManager $shareManager) { - $this->config = $config; - $this->mapping = $mapping; - $this->shareManager = $shareManager; + public function __construct( + protected IConfig $config, + protected UserMapping $mapping, + private IManager $shareManager, + ) { } /** * reads LDAP users marked as deleted from the database - * @return \OCA\User_LDAP\User\OfflineUser[] + * @return OfflineUser[] */ - private function fetchDeletedUsers() { - $deletedUsers = $this->config->getUsersForUserValue( - 'user_ldap', 'isDeleted', '1'); + private function fetchDeletedUsers(): array { + $deletedUsers = $this->config->getUsersForUserValue('user_ldap', 'isDeleted', '1'); $userObjects = []; foreach ($deletedUsers as $user) { - $userObjects[] = new OfflineUser($user, $this->config, $this->mapping, $this->shareManager); + $userObject = new OfflineUser($user, $this->config, $this->mapping, $this->shareManager); + if ($userObject->getLastLogin() > $userObject->getDetectedOn()) { + $userObject->unmark(); + } else { + $userObjects[] = $userObject; + } } $this->deletedUsers = $userObjects; @@ -73,9 +48,9 @@ class DeletedUsersIndex { /** * returns all LDAP users that are marked as deleted - * @return \OCA\User_LDAP\User\OfflineUser[] + * @return OfflineUser[] */ - public function getUsers() { + public function getUsers(): array { if (is_array($this->deletedUsers)) { return $this->deletedUsers; } @@ -84,9 +59,8 @@ class DeletedUsersIndex { /** * whether at least one user was detected as deleted - * @return bool */ - public function hasUsers() { + public function hasUsers(): bool { if (!is_array($this->deletedUsers)) { $this->fetchDeletedUsers(); } @@ -96,12 +70,10 @@ class DeletedUsersIndex { /** * marks a user as deleted * - * @param string $ocName - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ - public function markUser($ocName) { - $curValue = $this->config->getUserValue($ocName, 'user_ldap', 'isDeleted', '0'); - if ($curValue === '1') { + public function markUser(string $ocName): void { + if ($this->isUserMarked($ocName)) { // the user is already marked, do not write to DB again return; } @@ -109,4 +81,8 @@ class DeletedUsersIndex { $this->config->setUserValue($ocName, 'user_ldap', 'foundDeleted', (string)time()); $this->deletedUsers = null; } + + public function isUserMarked(string $ocName): bool { + return ($this->config->getUserValue($ocName, 'user_ldap', 'isDeleted', '0') === '1'); + } } diff --git a/apps/user_ldap/lib/User/Manager.php b/apps/user_ldap/lib/User/Manager.php index e752b113e3f..88a001dd965 100644 --- a/apps/user_ldap/lib/User/Manager.php +++ b/apps/user_ldap/lib/User/Manager.php @@ -1,36 +1,14 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Roger Szabo <roger.szabo@web.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\User_LDAP\User; -use OC\Cache\CappedMemoryCache; use OCA\User_LDAP\Access; -use OCA\User_LDAP\FilesystemHelper; +use OCP\Cache\CappedMemoryCache; use OCP\IAvatarManager; use OCP\IConfig; use OCP\IDBConnection; @@ -47,64 +25,24 @@ use Psr\Log\LoggerInterface; * cache */ class Manager { - /** @var Access */ - protected $access; - - /** @var IConfig */ - protected $ocConfig; - - /** @var IDBConnection */ - protected $db; - - /** @var IUserManager */ - protected $userManager; - - /** @var INotificationManager */ - protected $notificationManager; - - /** @var FilesystemHelper */ - protected $ocFilesystem; - - /** @var LoggerInterface */ - protected $logger; - - /** @var Image */ - protected $image; - - /** @param \OCP\IAvatarManager */ - protected $avatarManager; - - /** - * @var CappedMemoryCache $usersByDN - */ - protected $usersByDN; - /** - * @var CappedMemoryCache $usersByUid - */ - protected $usersByUid; - /** @var IManager */ - private $shareManager; + protected ?Access $access = null; + protected IDBConnection $db; + /** @var CappedMemoryCache<User> $usersByDN */ + protected CappedMemoryCache $usersByDN; + /** @var CappedMemoryCache<User> $usersByUid */ + protected CappedMemoryCache $usersByUid; public function __construct( - IConfig $ocConfig, - FilesystemHelper $ocFilesystem, - LoggerInterface $logger, - IAvatarManager $avatarManager, - Image $image, - IUserManager $userManager, - INotificationManager $notificationManager, - IManager $shareManager + protected IConfig $ocConfig, + protected LoggerInterface $logger, + protected IAvatarManager $avatarManager, + protected Image $image, + protected IUserManager $userManager, + protected INotificationManager $notificationManager, + private IManager $shareManager, ) { - $this->ocConfig = $ocConfig; - $this->ocFilesystem = $ocFilesystem; - $this->logger = $logger; - $this->avatarManager = $avatarManager; - $this->image = $image; - $this->userManager = $userManager; - $this->notificationManager = $notificationManager; $this->usersByDN = new CappedMemoryCache(); $this->usersByUid = new CappedMemoryCache(); - $this->shareManager = $shareManager; } /** @@ -121,12 +59,12 @@ class Manager { * property array * @param string $dn the DN of the user * @param string $uid the internal (owncloud) username - * @return \OCA\User_LDAP\User\User + * @return User */ private function createAndCache($dn, $uid) { $this->checkAccess(); $user = new User($uid, $dn, $this->access, $this->ocConfig, - $this->ocFilesystem, clone $this->image, $this->logger, + clone $this->image, $this->logger, $this->avatarManager, $this->userManager, $this->notificationManager); $this->usersByDN[$dn] = $user; @@ -150,6 +88,7 @@ class Manager { /** * @brief checks whether the Access instance has been set * @throws \Exception if Access has not been set + * @psalm-assert !null $this->access * @return null */ private function checkAccess() { @@ -163,22 +102,34 @@ class Manager { * email, displayname, or others. * * @param bool $minimal - optional, set to true to skip attributes with big - * payload + * payload * @return string[] */ public function getAttributes($minimal = false) { $baseAttributes = array_merge(Access::UUID_ATTRIBUTES, ['dn', 'uid', 'samaccountname', 'memberof']); $attributes = [ $this->access->getConnection()->ldapExpertUUIDUserAttr, + $this->access->getConnection()->ldapExpertUsernameAttr, $this->access->getConnection()->ldapQuotaAttribute, $this->access->getConnection()->ldapEmailAttribute, $this->access->getConnection()->ldapUserDisplayName, $this->access->getConnection()->ldapUserDisplayName2, $this->access->getConnection()->ldapExtStorageHomeAttribute, + $this->access->getConnection()->ldapAttributePhone, + $this->access->getConnection()->ldapAttributeWebsite, + $this->access->getConnection()->ldapAttributeAddress, + $this->access->getConnection()->ldapAttributeTwitter, + $this->access->getConnection()->ldapAttributeFediverse, + $this->access->getConnection()->ldapAttributeOrganisation, + $this->access->getConnection()->ldapAttributeRole, + $this->access->getConnection()->ldapAttributeHeadline, + $this->access->getConnection()->ldapAttributeBiography, + $this->access->getConnection()->ldapAttributeBirthDate, + $this->access->getConnection()->ldapAttributePronouns, ]; $homeRule = (string)$this->access->getConnection()->homeFolderNamingRule; - if (strpos($homeRule, 'attr:') === 0) { + if (str_starts_with($homeRule, 'attr:')) { $attributes[] = substr($homeRule, strlen('attr:')); } @@ -220,7 +171,7 @@ class Manager { /** * creates and returns an instance of OfflineUser for the specified user * @param string $id - * @return \OCA\User_LDAP\User\OfflineUser + * @return OfflineUser */ public function getDeletedUser($id) { return new OfflineUser( @@ -232,9 +183,9 @@ class Manager { } /** - * @brief returns a User object by it's Nextcloud username + * @brief returns a User object by its Nextcloud username * @param string $id the DN or username of the user - * @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null + * @return User|OfflineUser|null */ protected function createInstancyByUserName($id) { //most likely a uid. Check whether it is a deleted user @@ -249,9 +200,9 @@ class Manager { } /** - * @brief returns a User object by it's DN or Nextcloud username + * @brief returns a User object by its DN or Nextcloud username * @param string $id the DN or username of the user - * @return \OCA\User_LDAP\User\User|\OCA\User_LDAP\User\OfflineUser|null + * @return User|OfflineUser|null * @throws \Exception when connection could not be established */ public function get($id) { @@ -271,4 +222,37 @@ class Manager { return $this->createInstancyByUserName($id); } + + /** + * @brief Checks whether a User object by its DN or Nextcloud username exists + * @param string $id the DN or username of the user + * @throws \Exception when connection could not be established + */ + public function exists($id): bool { + $this->checkAccess(); + $this->logger->debug('Checking if {id} exists', ['id' => $id]); + if (isset($this->usersByDN[$id])) { + return true; + } elseif (isset($this->usersByUid[$id])) { + return true; + } + + if ($this->access->stringResemblesDN($id)) { + $this->logger->debug('{id} looks like a dn', ['id' => $id]); + $uid = $this->access->dn2username($id); + if ($uid !== false) { + return true; + } + } + + // Most likely a uid. Check whether it is a deleted user + if ($this->isDeletedUser($id)) { + return true; + } + $dn = $this->access->username2dn($id); + if ($dn !== false) { + return true; + } + return false; + } } diff --git a/apps/user_ldap/lib/User/OfflineUser.php b/apps/user_ldap/lib/User/OfflineUser.php index 4adf5302bfe..ecaab7188ba 100644 --- a/apps/user_ldap/lib/User/OfflineUser.php +++ b/apps/user_ldap/lib/User/OfflineUser.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\User_LDAP\User; @@ -33,10 +15,6 @@ use OCP\Share\IShare; class OfflineUser { /** - * @var string $ocName - */ - protected $ocName; - /** * @var string $dn */ protected $dn; @@ -60,6 +38,7 @@ class OfflineUser { * @var string $foundDeleted the timestamp when the user was detected as unavailable */ protected $foundDeleted; + protected ?string $extStorageHome = null; /** * @var string $email */ @@ -69,30 +48,19 @@ class OfflineUser { */ protected $hasActiveShares; /** - * @var IConfig $config - */ - protected $config; - /** * @var IDBConnection $db */ protected $db; + /** - * @var \OCA\User_LDAP\Mapping\UserMapping + * @param string $ocName */ - protected $mapping; - /** @var IManager */ - private $shareManager; - public function __construct( - $ocName, - IConfig $config, - UserMapping $mapping, - IManager $shareManager + protected $ocName, + protected IConfig $config, + protected UserMapping $mapping, + private IManager $shareManager, ) { - $this->ocName = $ocName; - $this->config = $config; - $this->mapping = $mapping; - $this->shareManager = $shareManager; } /** @@ -207,6 +175,13 @@ class OfflineUser { return (int)$this->foundDeleted; } + public function getExtStorageHome(): string { + if ($this->extStorageHome === null) { + $this->fetchDetails(); + } + return (string)$this->extStorageHome; + } + /** * getter for having active shares * @return bool @@ -227,6 +202,7 @@ class OfflineUser { 'uid' => 'user_ldap', 'homePath' => 'user_ldap', 'foundDeleted' => 'user_ldap', + 'extStorageHome' => 'user_ldap', 'email' => 'settings', 'lastLogin' => 'login', ]; @@ -244,7 +220,7 @@ class OfflineUser { $shareConstants = $shareInterface->getConstants(); foreach ($shareConstants as $constantName => $constantValue) { - if (strpos($constantName, 'TYPE_') !== 0 + if (!str_starts_with($constantName, 'TYPE_') || $constantValue === IShare::TYPE_USERGROUP ) { continue; diff --git a/apps/user_ldap/lib/User/User.php b/apps/user_ldap/lib/User/User.php index 15894ce04b7..8f97ec1701f 100644 --- a/apps/user_ldap/lib/User/User.php +++ b/apps/user_ldap/lib/User/User.php @@ -1,47 +1,30 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Juan Pablo Villafáñez <jvillafanez@solidgear.es> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Philipp Staiger <philipp@staiger.it> - * @author Roger Szabo <roger.szabo@web.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\User_LDAP\User; +use InvalidArgumentException; +use OC\Accounts\AccountManager; use OCA\User_LDAP\Access; use OCA\User_LDAP\Connection; use OCA\User_LDAP\Exceptions\AttributeNotSet; -use OCA\User_LDAP\FilesystemHelper; +use OCA\User_LDAP\Service\BirthdateParserService; +use OCP\Accounts\IAccountManager; +use OCP\Accounts\PropertyDoesNotExistException; use OCP\IAvatarManager; use OCP\IConfig; -use OCP\ILogger; use OCP\Image; +use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\Notification\IManager as INotificationManager; +use OCP\PreConditionNotMetException; +use OCP\Server; +use OCP\Util; use Psr\Log\LoggerInterface; /** @@ -50,102 +33,51 @@ use Psr\Log\LoggerInterface; * represents an LDAP user, gets and holds user-specific information from LDAP */ class User { + protected Connection $connection; /** - * @var Access - */ - protected $access; - /** - * @var Connection - */ - protected $connection; - /** - * @var IConfig - */ - protected $config; - /** - * @var FilesystemHelper - */ - protected $fs; - /** - * @var Image - */ - protected $image; - /** - * @var LoggerInterface - */ - protected $logger; - /** - * @var IAvatarManager - */ - protected $avatarManager; - /** - * @var IUserManager + * @var array<string,1> */ - protected $userManager; - /** - * @var INotificationManager - */ - protected $notificationManager; - /** - * @var string - */ - protected $dn; - /** - * @var string - */ - protected $uid; - /** - * @var string[] - */ - protected $refreshedFeatures = []; - /** - * @var string - */ - protected $avatarImage; + protected array $refreshedFeatures = []; + protected string|false|null $avatarImage = null; + + protected BirthdateParserService $birthdateParser; /** * DB config keys for user preferences + * @var string */ public const USER_PREFKEY_FIRSTLOGIN = 'firstLoginAccomplished'; /** * @brief constructor, make sure the subclasses call this one! - * @param string $username the internal username - * @param string $dn the LDAP DN */ - public function __construct($username, $dn, Access $access, - IConfig $config, FilesystemHelper $fs, Image $image, - LoggerInterface $logger, IAvatarManager $avatarManager, IUserManager $userManager, - INotificationManager $notificationManager) { - if ($username === null) { - $logger->error("uid for '$dn' must not be null!", ['app' => 'user_ldap']); - throw new \InvalidArgumentException('uid must not be null!'); - } elseif ($username === '') { + public function __construct( + protected string $uid, + protected string $dn, + protected Access $access, + protected IConfig $config, + protected Image $image, + protected LoggerInterface $logger, + protected IAvatarManager $avatarManager, + protected IUserManager $userManager, + protected INotificationManager $notificationManager, + ) { + if ($uid === '') { $logger->error("uid for '$dn' must not be an empty string", ['app' => 'user_ldap']); throw new \InvalidArgumentException('uid must not be an empty string!'); } + $this->connection = $this->access->getConnection(); + $this->birthdateParser = new BirthdateParserService(); - $this->access = $access; - $this->connection = $access->getConnection(); - $this->config = $config; - $this->fs = $fs; - $this->dn = $dn; - $this->uid = $username; - $this->image = $image; - $this->logger = $logger; - $this->avatarManager = $avatarManager; - $this->userManager = $userManager; - $this->notificationManager = $notificationManager; - - \OCP\Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry'); + Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry'); } /** * marks a user as deleted * - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ - public function markUser() { + public function markUser(): void { $curValue = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '0'); if ($curValue === '1') { // the user is already marked, do not write to DB again @@ -159,7 +91,7 @@ class User { * processes results from LDAP for attributes as returned by getAttributesToRead() * @param array $ldapEntry the user entry as retrieved from LDAP */ - public function processAttributes($ldapEntry) { + public function processAttributes(array $ldapEntry): void { //Quota $attr = strtolower($this->connection->ldapQuotaAttribute); if (isset($ldapEntry[$attr])) { @@ -196,7 +128,14 @@ class User { //change event that will trigger fetching the display name again $attr = strtolower($this->connection->ldapEmailAttribute); if (isset($ldapEntry[$attr])) { - $this->updateEmail($ldapEntry[$attr][0]); + $mailValue = 0; + for ($x = 0; $x < count($ldapEntry[$attr]); $x++) { + if (filter_var($ldapEntry[$attr][$x], FILTER_VALIDATE_EMAIL)) { + $mailValue = $x; + break; + } + } + $this->updateEmail($ldapEntry[$attr][$mailValue]); } unset($attr); @@ -208,7 +147,7 @@ class User { } //homePath - if (strpos($this->connection->homeFolderNamingRule, 'attr:') === 0) { + if (str_starts_with($this->connection->homeFolderNamingRule, 'attr:')) { $attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:'))); if (isset($ldapEntry[$attr])) { $this->access->cacheUserHome( @@ -217,7 +156,7 @@ class User { } //memberOf groups - $cacheKey = 'getMemberOf'.$this->getUsername(); + $cacheKey = 'getMemberOf' . $this->getUsername(); $groups = false; if (isset($ldapEntry['memberof'])) { $groups = $ldapEntry['memberof']; @@ -231,6 +170,134 @@ class User { } unset($attr); + // check for cached profile data + $username = $this->getUsername(); // buffer variable, to save resource + $cacheKey = 'getUserProfile-' . $username; + $profileCached = $this->connection->getFromCache($cacheKey); + // honoring profile disabled in config.php and check if user profile was refreshed + if ($this->config->getSystemValueBool('profile.enabled', true) + && ($profileCached === null) // no cache or TTL not expired + && !$this->wasRefreshed('profile')) { + // check current data + $profileValues = []; + //User Profile Field - Phone number + $attr = strtolower($this->connection->ldapAttributePhone); + if (!empty($attr)) { // attribute configured + $profileValues[IAccountManager::PROPERTY_PHONE] + = $ldapEntry[$attr][0] ?? ''; + } + //User Profile Field - website + $attr = strtolower($this->connection->ldapAttributeWebsite); + if (isset($ldapEntry[$attr])) { + $cutPosition = strpos($ldapEntry[$attr][0], ' '); + if ($cutPosition) { + // drop appended label + $profileValues[IAccountManager::PROPERTY_WEBSITE] + = substr($ldapEntry[$attr][0], 0, $cutPosition); + } else { + $profileValues[IAccountManager::PROPERTY_WEBSITE] + = $ldapEntry[$attr][0]; + } + } elseif (!empty($attr)) { // configured, but not defined + $profileValues[IAccountManager::PROPERTY_WEBSITE] = ''; + } + //User Profile Field - Address + $attr = strtolower($this->connection->ldapAttributeAddress); + if (isset($ldapEntry[$attr])) { + if (str_contains($ldapEntry[$attr][0], '$')) { + // basic format conversion from postalAddress syntax to commata delimited + $profileValues[IAccountManager::PROPERTY_ADDRESS] + = str_replace('$', ', ', $ldapEntry[$attr][0]); + } else { + $profileValues[IAccountManager::PROPERTY_ADDRESS] + = $ldapEntry[$attr][0]; + } + } elseif (!empty($attr)) { // configured, but not defined + $profileValues[IAccountManager::PROPERTY_ADDRESS] = ''; + } + //User Profile Field - Twitter + $attr = strtolower($this->connection->ldapAttributeTwitter); + if (!empty($attr)) { + $profileValues[IAccountManager::PROPERTY_TWITTER] + = $ldapEntry[$attr][0] ?? ''; + } + //User Profile Field - fediverse + $attr = strtolower($this->connection->ldapAttributeFediverse); + if (!empty($attr)) { + $profileValues[IAccountManager::PROPERTY_FEDIVERSE] + = $ldapEntry[$attr][0] ?? ''; + } + //User Profile Field - organisation + $attr = strtolower($this->connection->ldapAttributeOrganisation); + if (!empty($attr)) { + $profileValues[IAccountManager::PROPERTY_ORGANISATION] + = $ldapEntry[$attr][0] ?? ''; + } + //User Profile Field - role + $attr = strtolower($this->connection->ldapAttributeRole); + if (!empty($attr)) { + $profileValues[IAccountManager::PROPERTY_ROLE] + = $ldapEntry[$attr][0] ?? ''; + } + //User Profile Field - headline + $attr = strtolower($this->connection->ldapAttributeHeadline); + if (!empty($attr)) { + $profileValues[IAccountManager::PROPERTY_HEADLINE] + = $ldapEntry[$attr][0] ?? ''; + } + //User Profile Field - biography + $attr = strtolower($this->connection->ldapAttributeBiography); + if (isset($ldapEntry[$attr])) { + if (str_contains($ldapEntry[$attr][0], '\r')) { + // convert line endings + $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] + = str_replace(["\r\n","\r"], "\n", $ldapEntry[$attr][0]); + } else { + $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] + = $ldapEntry[$attr][0]; + } + } elseif (!empty($attr)) { // configured, but not defined + $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] = ''; + } + //User Profile Field - birthday + $attr = strtolower($this->connection->ldapAttributeBirthDate); + if (!empty($attr) && !empty($ldapEntry[$attr][0])) { + $value = $ldapEntry[$attr][0]; + try { + $birthdate = $this->birthdateParser->parseBirthdate($value); + $profileValues[IAccountManager::PROPERTY_BIRTHDATE] + = $birthdate->format('Y-m-d'); + } catch (InvalidArgumentException $e) { + // Invalid date -> just skip the property + $this->logger->info("Failed to parse user's birthdate from LDAP: $value", [ + 'exception' => $e, + 'userId' => $username, + ]); + } + } + //User Profile Field - pronouns + $attr = strtolower($this->connection->ldapAttributePronouns); + if (!empty($attr)) { + $profileValues[IAccountManager::PROPERTY_PRONOUNS] + = $ldapEntry[$attr][0] ?? ''; + } + // check for changed data and cache just for TTL checking + $checksum = hash('sha256', json_encode($profileValues)); + $this->connection->writeToCache($cacheKey, $checksum // write array to cache. is waste of cache space + , null); // use ldapCacheTTL from configuration + // Update user profile + if ($this->config->getUserValue($username, 'user_ldap', 'lastProfileChecksum', null) !== $checksum) { + $this->config->setUserValue($username, 'user_ldap', 'lastProfileChecksum', $checksum); + $this->updateProfile($profileValues); + $this->logger->info("updated profile uid=$username", ['app' => 'user_ldap']); + } else { + $this->logger->debug('profile data from LDAP unchanged', ['app' => 'user_ldap', 'uid' => $username]); + } + unset($attr); + } elseif ($profileCached !== null) { // message delayed, to declutter log + $this->logger->debug('skipping profile check, while cached data exist', ['app' => 'user_ldap', 'uid' => $username]); + } + //Avatar /** @var Connection $connection */ $connection = $this->access->getConnection(); @@ -238,11 +305,7 @@ class User { foreach ($attributes as $attribute) { if (isset($ldapEntry[$attribute])) { $this->avatarImage = $ldapEntry[$attribute][0]; - // the call to the method that saves the avatar in the file - // system must be postponed after the login. It is to ensure - // external mounts are mounted properly (e.g. with login - // credentials from the session). - \OCP\Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin'); + $this->updateAvatar(); break; } } @@ -266,20 +329,22 @@ class User { /** * returns the home directory of the user if specified by LDAP settings - * @param ?string $valueFromLDAP - * @return false|string * @throws \Exception */ - public function getHomePath($valueFromLDAP = null) { + public function getHomePath(?string $valueFromLDAP = null): string|false { $path = (string)$valueFromLDAP; $attr = null; if (is_null($valueFromLDAP) - && strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0 + && str_starts_with($this->access->connection->homeFolderNamingRule, 'attr:') && $this->access->connection->homeFolderNamingRule !== 'attr:') { $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:')); - $homedir = $this->access->readAttribute($this->access->username2dn($this->getUsername()), $attr); - if ($homedir && isset($homedir[0])) { + $dn = $this->access->username2dn($this->getUsername()); + if ($dn === false) { + return false; + } + $homedir = $this->access->readAttribute($dn, $attr); + if ($homedir !== false && isset($homedir[0])) { $path = $homedir[0]; } } @@ -287,12 +352,12 @@ class User { if ($path !== '') { //if attribute's value is an absolute path take this, otherwise append it to data dir //check for / at the beginning or pattern c:\ resp. c:/ - if ('/' !== $path[0] - && !(3 < strlen($path) && ctype_alpha($path[0]) - && $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2])) + if ($path[0] !== '/' + && !(strlen($path) > 3 && ctype_alpha($path[0]) + && $path[1] === ':' && ($path[2] === '\\' || $path[2] === '/')) ) { $path = $this->config->getSystemValue('datadirectory', - \OC::$SERVERROOT.'/data') . '/' . $path; + \OC::$SERVERROOT . '/data') . '/' . $path; } //we need it to store it in the DB as well in case a user gets //deleted so we can clean up afterwards @@ -303,7 +368,7 @@ class User { } if (!is_null($attr) - && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true) + && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', 'true') ) { // a naming rule attribute is defined, but it doesn't exist for that LDAP user throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername()); @@ -314,8 +379,8 @@ class User { return false; } - public function getMemberOfGroups() { - $cacheKey = 'getMemberOf'.$this->getUsername(); + public function getMemberOfGroups(): array|false { + $cacheKey = 'getMemberOf' . $this->getUsername(); $memberOfGroups = $this->connection->getFromCache($cacheKey); if (!is_null($memberOfGroups)) { return $memberOfGroups; @@ -327,9 +392,9 @@ class User { /** * @brief reads the image from LDAP that shall be used as Avatar - * @return string data (provided by LDAP) | false + * @return string|false data (provided by LDAP) */ - public function getAvatarImage() { + public function getAvatarImage(): string|false { if (!is_null($this->avatarImage)) { return $this->avatarImage; } @@ -340,7 +405,7 @@ class User { $attributes = $connection->resolveRule('avatar'); foreach ($attributes as $attribute) { $result = $this->access->readAttribute($this->dn, $attribute); - if ($result !== false && is_array($result) && isset($result[0])) { + if ($result !== false && isset($result[0])) { $this->avatarImage = $result[0]; break; } @@ -351,20 +416,16 @@ class User { /** * @brief marks the user as having logged in at least once - * @return null */ - public function markLogin() { + public function markLogin(): void { $this->config->setUserValue( - $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, 1); + $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, '1'); } /** * Stores a key-value pair in relation to this user - * - * @param string $key - * @param string $value */ - private function store($key, $value) { + private function store(string $key, string $value): void { $this->config->setUserValue($this->uid, 'user_ldap', $key, $value); } @@ -372,12 +433,9 @@ class User { * Composes the display name and stores it in the database. The final * display name is returned. * - * @param string $displayName - * @param string $displayName2 * @return string the effective display name */ - public function composeAndStoreDisplayName($displayName, $displayName2 = '') { - $displayName2 = (string)$displayName2; + public function composeAndStoreDisplayName(string $displayName, string $displayName2 = ''): string { if ($displayName2 !== '') { $displayName .= ' (' . $displayName2 . ')'; } @@ -396,9 +454,8 @@ class User { /** * Stores the LDAP Username in the Database - * @param string $userName */ - public function storeLDAPUserName($userName) { + public function storeLDAPUserName(string $userName): void { $this->store('uid', $userName); } @@ -406,10 +463,9 @@ class User { * @brief checks whether an update method specified by feature was run * already. If not, it will marked like this, because it is expected that * the method will be run, when false is returned. - * @param string $feature email | quota | avatar (can be extended) - * @return bool + * @param string $feature email | quota | avatar | profile (can be extended) */ - private function wasRefreshed($feature) { + private function wasRefreshed(string $feature): bool { if (isset($this->refreshedFeatures[$feature])) { return true; } @@ -419,10 +475,9 @@ class User { /** * fetches the email from LDAP and stores it as Nextcloud user value - * @param string $valueFromLDAP if known, to save an LDAP read request - * @return null + * @param ?string $valueFromLDAP if known, to save an LDAP read request */ - public function updateEmail($valueFromLDAP = null) { + public function updateEmail(?string $valueFromLDAP = null): void { if ($this->wasRefreshed('email')) { return; } @@ -441,7 +496,7 @@ class User { if (!is_null($user)) { $currentEmail = (string)$user->getSystemEMailAddress(); if ($currentEmail !== $email) { - $user->setEMailAddress($email); + $user->setSystemEMailAddress($email); } } } @@ -460,14 +515,13 @@ class User { * fetch all the user's attributes in one call and use the fetched values in this function. * The expected value for that parameter is a string describing the quota for the user. Valid * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in - * bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info) + * bytes), '1234 MB' (quota in MB - check the \OCP\Util::computerFileSize method for more info) * * fetches the quota from LDAP and stores it as Nextcloud user value * @param ?string $valueFromLDAP the quota attribute's value can be passed, - * to save the readAttribute request - * @return void + * to save the readAttribute request */ - public function updateQuota($valueFromLDAP = null) { + public function updateQuota(?string $valueFromLDAP = null): void { if ($this->wasRefreshed('quota')) { return; } @@ -481,7 +535,7 @@ class User { $quota = false; if (is_null($valueFromLDAP) && $quotaAttribute !== '') { $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute); - if ($aQuota && (count($aQuota) > 0) && $this->verifyQuotaValue($aQuota[0])) { + if ($aQuota !== false && isset($aQuota[0]) && $this->verifyQuotaValue($aQuota[0])) { $quota = $aQuota[0]; } elseif (is_array($aQuota) && isset($aQuota[0])) { $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ['app' => 'user_ldap']); @@ -489,7 +543,7 @@ class User { } elseif (!is_null($valueFromLDAP) && $this->verifyQuotaValue($valueFromLDAP)) { $quota = $valueFromLDAP; } else { - $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ['app' => 'user_ldap']); + $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . ($valueFromLDAP ?? '') . ']', ['app' => 'user_ldap']); } if ($quota === false && $this->verifyQuotaValue($defaultQuota)) { @@ -508,26 +562,65 @@ class User { } } - private function verifyQuotaValue(string $quotaValue) { - return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false; + private function verifyQuotaValue(string $quotaValue): bool { + return $quotaValue === 'none' || $quotaValue === 'default' || Util::computerFileSize($quotaValue) !== false; } /** - * called by a post_login hook to save the avatar picture + * takes values from LDAP and stores it as Nextcloud user profile value * - * @param array $params + * @param array $profileValues associative array of property keys and values from LDAP */ - public function updateAvatarPostLogin($params) { - if (isset($params['uid']) && $params['uid'] === $this->getUsername()) { - $this->updateAvatar(); + private function updateProfile(array $profileValues): void { + // check if given array is empty + if (empty($profileValues)) { + return; // okay, nothing to do + } + // fetch/prepare user + $user = $this->userManager->get($this->uid); + if (is_null($user)) { + $this->logger->error('could not get user for uid=' . $this->uid . '', ['app' => 'user_ldap']); + return; + } + // prepare AccountManager and Account + $accountManager = Server::get(IAccountManager::class); + $account = $accountManager->getAccount($user); // get Account + $defaultScopes = array_merge(AccountManager::DEFAULT_SCOPES, + $this->config->getSystemValue('account_manager.default_property_scope', [])); + // loop through the properties and handle them + foreach ($profileValues as $property => $valueFromLDAP) { + // check and update profile properties + $value = (is_array($valueFromLDAP) ? $valueFromLDAP[0] : $valueFromLDAP); // take ONLY the first value, if multiple values specified + try { + $accountProperty = $account->getProperty($property); + $currentValue = $accountProperty->getValue(); + $scope = ($accountProperty->getScope() ?: $defaultScopes[$property]); + } catch (PropertyDoesNotExistException $e) { // thrown at getProperty + $this->logger->error('property does not exist: ' . $property + . ' for uid=' . $this->uid . '', ['app' => 'user_ldap', 'exception' => $e]); + $currentValue = ''; + $scope = $defaultScopes[$property]; + } + $verified = IAccountManager::VERIFIED; // trust the LDAP admin knew what they put there + if ($currentValue !== $value) { + $account->setProperty($property, $value, $scope, $verified); + $this->logger->debug('update user profile: ' . $property . '=' . $value + . ' for uid=' . $this->uid . '', ['app' => 'user_ldap']); + } + } + try { + $accountManager->updateAccount($account); // may throw InvalidArgumentException + } catch (\InvalidArgumentException $e) { + $this->logger->error('invalid data from LDAP: for uid=' . $this->uid . '', ['app' => 'user_ldap', 'func' => 'updateProfile' + , 'exception' => $e]); } } /** * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar - * @return bool + * @return bool true when the avatar was set successfully or is up to date */ - public function updateAvatar($force = false) { + public function updateAvatar(bool $force = false): bool { if (!$force && $this->wasRefreshed('avatar')) { return false; } @@ -544,11 +637,11 @@ class User { // use the checksum before modifications $checksum = md5($this->image->data()); - if ($checksum === $this->config->getUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', '')) { + if ($checksum === $this->config->getUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', '') && $this->avatarExists()) { return true; } - $isSet = $this->setOwnCloudAvatar(); + $isSet = $this->setNextcloudAvatar(); if ($isSet) { // save checksum only after successful setting @@ -558,38 +651,38 @@ class User { return $isSet; } + private function avatarExists(): bool { + try { + $currentAvatar = $this->avatarManager->getAvatar($this->uid); + return $currentAvatar->exists() && $currentAvatar->isCustomAvatar(); + } catch (\Exception $e) { + return false; + } + } + /** * @brief sets an image as Nextcloud avatar - * @return bool */ - private function setOwnCloudAvatar() { + private function setNextcloudAvatar(): bool { if (!$this->image->valid()) { - $this->logger->error('avatar image data from LDAP invalid for '.$this->dn, ['app' => 'user_ldap']); + $this->logger->error('avatar image data from LDAP invalid for ' . $this->dn, ['app' => 'user_ldap']); return false; } - //make sure it is a square and not bigger than 128x128 - $size = min([$this->image->width(), $this->image->height(), 128]); + //make sure it is a square and not bigger than 512x512 + $size = min([$this->image->width(), $this->image->height(), 512]); if (!$this->image->centerCrop($size)) { - $this->logger->error('croping image for avatar failed for '.$this->dn, ['app' => 'user_ldap']); + $this->logger->error('croping image for avatar failed for ' . $this->dn, ['app' => 'user_ldap']); return false; } - if (!$this->fs->isLoaded()) { - $this->fs->setup($this->uid); - } - try { $avatar = $this->avatarManager->getAvatar($this->uid); $avatar->set($this->image); return true; } catch (\Exception $e) { - \OC::$server->getLogger()->logException($e, [ - 'message' => 'Could not set avatar for ' . $this->dn, - 'level' => ILogger::INFO, - 'app' => 'user_ldap', - ]); + $this->logger->info('Could not set avatar for ' . $this->dn, ['exception' => $e]); } return false; } @@ -597,7 +690,7 @@ class User { /** * @throws AttributeNotSet * @throws \OC\ServerNotAvailableException - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ public function getExtStorageHome():string { $value = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', ''); @@ -616,16 +709,16 @@ class User { } /** - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException * @throws \OC\ServerNotAvailableException */ - public function updateExtStorageHome(string $valueFromLDAP = null):string { + public function updateExtStorageHome(?string $valueFromLDAP = null):string { if ($valueFromLDAP === null) { $extHomeValues = $this->access->readAttribute($this->getDN(), $this->connection->ldapExtStorageHomeAttribute); } else { $extHomeValues = [$valueFromLDAP]; } - if ($extHomeValues && isset($extHomeValues[0])) { + if ($extHomeValues !== false && isset($extHomeValues[0])) { $extHome = $extHomeValues[0]; $this->config->setUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', $extHome); return $extHome; @@ -637,29 +730,30 @@ class User { /** * called by a post_login hook to handle password expiry - * - * @param array $params */ - public function handlePasswordExpiry($params) { + public function handlePasswordExpiry(array $params): void { $ppolicyDN = $this->connection->ldapDefaultPPolicyDN; if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) { - return;//password expiry handling disabled + //password expiry handling disabled + return; } $uid = $params['uid']; if (isset($uid) && $uid === $this->getUsername()) { //retrieve relevant user attributes $result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']); - if (array_key_exists('pwdpolicysubentry', $result[0])) { - $pwdPolicySubentry = $result[0]['pwdpolicysubentry']; - if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)) { - $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN + if (!empty($result)) { + if (array_key_exists('pwdpolicysubentry', $result[0])) { + $pwdPolicySubentry = $result[0]['pwdpolicysubentry']; + if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)) { + $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN + } } - } - $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : []; - $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : []; - $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : []; + $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : []; + $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : []; + $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : []; + } //retrieve relevant password policy attributes $cacheKey = 'ppolicyAttributes' . $ppolicyDN; @@ -678,19 +772,19 @@ class User { if (!empty($pwdGraceAuthNLimit) && count($pwdGraceUseTime) < (int)$pwdGraceAuthNLimit[0]) { //at least one more grace login available? $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); - header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( - 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( + 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); } else { //no more grace login available - header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( - 'user_ldap.renewPassword.showLoginFormInvalidPassword', ['user' => $uid])); + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( + 'user_ldap.renewPassword.showLoginFormInvalidPassword', ['user' => $uid])); } exit(); } //handle pwdReset attribute - if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change his password + if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change their password $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); - header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( - 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( + 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); exit(); } //handle password expiry warning @@ -701,7 +795,7 @@ class User { $pwdExpireWarningInt = (int)$pwdExpireWarning[0]; if ($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0) { $pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]); - $pwdChangedTimeDt->add(new \DateInterval('PT'.$pwdMaxAgeInt.'S')); + $pwdChangedTimeDt->add(new \DateInterval('PT' . $pwdMaxAgeInt . 'S')); $currentDateTime = new \DateTime(); $secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp(); if ($secondsToExpiry <= $pwdExpireWarningInt) { @@ -718,7 +812,7 @@ class User { ->setUser($uid) ->setDateTime($currentDateTime) ->setObject('pwd_exp_warn', $uid) - ->setSubject('pwd_exp_warn_days', [(int) ceil($secondsToExpiry / 60 / 60 / 24)]) + ->setSubject('pwd_exp_warn_days', [(int)ceil($secondsToExpiry / 60 / 60 / 24)]) ; $this->notificationManager->notify($notification); } |