]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat(NavigationManager): Add default entries handling
authorprovokateurin <kate@provokateurin.de>
Tue, 27 Aug 2024 10:13:04 +0000 (12:13 +0200)
committerprovokateurin <kate@provokateurin.de>
Mon, 9 Sep 2024 09:04:36 +0000 (11:04 +0200)
Signed-off-by: provokateurin <kate@provokateurin.de>
apps/files/lib/App.php
lib/private/NavigationManager.php
lib/public/INavigationManager.php
tests/lib/NavigationManagerTest.php

index e24f204b744bb816bd8a2c273a87384bfca6679c..3fb93df6f1d020ea7d311231c99cee073838fb85 100644 (file)
@@ -16,6 +16,7 @@ use OCP\IURLGenerator;
 use OCP\IUserSession;
 use OCP\L10N\IFactory;
 use OCP\Server;
+use Psr\Log\LoggerInterface;
 
 class App {
        private static ?INavigationManager $navigationManager = null;
@@ -32,7 +33,8 @@ class App {
                                Server::get(IFactory::class),
                                Server::get(IUserSession::class),
                                Server::get(IGroupManager::class),
-                               Server::get(IConfig::class)
+                               Server::get(IConfig::class),
+                               Server::get(LoggerInterface::class),
                        );
                        self::$navigationManager->clear(false);
                }
index 5d71c83e77a3c48bd6671bb466810aacc5536827..e1fbb02fda43e3b281abfe8de6cebb76035678f3 100644 (file)
@@ -7,6 +7,7 @@
  */
 namespace OC;
 
+use InvalidArgumentException;
 use OC\App\AppManager;
 use OC\Group\Manager;
 use OCP\App\IAppManager;
@@ -14,8 +15,10 @@ use OCP\IConfig;
 use OCP\IGroupManager;
 use OCP\INavigationManager;
 use OCP\IURLGenerator;
+use OCP\IUser;
 use OCP\IUserSession;
 use OCP\L10N\IFactory;
+use Psr\Log\LoggerInterface;
 
 /**
  * Manages the ownCloud navigation
@@ -41,25 +44,30 @@ class NavigationManager implements INavigationManager {
        private $groupManager;
        /** @var IConfig */
        private $config;
-       /** The default app for the current user (cached for the `add` function) */
-       private ?string $defaultApp;
+       /** The default entry for the current user (cached for the `add` function) */
+       private ?string $defaultEntry;
        /** User defined app order (cached for the `add` function) */
        private array $customAppOrder;
+       private LoggerInterface $logger;
 
