diff options
Diffstat (limited to 'lib/private/Group')
-rw-r--r-- | lib/private/Group/Backend.php | 29 | ||||
-rw-r--r-- | lib/private/Group/Database.php | 252 | ||||
-rw-r--r-- | lib/private/Group/DisplayNameCache.php | 76 | ||||
-rw-r--r-- | lib/private/Group/Group.php | 162 | ||||
-rw-r--r-- | lib/private/Group/Manager.php | 237 | ||||
-rw-r--r-- | lib/private/Group/MetaData.php | 59 |
6 files changed, 465 insertions, 350 deletions
diff --git a/lib/private/Group/Backend.php b/lib/private/Group/Backend.php index 037cdacf445..f4a90018b5a 100644 --- a/lib/private/Group/Backend.php +++ b/lib/private/Group/Backend.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Knut Ahlers <knut@ahlers.me> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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 OC\Group; @@ -88,7 +71,7 @@ abstract class Backend implements \OCP\GroupInterface { /** * Get all groups a user belongs to * @param string $uid Name of the user - * @return array an array of group names + * @return list<string> an array of group names * * This function fetches all groups a user belongs to. It does not check * if the user exists at all. @@ -126,7 +109,7 @@ abstract class Backend implements \OCP\GroupInterface { * @param string $search * @param int $limit * @param int $offset - * @return array an array of user ids + * @return array<int,string> an array of user ids */ public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) { return []; diff --git a/lib/private/Group/Database.php b/lib/private/Group/Database.php index 7b7ee41def9..0cb571a3935 100644 --- a/lib/private/Group/Database.php +++ b/lib/private/Group/Database.php @@ -1,77 +1,58 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Johannes Leuker <j.leuker@hosting.de> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Loki3000 <github@labcms.ru> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author tgrant <tom.grant760@gmail.com> - * @author Tom Grant <TomG736@users.noreply.github.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 OC\Group; use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use OC\User\LazyUser; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Group\Backend\ABackend; use OCP\Group\Backend\IAddToGroupBackend; +use OCP\Group\Backend\IBatchMethodsBackend; use OCP\Group\Backend\ICountDisabledInGroup; use OCP\Group\Backend\ICountUsersBackend; -use OCP\Group\Backend\ICreateGroupBackend; +use OCP\Group\Backend\ICreateNamedGroupBackend; use OCP\Group\Backend\IDeleteGroupBackend; use OCP\Group\Backend\IGetDisplayNameBackend; use OCP\Group\Backend\IGroupDetailsBackend; +use OCP\Group\Backend\INamedBackend; use OCP\Group\Backend\IRemoveFromGroupBackend; +use OCP\Group\Backend\ISearchableGroupBackend; use OCP\Group\Backend\ISetDisplayNameBackend; -use OCP\Group\Backend\INamedBackend; use OCP\IDBConnection; +use OCP\IUserManager; /** * Class for group management in a SQL Database (e.g. MySQL, SQLite) */ class Database extends ABackend implements IAddToGroupBackend, - ICountDisabledInGroup, - ICountUsersBackend, - ICreateGroupBackend, - IDeleteGroupBackend, - IGetDisplayNameBackend, - IGroupDetailsBackend, - IRemoveFromGroupBackend, - ISetDisplayNameBackend, - INamedBackend { - - /** @var string[] */ + ICountDisabledInGroup, + ICountUsersBackend, + ICreateNamedGroupBackend, + IDeleteGroupBackend, + IGetDisplayNameBackend, + IGroupDetailsBackend, + IRemoveFromGroupBackend, + ISetDisplayNameBackend, + ISearchableGroupBackend, + IBatchMethodsBackend, + INamedBackend { + /** @var array<string, array{gid: string, displayname: string}> */ private $groupCache = []; - /** @var IDBConnection */ - private $dbConn; - /** * \OC\Group\Database constructor. * * @param IDBConnection|null $dbConn */ - public function __construct(IDBConnection $dbConn = null) { - $this->dbConn = $dbConn; + public function __construct( + private ?IDBConnection $dbConn = null, + ) { } /** @@ -83,35 +64,28 @@ class Database extends ABackend implements } } - /** - * Try to create a new group - * @param string $gid The name of the group to create - * @return bool - * - * Tries to create a new group. If the group name already exists, false will - * be returned. - */ - public function createGroup(string $gid): bool { + public function createGroup(string $name): ?string { $this->fixDI(); + $gid = $this->computeGid($name); try { // Add group $builder = $this->dbConn->getQueryBuilder(); $result = $builder->insert('groups') ->setValue('gid', $builder->createNamedParameter($gid)) - ->setValue('displayname', $builder->createNamedParameter($gid)) + ->setValue('displayname', $builder->createNamedParameter($name)) ->execute(); } catch (UniqueConstraintViolationException $e) { - $result = 0; + return null; } // Add to cache $this->groupCache[$gid] = [ 'gid' => $gid, - 'displayname' => $gid + 'displayname' => $name ]; - return $result === 1; + return $gid; } /** @@ -128,19 +102,19 @@ class Database extends ABackend implements $qb = $this->dbConn->getQueryBuilder(); $qb->delete('groups') ->where($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) - ->execute(); + ->executeStatement(); // Delete the group-user relation $qb = $this->dbConn->getQueryBuilder(); $qb->delete('group_user') ->where($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) - ->execute(); + ->executeStatement(); // Delete the group-groupadmin relation $qb = $this->dbConn->getQueryBuilder(); $qb->delete('group_admin') ->where($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) - ->execute(); + ->executeStatement(); // Delete from cache unset($this->groupCache[$gid]); @@ -165,7 +139,7 @@ class Database extends ABackend implements ->from('group_user') ->where($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) - ->execute(); + ->executeQuery(); $result = $cursor->fetch(); $cursor->closeCursor(); @@ -190,7 +164,7 @@ class Database extends ABackend implements $qb->insert('group_user') ->setValue('uid', $qb->createNamedParameter($uid)) ->setValue('gid', $qb->createNamedParameter($gid)) - ->execute(); + ->executeStatement(); return true; } else { return false; @@ -212,7 +186,7 @@ class Database extends ABackend implements $qb->delete('group_user') ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) ->andWhere($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) - ->execute(); + ->executeStatement(); return true; } @@ -220,7 +194,7 @@ class Database extends ABackend implements /** * Get all groups a user belongs to * @param string $uid Name of the user - * @return array an array of group names + * @return list<string> an array of group names * * This function fetches all groups a user belongs to. It does not check * if the user exists at all. @@ -239,7 +213,7 @@ class Database extends ABackend implements ->from('group_user', 'gu') ->leftJoin('gu', 'groups', 'g', $qb->expr()->eq('gu.gid', 'g.gid')) ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) - ->execute(); + ->executeQuery(); $groups = []; while ($row = $cursor->fetch()) { @@ -263,11 +237,11 @@ class Database extends ABackend implements * * Returns a list with all groups */ - public function getGroups($search = '', $limit = null, $offset = null) { + public function getGroups(string $search = '', int $limit = -1, int $offset = 0) { $this->fixDI(); $query = $this->dbConn->getQueryBuilder(); - $query->select('gid') + $query->select('gid', 'displayname') ->from('groups') ->orderBy('gid', 'ASC'); @@ -280,12 +254,20 @@ class Database extends ABackend implements ))); } - $query->setMaxResults($limit) - ->setFirstResult($offset); - $result = $query->execute(); + if ($limit > 0) { + $query->setMaxResults($limit); + } + if ($offset > 0) { + $query->setFirstResult($offset); + } + $result = $query->executeQuery(); $groups = []; while ($row = $result->fetch()) { + $this->groupCache[$row['gid']] = [ + 'displayname' => $row['displayname'], + 'gid' => $row['gid'], + ]; $groups[] = $row['gid']; } $result->closeCursor(); @@ -310,7 +292,7 @@ class Database extends ABackend implements $cursor = $qb->select('gid', 'displayname') ->from('groups') ->where($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) - ->execute(); + ->executeQuery(); $result = $cursor->fetch(); $cursor->closeCursor(); @@ -325,29 +307,72 @@ class Database extends ABackend implements } /** - * get a list of all users in a group + * {@inheritdoc} + */ + public function groupsExists(array $gids): array { + $notFoundGids = []; + $existingGroups = []; + + // In case the data is already locally accessible, not need to do SQL query + // or do a SQL query but with a smaller in clause + foreach ($gids as $gid) { + if (isset($this->groupCache[$gid])) { + $existingGroups[] = $gid; + } else { + $notFoundGids[] = $gid; + } + } + + $qb = $this->dbConn->getQueryBuilder(); + $qb->select('gid', 'displayname') + ->from('groups') + ->where($qb->expr()->in('gid', $qb->createParameter('ids'))); + foreach (array_chunk($notFoundGids, 1000) as $chunk) { + $qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_STR_ARRAY); + $result = $qb->executeQuery(); + while ($row = $result->fetch()) { + $this->groupCache[(string)$row['gid']] = [ + 'displayname' => (string)$row['displayname'], + 'gid' => (string)$row['gid'], + ]; + $existingGroups[] = (string)$row['gid']; + } + $result->closeCursor(); + } + + return $existingGroups; + } + + /** + * Get a list of all users in a group * @param string $gid * @param string $search * @param int $limit * @param int $offset - * @return array an array of user ids + * @return array<int,string> an array of user ids */ - public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) { + public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0): array { + return array_values(array_map(fn ($user) => $user->getUid(), $this->searchInGroup($gid, $search, $limit, $offset))); + } + + public function searchInGroup(string $gid, string $search = '', int $limit = -1, int $offset = 0): array { $this->fixDI(); $query = $this->dbConn->getQueryBuilder(); - $query->select('g.uid') - ->from('group_user', 'g') + $query->select('g.uid', 'u.displayname'); + + $query->from('group_user', 'g') ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))) ->orderBy('g.uid', 'ASC'); + $query->leftJoin('g', 'users', 'u', $query->expr()->eq('g.uid', 'u.uid')); + if ($search !== '') { - $query->leftJoin('g', 'users', 'u', $query->expr()->eq('g.uid', 'u.uid')) - ->leftJoin('u', 'preferences', 'p', $query->expr()->andX( - $query->expr()->eq('p.userid', 'u.uid'), - $query->expr()->eq('p.appid', $query->expr()->literal('settings')), - $query->expr()->eq('p.configkey', $query->expr()->literal('email'))) - ) + $query->leftJoin('u', 'preferences', 'p', $query->expr()->andX( + $query->expr()->eq('p.userid', 'u.uid'), + $query->expr()->eq('p.appid', $query->expr()->literal('settings')), + $query->expr()->eq('p.configkey', $query->expr()->literal('email')) + )) // sqlite doesn't like re-using a single named parameter here ->andWhere( $query->expr()->orX( @@ -366,11 +391,12 @@ class Database extends ABackend implements $query->setFirstResult($offset); } - $result = $query->execute(); + $result = $query->executeQuery(); $users = []; + $userManager = \OCP\Server::get(IUserManager::class); while ($row = $result->fetch()) { - $users[] = $row['uid']; + $users[$row['uid']] = new LazyUser($row['uid'], $userManager, $row['displayname'] ?? null); } $result->closeCursor(); @@ -397,7 +423,7 @@ class Database extends ABackend implements ))); } - $result = $query->execute(); + $result = $query->executeQuery(); $count = $result->fetchOne(); $result->closeCursor(); @@ -429,7 +455,7 @@ class Database extends ABackend implements ->andWhere($query->expr()->eq('configvalue', $query->createNamedParameter('false'), IQueryBuilder::PARAM_STR)) ->andWhere($query->expr()->eq('gid', $query->createNamedParameter($gid), IQueryBuilder::PARAM_STR)); - $result = $query->execute(); + $result = $query->executeQuery(); $count = $result->fetchOne(); $result->closeCursor(); @@ -458,11 +484,11 @@ class Database extends ABackend implements ->from('groups') ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))); - $result = $query->execute(); + $result = $query->executeQuery(); $displayName = $result->fetchOne(); $result->closeCursor(); - return (string) $displayName; + return (string)$displayName; } public function getGroupDetails(string $gid): array { @@ -474,6 +500,45 @@ class Database extends ABackend implements return []; } + /** + * {@inheritdoc} + */ + public function getGroupsDetails(array $gids): array { + $notFoundGids = []; + $details = []; + + $this->fixDI(); + + // In case the data is already locally accessible, not need to do SQL query + // or do a SQL query but with a smaller in clause + foreach ($gids as $gid) { + if (isset($this->groupCache[$gid])) { + $details[$gid] = ['displayName' => $this->groupCache[$gid]['displayname']]; + } else { + $notFoundGids[] = $gid; + } + } + + foreach (array_chunk($notFoundGids, 1000) as $chunk) { + $query = $this->dbConn->getQueryBuilder(); + $query->select('gid', 'displayname') + ->from('groups') + ->where($query->expr()->in('gid', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY))); + + $result = $query->executeQuery(); + while ($row = $result->fetch()) { + $details[(string)$row['gid']] = ['displayName' => (string)$row['displayname']]; + $this->groupCache[(string)$row['gid']] = [ + 'displayname' => (string)$row['displayname'], + 'gid' => (string)$row['gid'], + ]; + } + $result->closeCursor(); + } + + return $details; + } + public function setDisplayName(string $gid, string $displayName): bool { if (!$this->groupExists($gid)) { return false; @@ -490,7 +555,7 @@ class Database extends ABackend implements $query->update('groups') ->set('displayname', $query->createNamedParameter($displayName)) ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))); - $query->execute(); + $query->executeStatement(); return true; } @@ -503,4 +568,13 @@ class Database extends ABackend implements public function getBackendName(): string { return 'Database'; } + + /** + * Compute group ID from display name (GIDs are limited to 64 characters in database) + */ + private function computeGid(string $displayName): string { + return mb_strlen($displayName) > 64 + ? hash('sha256', $displayName) + : $displayName; + } } diff --git a/lib/private/Group/DisplayNameCache.php b/lib/private/Group/DisplayNameCache.php new file mode 100644 index 00000000000..fef8d40a05f --- /dev/null +++ b/lib/private/Group/DisplayNameCache.php @@ -0,0 +1,76 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Group; + +use OCP\Cache\CappedMemoryCache; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Group\Events\GroupChangedEvent; +use OCP\Group\Events\GroupDeletedEvent; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IGroupManager; + +/** + * Class that cache the relation Group ID -> Display name + * + * This saves fetching the group from the backend for "just" the display name + * @template-implements IEventListener<GroupChangedEvent|GroupDeletedEvent> + */ +class DisplayNameCache implements IEventListener { + private CappedMemoryCache $cache; + private ICache $memCache; + private IGroupManager $groupManager; + + public function __construct(ICacheFactory $cacheFactory, IGroupManager $groupManager) { + $this->cache = new CappedMemoryCache(); + $this->memCache = $cacheFactory->createDistributed('groupDisplayNameMappingCache'); + $this->groupManager = $groupManager; + } + + public function getDisplayName(string $groupId): ?string { + if (isset($this->cache[$groupId])) { + return $this->cache[$groupId]; + } + $displayName = $this->memCache->get($groupId); + if ($displayName) { + $this->cache[$groupId] = $displayName; + return $displayName; + } + + $group = $this->groupManager->get($groupId); + if ($group) { + $displayName = $group->getDisplayName(); + } else { + $displayName = null; + } + $this->cache[$groupId] = $displayName; + $this->memCache->set($groupId, $displayName, 60 * 10); // 10 minutes + + return $displayName; + } + + public function clear(): void { + $this->cache = new CappedMemoryCache(); + $this->memCache->clear(); + } + + public function handle(Event $event): void { + if ($event instanceof GroupChangedEvent && $event->getFeature() === 'displayName') { + $groupId = $event->getGroup()->getGID(); + $newDisplayName = $event->getValue(); + $this->cache[$groupId] = $newDisplayName; + $this->memCache->set($groupId, $newDisplayName, 60 * 10); // 10 minutes + } + if ($event instanceof GroupDeletedEvent) { + $groupId = $event->getGroup()->getGID(); + unset($this->cache[$groupId]); + $this->memCache->remove($groupId); + } + } +} diff --git a/lib/private/Group/Group.php b/lib/private/Group/Group.php index 2ef4d2ee23f..6e42fad8b9f 100644 --- a/lib/private/Group/Group.php +++ b/lib/private/Group/Group.php @@ -1,52 +1,36 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Johannes Leuker <j.leuker@hosting.de> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @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 OC\Group; use OC\Hooks\PublicEmitter; +use OC\User\LazyUser; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Group\Backend\ICountDisabledInGroup; use OCP\Group\Backend\IGetDisplayNameBackend; use OCP\Group\Backend\IHideFromCollaborationBackend; use OCP\Group\Backend\INamedBackend; +use OCP\Group\Backend\ISearchableGroupBackend; use OCP\Group\Backend\ISetDisplayNameBackend; +use OCP\Group\Events\BeforeGroupChangedEvent; +use OCP\Group\Events\BeforeGroupDeletedEvent; +use OCP\Group\Events\BeforeUserAddedEvent; +use OCP\Group\Events\BeforeUserRemovedEvent; +use OCP\Group\Events\GroupChangedEvent; +use OCP\Group\Events\GroupDeletedEvent; +use OCP\Group\Events\UserAddedEvent; +use OCP\Group\Events\UserRemovedEvent; use OCP\GroupInterface; use OCP\IGroup; use OCP\IUser; use OCP\IUserManager; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; class Group implements IGroup { - /** @var null|string */ + /** @var null|string */ protected $displayName; /** @var string */ @@ -60,23 +44,14 @@ class Group implements IGroup { /** @var Backend[] */ private $backends; - /** @var EventDispatcherInterface */ + /** @var IEventDispatcher */ private $dispatcher; - /** @var \OC\User\Manager|IUserManager */ + /** @var \OC\User\Manager|IUserManager */ private $userManager; /** @var PublicEmitter */ private $emitter; - - /** - * @param string $gid - * @param Backend[] $backends - * @param EventDispatcherInterface $dispatcher - * @param IUserManager $userManager - * @param PublicEmitter $emitter - * @param string $displayName - */ - public function __construct(string $gid, array $backends, EventDispatcherInterface $dispatcher, IUserManager $userManager, PublicEmitter $emitter = null, ?string $displayName = null) { + public function __construct(string $gid, array $backends, IEventDispatcher $dispatcher, IUserManager $userManager, ?PublicEmitter $emitter = null, ?string $displayName = null) { $this->gid = $gid; $this->backends = $backends; $this->dispatcher = $dispatcher; @@ -85,11 +60,11 @@ class Group implements IGroup { $this->displayName = $displayName; } - public function getGID() { + public function getGID(): string { return $this->gid; } - public function getDisplayName() { + public function getDisplayName(): string { if (is_null($this->displayName)) { foreach ($this->backends as $backend) { if ($backend instanceof IGetDisplayNameBackend) { @@ -108,10 +83,12 @@ class Group implements IGroup { public function setDisplayName(string $displayName): bool { $displayName = trim($displayName); if ($displayName !== '') { + $this->dispatcher->dispatchTyped(new BeforeGroupChangedEvent($this, 'displayName', $displayName, $this->displayName)); foreach ($this->backends as $backend) { if (($backend instanceof ISetDisplayNameBackend) && $backend->setDisplayName($this->gid, $displayName)) { $this->displayName = $displayName; + $this->dispatcher->dispatchTyped(new GroupChangedEvent($this, 'displayName', $displayName, '')); return true; } } @@ -124,7 +101,7 @@ class Group implements IGroup { * * @return \OC\User\User[] */ - public function getUsers() { + public function getUsers(): array { if ($this->usersLoaded) { return $this->users; } @@ -151,7 +128,7 @@ class Group implements IGroup { * @param IUser $user * @return bool */ - public function inGroup(IUser $user) { + public function inGroup(IUser $user): bool { if (isset($this->users[$user->getUID()])) { return true; } @@ -169,14 +146,12 @@ class Group implements IGroup { * * @param IUser $user */ - public function addUser(IUser $user) { + public function addUser(IUser $user): void { if ($this->inGroup($user)) { return; } - $this->dispatcher->dispatch(IGroup::class . '::preAddUser', new GenericEvent($this, [ - 'user' => $user, - ])); + $this->dispatcher->dispatchTyped(new BeforeUserAddedEvent($this, $user)); if ($this->emitter) { $this->emitter->emit('\OC\Group', 'preAddUser', [$this, $user]); @@ -184,13 +159,9 @@ class Group implements IGroup { foreach ($this->backends as $backend) { if ($backend->implementsActions(\OC\Group\Backend::ADD_TO_GROUP)) { $backend->addToGroup($user->getUID(), $this->gid); - if ($this->users) { - $this->users[$user->getUID()] = $user; - } + $this->users[$user->getUID()] = $user; - $this->dispatcher->dispatch(IGroup::class . '::postAddUser', new GenericEvent($this, [ - 'user' => $user, - ])); + $this->dispatcher->dispatchTyped(new UserAddedEvent($this, $user)); if ($this->emitter) { $this->emitter->emit('\OC\Group', 'postAddUser', [$this, $user]); @@ -202,14 +173,10 @@ class Group implements IGroup { /** * remove a user from the group - * - * @param \OC\User\User $user */ - public function removeUser($user) { + public function removeUser(IUser $user): void { $result = false; - $this->dispatcher->dispatch(IGroup::class . '::preRemoveUser', new GenericEvent($this, [ - 'user' => $user, - ])); + $this->dispatcher->dispatchTyped(new BeforeUserRemovedEvent($this, $user)); if ($this->emitter) { $this->emitter->emit('\OC\Group', 'preRemoveUser', [$this, $user]); } @@ -220,9 +187,7 @@ class Group implements IGroup { } } if ($result) { - $this->dispatcher->dispatch(IGroup::class . '::postRemoveUser', new GenericEvent($this, [ - 'user' => $user, - ])); + $this->dispatcher->dispatchTyped(new UserRemovedEvent($this, $user)); if ($this->emitter) { $this->emitter->emit('\OC\Group', 'postRemoveUser', [$this, $user]); } @@ -238,18 +203,23 @@ class Group implements IGroup { } /** - * search for users in the group by userid - * - * @param string $search - * @param int $limit - * @param int $offset - * @return \OC\User\User[] + * Search for users in the group by userid or display name + * @return IUser[] */ - public function searchUsers($search, $limit = null, $offset = null) { + public function searchUsers(string $search, ?int $limit = null, ?int $offset = null): array { $users = []; foreach ($this->backends as $backend) { - $userIds = $backend->usersInGroup($this->gid, $search, $limit, $offset); - $users += $this->getVerifiedUsers($userIds); + if ($backend instanceof ISearchableGroupBackend) { + $users += $backend->searchInGroup($this->gid, $search, $limit ?? -1, $offset ?? 0); + } else { + $userIds = $backend->usersInGroup($this->gid, $search, $limit ?? -1, $offset ?? 0); + $userManager = \OCP\Server::get(IUserManager::class); + foreach ($userIds as $userId) { + if (!isset($users[$userId])) { + $users[$userId] = new LazyUser($userId, $userManager); + } + } + } if (!is_null($limit) and $limit <= 0) { return $users; } @@ -263,7 +233,7 @@ class Group implements IGroup { * @param string $search * @return int|bool */ - public function count($search = '') { + public function count($search = ''): int|bool { $users = false; foreach ($this->backends as $backend) { if ($backend->implementsActions(\OC\Group\Backend::COUNT_USERS)) { @@ -283,7 +253,7 @@ class Group implements IGroup { * * @return int|bool */ - public function countDisabled() { + public function countDisabled(): int|bool { $users = false; foreach ($this->backends as $backend) { if ($backend instanceof ICountDisabledInGroup) { @@ -304,18 +274,11 @@ class Group implements IGroup { * @param string $search * @param int $limit * @param int $offset - * @return \OC\User\User[] + * @return IUser[] + * @deprecated 27.0.0 Use searchUsers instead (same implementation) */ - public function searchDisplayName($search, $limit = null, $offset = null) { - $users = []; - foreach ($this->backends as $backend) { - $userIds = $backend->usersInGroup($this->gid, $search, $limit, $offset); - $users = $this->getVerifiedUsers($userIds); - if (!is_null($limit) and $limit <= 0) { - return array_values($users); - } - } - return array_values($users); + public function searchDisplayName(string $search, ?int $limit = null, ?int $offset = null): array { + return $this->searchUsers($search, $limit, $offset); } /** @@ -323,7 +286,7 @@ class Group implements IGroup { * * @return string[] */ - public function getBackendNames() { + public function getBackendNames(): array { $backends = []; foreach ($this->backends as $backend) { if ($backend instanceof INamedBackend) { @@ -337,18 +300,18 @@ class Group implements IGroup { } /** - * delete the group + * Delete the group * * @return bool */ - public function delete() { + public function delete(): bool { // Prevent users from deleting group admin if ($this->getGID() === 'admin') { return false; } $result = false; - $this->dispatcher->dispatch(IGroup::class . '::preDelete', new GenericEvent($this)); + $this->dispatcher->dispatchTyped(new BeforeGroupDeletedEvent($this)); if ($this->emitter) { $this->emitter->emit('\OC\Group', 'preDelete', [$this]); } @@ -358,7 +321,7 @@ class Group implements IGroup { } } if ($result) { - $this->dispatcher->dispatch(IGroup::class . '::postDelete', new GenericEvent($this)); + $this->dispatcher->dispatchTyped(new GroupDeletedEvent($this)); if ($this->emitter) { $this->emitter->emit('\OC\Group', 'postDelete', [$this]); } @@ -371,10 +334,7 @@ class Group implements IGroup { * @param string[] $userIds an array containing user IDs * @return \OC\User\User[] an Array with the userId as Key and \OC\User\User as value */ - private function getVerifiedUsers($userIds) { - if (!is_array($userIds)) { - return []; - } + private function getVerifiedUsers(array $userIds): array { $users = []; foreach ($userIds as $userId) { $user = $this->userManager->get($userId); @@ -389,7 +349,7 @@ class Group implements IGroup { * @return bool * @since 14.0.0 */ - public function canRemoveUser() { + public function canRemoveUser(): bool { foreach ($this->backends as $backend) { if ($backend->implementsActions(GroupInterface::REMOVE_FROM_GOUP)) { return true; @@ -402,7 +362,7 @@ class Group implements IGroup { * @return bool * @since 14.0.0 */ - public function canAddUser() { + public function canAddUser(): bool { foreach ($this->backends as $backend) { if ($backend->implementsActions(GroupInterface::ADD_TO_GROUP)) { return true; @@ -417,7 +377,7 @@ class Group implements IGroup { */ public function hideFromCollaboration(): bool { return array_reduce($this->backends, function (bool $hide, GroupInterface $backend) { - return $hide | ($backend instanceof IHideFromCollaborationBackend && $backend->hideGroup($this->gid)); + return $hide || ($backend instanceof IHideFromCollaborationBackend && $backend->hideGroup($this->gid)); }, false); } } diff --git a/lib/private/Group/Manager.php b/lib/private/Group/Manager.php index 28f7a400b41..e58a1fe6585 100644 --- a/lib/private/Group/Manager.php +++ b/lib/private/Group/Manager.php @@ -1,52 +1,28 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Knut Ahlers <knut@ahlers.me> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author macjohnny <estebanmarin@gmx.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Roman Kreisel <mail@romankreisel.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Vincent Petry <vincent@nextcloud.com> - * @author Vinicius Cubas Brand <vinicius@eita.org.br> - * @author voxsim "Simon Vocella" - * - * @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 OC\Group; use OC\Hooks\PublicEmitter; +use OC\Settings\AuthorizedGroupMapper; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Group\Backend\IBatchMethodsBackend; +use OCP\Group\Backend\ICreateNamedGroupBackend; +use OCP\Group\Backend\IGroupDetailsBackend; +use OCP\Group\Events\BeforeGroupCreatedEvent; +use OCP\Group\Events\GroupCreatedEvent; use OCP\GroupInterface; +use OCP\ICacheFactory; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; +use OCP\Security\Ip\IRemoteAddress; use Psr\Log\LoggerInterface; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use function is_string; /** * Class Manager @@ -67,48 +43,37 @@ class Manager extends PublicEmitter implements IGroupManager { /** @var GroupInterface[] */ private $backends = []; - /** @var \OC\User\Manager */ - private $userManager; - /** @var EventDispatcherInterface */ - private $dispatcher; - private LoggerInterface $logger; - - /** @var \OC\Group\Group[] */ + /** @var array<string, IGroup> */ private $cachedGroups = []; - /** @var (string[])[] */ + /** @var array<string, list<string>> */ private $cachedUserGroups = []; /** @var \OC\SubAdmin */ private $subAdmin = null; - public function __construct(\OC\User\Manager $userManager, - EventDispatcherInterface $dispatcher, - LoggerInterface $logger) { - $this->userManager = $userManager; - $this->dispatcher = $dispatcher; - $this->logger = $logger; - - $cachedGroups = &$this->cachedGroups; - $cachedUserGroups = &$this->cachedUserGroups; - $this->listen('\OC\Group', 'postDelete', function ($group) use (&$cachedGroups, &$cachedUserGroups) { - /** - * @var \OC\Group\Group $group - */ - unset($cachedGroups[$group->getGID()]); - $cachedUserGroups = []; + private DisplayNameCache $displayNameCache; + + private const MAX_GROUP_LENGTH = 255; + + public function __construct( + private \OC\User\Manager $userManager, + private IEventDispatcher $dispatcher, + private LoggerInterface $logger, + ICacheFactory $cacheFactory, + private IRemoteAddress $remoteAddress, + ) { + $this->displayNameCache = new DisplayNameCache($cacheFactory, $this); + + $this->listen('\OC\Group', 'postDelete', function (IGroup $group): void { + unset($this->cachedGroups[$group->getGID()]); + $this->cachedUserGroups = []; }); - $this->listen('\OC\Group', 'postAddUser', function ($group) use (&$cachedUserGroups) { - /** - * @var \OC\Group\Group $group - */ - $cachedUserGroups = []; + $this->listen('\OC\Group', 'postAddUser', function (IGroup $group): void { + $this->cachedUserGroups = []; }); - $this->listen('\OC\Group', 'postRemoveUser', function ($group) use (&$cachedUserGroups) { - /** - * @var \OC\Group\Group $group - */ - $cachedUserGroups = []; + $this->listen('\OC\Group', 'postRemoveUser', function (IGroup $group): void { + $this->cachedUserGroups = []; }); } @@ -180,7 +145,7 @@ class Manager extends PublicEmitter implements IGroupManager { if ($backend->implementsActions(Backend::GROUP_DETAILS)) { $groupData = $backend->getGroupDetails($gid); if (is_array($groupData) && !empty($groupData)) { - // take the display name from the first backend that has a non-null one + // take the display name from the last backend that has a non-null one if (is_null($displayName) && isset($groupData['displayName'])) { $displayName = $groupData['displayName']; } @@ -193,11 +158,69 @@ class Manager extends PublicEmitter implements IGroupManager { if (count($backends) === 0) { return null; } + /** @var GroupInterface[] $backends */ $this->cachedGroups[$gid] = new Group($gid, $backends, $this->dispatcher, $this->userManager, $this, $displayName); return $this->cachedGroups[$gid]; } /** + * @brief Batch method to create group objects + * + * @param list<string> $gids List of groupIds for which we want to create a IGroup object + * @param array<string, string> $displayNames Array containing already know display name for a groupId + * @return array<string, IGroup> + */ + protected function getGroupsObjects(array $gids, array $displayNames = []): array { + $backends = []; + $groups = []; + foreach ($gids as $gid) { + $backends[$gid] = []; + if (!isset($displayNames[$gid])) { + $displayNames[$gid] = null; + } + } + foreach ($this->backends as $backend) { + if ($backend instanceof IGroupDetailsBackend || $backend->implementsActions(GroupInterface::GROUP_DETAILS)) { + /** @var IGroupDetailsBackend $backend */ + if ($backend instanceof IBatchMethodsBackend) { + $groupDatas = $backend->getGroupsDetails($gids); + } else { + $groupDatas = []; + foreach ($gids as $gid) { + $groupDatas[$gid] = $backend->getGroupDetails($gid); + } + } + foreach ($groupDatas as $gid => $groupData) { + if (!empty($groupData)) { + // take the display name from the last backend that has a non-null one + if (isset($groupData['displayName'])) { + $displayNames[$gid] = $groupData['displayName']; + } + $backends[$gid][] = $backend; + } + } + } else { + if ($backend instanceof IBatchMethodsBackend) { + $existingGroups = $backend->groupsExists($gids); + } else { + $existingGroups = array_filter($gids, fn (string $gid): bool => $backend->groupExists($gid)); + } + foreach ($existingGroups as $group) { + $backends[$group][] = $backend; + } + } + } + foreach ($gids as $gid) { + if (count($backends[$gid]) === 0) { + continue; + } + $this->cachedGroups[$gid] = new Group($gid, $backends[$gid], $this->dispatcher, $this->userManager, $this, $displayNames[$gid]); + $groups[$gid] = $this->cachedGroups[$gid]; + } + return $groups; + } + + /** * @param string $gid * @return bool */ @@ -214,12 +237,24 @@ class Manager extends PublicEmitter implements IGroupManager { return null; } elseif ($group = $this->get($gid)) { return $group; + } elseif (mb_strlen($gid) > self::MAX_GROUP_LENGTH) { + throw new \Exception('Group name is limited to ' . self::MAX_GROUP_LENGTH . ' characters'); } else { + $this->dispatcher->dispatchTyped(new BeforeGroupCreatedEvent($gid)); $this->emit('\OC\Group', 'preCreate', [$gid]); foreach ($this->backends as $backend) { if ($backend->implementsActions(Backend::CREATE_GROUP)) { - if ($backend->createGroup($gid)) { + if ($backend instanceof ICreateNamedGroupBackend) { + $groupName = $gid; + if (($gid = $backend->createGroup($groupName)) !== null) { + $group = $this->getGroupObject($gid); + $this->dispatcher->dispatchTyped(new GroupCreatedEvent($group)); + $this->emit('\OC\Group', 'postCreate', [$group]); + return $group; + } + } elseif ($backend->createGroup($gid)) { $group = $this->getGroupObject($gid); + $this->dispatcher->dispatchTyped(new GroupCreatedEvent($group)); $this->emit('\OC\Group', 'postCreate', [$group]); return $group; } @@ -231,21 +266,17 @@ class Manager extends PublicEmitter implements IGroupManager { /** * @param string $search - * @param int $limit - * @param int $offset + * @param ?int $limit + * @param ?int $offset * @return \OC\Group\Group[] */ - public function search($search, $limit = null, $offset = null) { + public function search(string $search, ?int $limit = null, ?int $offset = 0) { $groups = []; foreach ($this->backends as $backend) { - $groupIds = $backend->getGroups($search, $limit, $offset); - foreach ($groupIds as $groupId) { - $aGroup = $this->get($groupId); - if ($aGroup instanceof IGroup) { - $groups[$groupId] = $aGroup; - } else { - $this->logger->debug('Group "' . $groupId . '" was returned by search but not found through direct access', ['app' => 'core']); - } + $groupIds = $backend->getGroups($search, $limit ?? -1, $offset ?? 0); + $newGroups = $this->getGroupsObjects($groupIds); + foreach ($newGroups as $groupId => $group) { + $groups[$groupId] = $group; } if (!is_null($limit) and $limit <= 0) { return array_values($groups); @@ -258,7 +289,7 @@ class Manager extends PublicEmitter implements IGroupManager { * @param IUser|null $user * @return \OC\Group\Group[] */ - public function getUserGroups(IUser $user = null) { + public function getUserGroups(?IUser $user = null) { if (!$user instanceof IUser) { return []; } @@ -291,14 +322,30 @@ class Manager extends PublicEmitter implements IGroupManager { * @return bool if admin */ public function isAdmin($userId) { + if (!$this->remoteAddress->allowsAdminActions()) { + return false; + } + foreach ($this->backends as $backend) { - if ($backend->implementsActions(Backend::IS_ADMIN) && $backend->isAdmin($userId)) { + if (is_string($userId) && $backend->implementsActions(Backend::IS_ADMIN) && $backend->isAdmin($userId)) { return true; } } return $this->isInGroup($userId, 'admin'); } + public function isDelegatedAdmin(string $userId): bool { + if (!$this->remoteAddress->allowsAdminActions()) { + return false; + } + + // Check if the user as admin delegation for users listing + $authorizedGroupMapper = \OCP\Server::get(AuthorizedGroupMapper::class); + $user = $this->userManager->get($userId); + $authorizedClasses = $authorizedGroupMapper->findAllClassesForUser($user); + return in_array(\OCA\Settings\Settings\Admin\Users::class, $authorizedClasses, true); + } + /** * Checks if a userId is in a group * @@ -307,14 +354,14 @@ class Manager extends PublicEmitter implements IGroupManager { * @return bool if in group */ public function isInGroup($userId, $group) { - return array_search($group, $this->getUserIdGroupIds($userId)) !== false; + return in_array($group, $this->getUserIdGroupIds($userId)); } /** * get a list of group ids for a user * * @param IUser $user - * @return string[] with group ids + * @return list<string> with group ids */ public function getUserGroupIds(IUser $user): array { return $this->getUserIdGroupIds($user->getUID()); @@ -322,7 +369,7 @@ class Manager extends PublicEmitter implements IGroupManager { /** * @param string $uid the user id - * @return string[] + * @return list<string> */ private function getUserIdGroupIds(string $uid): array { if (!isset($this->cachedUserGroups[$uid])) { @@ -339,6 +386,14 @@ class Manager extends PublicEmitter implements IGroupManager { } /** + * @param string $groupId + * @return ?string + */ + public function getDisplayName(string $groupId): ?string { + return $this->displayNameCache->getDisplayName($groupId); + } + + /** * get an array of groupid and displayName for a user * * @param IUser $user @@ -346,7 +401,7 @@ class Manager extends PublicEmitter implements IGroupManager { */ public function getUserGroupNames(IUser $user) { return array_map(function ($group) { - return ['displayName' => $group->getDisplayName()]; + return ['displayName' => $this->displayNameCache->getDisplayName($group->getGID())]; }, $this->getUserGroups($user)); } @@ -397,7 +452,7 @@ class Manager extends PublicEmitter implements IGroupManager { $matchingUsers = []; foreach ($groupUsers as $groupUser) { - $matchingUsers[(string) $groupUser->getUID()] = $groupUser->getDisplayName(); + $matchingUsers[(string)$groupUser->getUID()] = $groupUser->getDisplayName(); } return $matchingUsers; } @@ -411,7 +466,7 @@ class Manager extends PublicEmitter implements IGroupManager { $this->userManager, $this, \OC::$server->getDatabaseConnection(), - \OC::$server->get(IEventDispatcher::class) + $this->dispatcher ); } diff --git a/lib/private/Group/MetaData.php b/lib/private/Group/MetaData.php index a58d7e78bfc..77432eea5ff 100644 --- a/lib/private/Group/MetaData.php +++ b/lib/private/Group/MetaData.php @@ -1,31 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Stephan Peijnik <speijnik@anexia-it.com> - * @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: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\Group; @@ -39,33 +17,22 @@ class MetaData { public const SORT_USERCOUNT = 1; // May have performance issues on LDAP backends public const SORT_GROUPNAME = 2; - /** @var string */ - protected $user; - /** @var bool */ - protected $isAdmin; /** @var array */ protected $metaData = []; - /** @var GroupManager */ - protected $groupManager; /** @var int */ protected $sorting = self::SORT_NONE; - /** @var IUserSession */ - protected $userSession; /** * @param string $user the uid of the current user * @param bool $isAdmin whether the current users is an admin */ public function __construct( - string $user, - bool $isAdmin, - IGroupManager $groupManager, - IUserSession $userSession - ) { - $this->user = $user; - $this->isAdmin = $isAdmin; - $this->groupManager = $groupManager; - $this->userSession = $userSession; + private string $user, + private bool $isAdmin, + private bool $isDelegatedAdmin, + private IGroupManager $groupManager, + private IUserSession $userSession, + ) { } /** @@ -74,7 +41,7 @@ class MetaData { * [0] array containing meta data about admin groups * [1] array containing meta data about unprivileged groups * @param string $groupSearch only effective when instance was created with - * isAdmin being true + * isAdmin being true * @param string $userSearch the pattern users are search for */ public function get(string $groupSearch = '', string $userSearch = ''): array { @@ -184,11 +151,11 @@ class MetaData { * @return IGroup[] */ public function getGroups(string $search = ''): array { - if ($this->isAdmin) { + if ($this->isAdmin || $this->isDelegatedAdmin) { return $this->groupManager->search($search); } else { $userObject = $this->userSession->getUser(); - if ($userObject !== null) { + if ($userObject !== null && $this->groupManager instanceof GroupManager) { $groups = $this->groupManager->getSubAdmin()->getSubAdminsGroups($userObject); } else { $groups = []; |