-       public function __construct(IAppManager $appManager,
+       public function __construct(
+               IAppManager $appManager,
                IURLGenerator $urlGenerator,
                IFactory $l10nFac,
                IUserSession $userSession,
                IGroupManager $groupManager,
-               IConfig $config) {
+               IConfig $config,
+               LoggerInterface $logger,
+       ) {
                $this->appManager = $appManager;
                $this->urlGenerator = $urlGenerator;
                $this->l10nFac = $l10nFac;
                $this->userSession = $userSession;
                $this->groupManager = $groupManager;
                $this->config = $config;
+               $this->logger = $logger;
 
-               $this->defaultApp = null;
+               $this->defaultEntry = null;
        }
 
        /**
@@ -93,7 +101,7 @@ class NavigationManager implements INavigationManager {
                        }
 
                        // This is the default app that will always be shown first
-                       $entry['default'] = ($entry['app'] ?? false) === $this->defaultApp;
+                       $entry['default'] = ($entry['id'] ?? false) === $this->defaultEntry;
                        // Set order from user defined app order
                        $entry['order'] = (int)($this->customAppOrder[$id]['order'] ?? $entry['order'] ?? 100);
                }
@@ -156,10 +164,10 @@ class NavigationManager implements INavigationManager {
                        unset($navEntry);
                }
 
-               $activeApp = $this->getActiveEntry();
-               if ($activeApp !== null) {
+               $activeEntry = $this->getActiveEntry();
+               if ($activeEntry !== null) {
                        foreach ($list as $index => &$navEntry) {
-                               if ($navEntry['id'] == $activeApp) {
+                               if ($navEntry['id'] == $activeEntry) {
                                        $navEntry['active'] = true;
                                } else {
                                        $navEntry['active'] = false;
@@ -213,7 +221,7 @@ class NavigationManager implements INavigationManager {
                        ]);
                }
 
-               $this->defaultApp = $this->appManager->getDefaultAppForUser($this->userSession->getUser(), false);
+               $this->defaultEntry = $this->getDefaultEntryIdForUser($this->userSession->getUser(), false);
 
                if ($this->userSession->isLoggedIn()) {
                        // Profile
@@ -401,4 +409,73 @@ class NavigationManager implements INavigationManager {
        public function setUnreadCounter(string $id, int $unreadCounter): void {
                $this->unreadCounters[$id] = $unreadCounter;
        }
+
+       public function get(string $id): array|null {
+               $this->init();
+               foreach ($this->closureEntries as $c) {
+                       $this->add($c());
+               }
+               $this->closureEntries = [];
+
+               return $this->entries[$id];
+       }
+
+       public function getDefaultEntryIdForUser(?IUser $user = null, bool $withFallbacks = true): string {
+               $this->init();
+               $defaultEntryIds = explode(',', $this->config->getSystemValueString('defaultapp', ''));
+               $defaultEntryIds = array_filter($defaultEntryIds);
+
+               $user ??= $this->userSession->getUser();
+
+               if ($user !== null) {
+                       $userDefaultEntryIds = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp'));
+                       $defaultEntryIds = array_filter(array_merge($userDefaultEntryIds, $defaultEntryIds));
+                       if (empty($defaultEntryIds) && $withFallbacks) {
+                               /* Fallback on user defined apporder */
+                               $customOrders = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags: JSON_THROW_ON_ERROR);
+                               if (!empty($customOrders)) {
+                                       // filter only entries with app key (when added using closures or NavigationManager::add the app is not guaranteed to be set)
+                                       $customOrders = array_filter($customOrders, static fn ($entry) => isset($entry['app']));
+                                       // sort apps by order
+                                       usort($customOrders, static fn ($a, $b) => $a['order'] - $b['order']);
+                                       // set default apps to sorted apps
+                                       $defaultEntryIds = array_map(static fn ($entry) => $entry['app'], $customOrders);
+                               }
+                       }
+               }
+
+               if (empty($defaultEntryIds) && $withFallbacks) {
+                       $defaultEntryIds = ['dashboard','files'];
+               }
+
+               $entryIds = array_keys($this->entries);
+
+               // Find the first app that is enabled for the current user
+               foreach ($defaultEntryIds as $defaultEntryId) {
+                       if (in_array($defaultEntryId, $entryIds, true)) {
+                               return $defaultEntryId;
+                       }
+               }
+
+               // Set fallback to always-enabled files app
+               return $withFallbacks ? 'files' : '';
+       }
+
+       public function getDefaultEntryIds(): array {
+               return explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files'));
+       }
+
+       public function setDefaultEntryIds(array $ids): void {
+               $this->init();
+               $entryIds = array_keys($this->entries);
+
+               foreach ($ids as $id) {
+                       if (!in_array($id, $entryIds, true)) {
+                               $this->logger->debug('Cannot set unavailable entry as default entry', ['missing_entry' => $id]);
+                               throw new InvalidArgumentException('Entry not available');
+                       }
+               }
+
+               $this->config->setSystemValue('defaultapp', join(',', $ids));
+       }
 }
index eaef1cb35ec57c96ea02f00eafe9443d9cf28ee1..d22e96aa9d337d095fcf875f236a21ccc99de447 100644 (file)
@@ -80,4 +80,42 @@ interface INavigationManager {
         * @since 22.0.0
         */
        public function setUnreadCounter(string $id, int $unreadCounter): void;
+
+       /**
+        * Get a navigation entry by id.
+        *
+        * @param string $id ID of the navigation entry
+        * @since 31.0.0
+        */
+       public function get(string $id): array|null;
+
+       /**
+        * Returns the id of the user's default entry
+        *
+        * If `user` is not passed, the currently logged in user will be used
+        *
+        * @param ?IUser $user User to query default entry for
+        * @param bool $withFallbacks Include fallback values if no default entry was configured manually
+        *                            Before falling back to predefined default entries,
+        *                            the user defined entry order is considered and the first entry would be used as the fallback.
+        * @since 31.0.0
+        */
+       public function getDefaultEntryIdForUser(?IUser $user = null, bool $withFallbacks = true): string;
+
+       /**
+        * Get the global default entries with fallbacks
+        *
+        * @return string[] The default entries
+        * @since 31.0.0
+        */
+       public function getDefaultEntryIds(): array;
+
+       /**
+        * Set the global default entries with fallbacks
+        *
+        * @param string[] $ids
+        * @throws \InvalidArgumentException If any of the entries is not available
+        * @since 31.0.0
+        */
+       public function setDefaultEntryIds(array $ids): void;
 }
index 71bebe7ce54fab5b0086782689658fc26d73c32d..416b4730147f3a9e7d09bb256e55a71004cebb1f 100644 (file)
@@ -18,6 +18,7 @@ use OCP\IURLGenerator;
 use OCP\IUser;
 use OCP\IUserSession;
 use OCP\L10N\IFactory;
+use Psr\Log\LoggerInterface;
 
 class NavigationManagerTest extends TestCase {
        /** @var AppManager|\PHPUnit\Framework\MockObject\MockObject */
@@ -35,6 +36,7 @@ class NavigationManagerTest extends TestCase {
 
        /** @var \OC\NavigationManager */
        protected $navigationManager;
+       protected LoggerInterface $logger;
 
        protected function setUp(): void {
                parent::setUp();
@@ -45,13 +47,15 @@ class NavigationManagerTest extends TestCase {
                $this->userSession = $this->createMock(IUserSession::class);
                $this->groupManager = $this->createMock(Manager::class);
                $this->config = $this->createMock(IConfig::class);
+               $this->logger = $this->createMock(LoggerInterface::class);
                $this->navigationManager = new NavigationManager(
                        $this->appManager,
                        $this->urlGenerator,
                        $this->l10nFac,
                        $this->userSession,
                        $this->groupManager,
-                       $this->config
+                       $this->config,
+                       $this->logger,
                );
 
                $this->navigationManager->clear(false);
@@ -557,4 +561,167 @@ class NavigationManagerTest extends TestCase {
                $entries = $this->navigationManager->getAll();
                $this->assertEquals($expected, $entries);
        }
+
+       public static function provideDefaultEntries(): array {
+               return [
+                       // none specified, default to files
+                       [
+                               '',
+                               '',
+                               '{}',
+                               true,
+                               'files',
+                       ],
+                       // none specified, without fallback
+                       [
+                               '',
+                               '',
+                               '{}',
+                               false,
+                               '',
+                       ],
+                       // unexisting or inaccessible app specified, default to files
+                       [
+                               'unexist',
+                               '',
+                               '{}',
+                               true,
+                               'files',
+                       ],
+                       // unexisting or inaccessible app specified, without fallbacks
+                       [
+                               'unexist',
+                               '',
+                               '{}',
+                               false,
+                               '',
+                       ],
+                       // non-standard app
+                       [
+                               'settings',
+                               '',
+                               '{}',
+                               true,
+                               'settings',
+                       ],
+                       // non-standard app, without fallback
+                       [
+                               'settings',
+                               '',
+                               '{}',
+                               false,
+                               'settings',
+                       ],
+                       // non-standard app with fallback
+                       [
+                               'unexist,settings',
+                               '',
+                               '{}',
+                               true,
+                               'settings',
+                       ],
+                       // system default app and user apporder
+                       [
+                               // system default is settings
+                               'unexist,settings',
+                               '',
+                               // apporder says default app is files (order is lower)
+                               '{"files_id":{"app":"files","order":1},"settings_id":{"app":"settings","order":2}}',
+                               true,
+                               // system default should override apporder
+                               'settings'
+                       ],
+                       // user-customized defaultapp
+                       [
+                               '',
+                               'files',
+                               '',
+                               true,
+                               'files',
+                       ],
+                       // user-customized defaultapp with systemwide
+                       [
+                               'unexist,settings',
+                               'files',
+                               '',
+                               true,
+                               'files',
+                       ],
+                       // user-customized defaultapp with system wide and apporder
+                       [
+                               'unexist,settings',
+                               'files',
+                               '{"settings_id":{"app":"settings","order":1},"files_id":{"app":"files","order":2}}',
+                               true,
+                               'files',
+                       ],
+                       // user-customized apporder fallback
+                       [
+                               '',
+                               '',
+                               '{"settings_id":{"app":"settings","order":1},"files":{"app":"files","order":2}}',
+                               true,
+                               'settings',
+                       ],
+                       // user-customized apporder fallback with missing app key (entries added by closures does not always have an app key set (Nextcloud 27 spreed app for example))
+                       [
+                               '',
+                               '',
+                               '{"spreed":{"order":1},"files":{"app":"files","order":2}}',
+                               true,
+                               'files',
+                       ],
+                       // user-customized apporder, but called without fallback
+                       [
+                               '',
+                               '',
+                               '{"settings":{"app":"settings","order":1},"files":{"app":"files","order":2}}',
+                               false,
+                               '',
+                       ],
+                       // user-customized apporder with an app that has multiple routes
+                       [
+                               '',
+                               '',
+                               '{"settings_id":{"app":"settings","order":1},"settings_id_2":{"app":"settings","order":3},"id_files":{"app":"files","order":2}}',
+                               true,
+                               'settings',
+                       ],
+               ];
+       }
+
+       /**
+        * @dataProvider provideDefaultEntries
+        */
+       public function testGetDefaultEntryIdForUser($defaultApps, $userDefaultApps, $userApporder, $withFallbacks, $expectedApp) {
+               $this->navigationManager->add([
+                       'id' => 'files',
+               ]);
+               $this->navigationManager->add([
+                       'id' => 'settings',
+               ]);
+
+               $this->appManager->method('getInstalledApps')->willReturn([]);
+
+               $user = $this->createMock(IUser::class);
+               $user->method('getUID')->willReturn('user1');
+
+               $this->userSession->expects($this->once())
+                       ->method('getUser')
+                       ->willReturn($user);
+
+               $this->config->expects($this->once())
+                       ->method('getSystemValueString')
+                       ->with('defaultapp', $this->anything())
+                       ->willReturn($defaultApps);
+
+               $this->config->expects($this->atLeastOnce())
+                       ->method('getUserValue')
+                       ->willReturnMap([
+                               ['user1', 'core', 'defaultapp', '', $userDefaultApps],
+                               ['user1', 'core', 'apporder', '[]', $userApporder],
+                       ]);
+
+               $this->assertEquals($expectedApp, $this->navigationManager->getDefaultEntryIdForUser(null, $withFallbacks));
+       }
 }