Merge pull request #47523 from nextcloud/fix/theming/custom-apps-order

This commit is contained in:
Kate 2024-09-09 18:04:41 +02:00 committed by GitHub
commit 3a85997940
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 484 additions and 259 deletions

View 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);
}

View file

@ -26,6 +26,7 @@ use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IURLGenerator;
use ScssPhp\ScssPhp\Compiler;
@ -47,6 +48,7 @@ class ThemingController extends Controller {
private IAppManager $appManager;
private ImageManager $imageManager;
private ThemesService $themesService;
private INavigationManager $navigationManager;
public function __construct(
$appName,
@ -57,7 +59,8 @@ class ThemingController extends Controller {
IURLGenerator $urlGenerator,
IAppManager $appManager,
ImageManager $imageManager,
ThemesService $themesService
ThemesService $themesService,
INavigationManager $navigationManager,
) {
parent::__construct($appName, $request);
@ -68,6 +71,7 @@ class ThemingController extends Controller {
$this->appManager = $appManager;
$this->imageManager = $imageManager;
$this->themesService = $themesService;
$this->navigationManager = $navigationManager;
}
/**
@ -163,7 +167,7 @@ class ThemingController extends Controller {
case 'defaultApps':
if (is_array($value)) {
try {
$this->appManager->setDefaultApps($value);
$this->navigationManager->setDefaultEntryIds($value);
} catch (InvalidArgumentException $e) {
$error = $this->l10n->t('Invalid app given');
}
@ -310,7 +314,7 @@ class ThemingController extends Controller {
#[AuthorizedAdminSetting(settings: Admin::class)]
public function undoAll(): DataResponse {
$this->themingDefaults->undoAll();
$this->appManager->setDefaultApps([]);
$this->navigationManager->setDefaultEntryIds([]);
return new DataResponse(
[

View file

@ -14,6 +14,7 @@ use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IURLGenerator;
use OCP\Settings\IDelegatedSettings;
use OCP\Util;
@ -28,6 +29,7 @@ class Admin implements IDelegatedSettings {
private IInitialState $initialState,
private IURLGenerator $urlGenerator,
private ImageManager $imageManager,
private INavigationManager $navigationManager,
) {
}
@ -70,7 +72,7 @@ class Admin implements IDelegatedSettings {
'docUrlIcons' => $this->urlGenerator->linkToDocs('admin-theming-icons'),
'canThemeIcons' => $this->imageManager->shouldReplaceIcons(),
'userThemingDisabled' => $this->themingDefaults->isUserThemingDisabled(),
'defaultApps' => array_filter(explode(',', $this->config->getSystemValueString('defaultapp', ''))),
'defaultApps' => $this->navigationManager->getDefaultEntryIds(),
]);
Util::addScript($this->appName, 'admin-theming');

View file

@ -9,10 +9,10 @@ use OCA\Theming\ITheme;
use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Service\ThemesService;
use OCA\Theming\ThemingDefaults;
use OCP\App\IAppManager;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\INavigationManager;
use OCP\Settings\ISettings;
use OCP\Util;
@ -25,7 +25,7 @@ class Personal implements ISettings {
private ThemesService $themesService,
private IInitialState $initialStateService,
private ThemingDefaults $themingDefaults,
private IAppManager $appManager,
private INavigationManager $navigationManager,
) {
}
@ -49,8 +49,8 @@ class Personal implements ISettings {
});
}
// Get the default app enforced by admin
$forcedDefaultApp = $this->appManager->getDefaultAppForUser(null, false);
// Get the default entry enforced by admin
$forcedDefaultEntry = $this->navigationManager->getDefaultEntryIdForUser(null, false);
/** List of all shipped backgrounds */
$this->initialStateService->provideInitialState('shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS);
@ -78,7 +78,7 @@ class Personal implements ISettings {
$this->initialStateService->provideInitialState('enableBlurFilter', $this->config->getUserValue($this->userId, 'theming', 'force_enable_blur_filter', ''));
$this->initialStateService->provideInitialState('navigationBar', [
'userAppOrder' => json_decode($this->config->getUserValue($this->userId, 'core', 'apporder', '[]'), true, flags:JSON_THROW_ON_ERROR),
'enforcedDefaultApp' => $forcedDefaultApp
'enforcedDefaultApp' => $forcedDefaultEntry
]);
Util::addScript($this->appName, 'personal-theming');

View file

@ -81,7 +81,7 @@ export default defineComponent({
*/
const initialAppOrder = loadState<INavigationEntry[]>('core', 'apps')
.filter(({ type }) => type === 'link')
.map((app) => ({ ...app, label: app.name, default: app.default && app.app === enforcedDefaultApp }))
.map((app) => ({ ...app, label: app.name, default: app.default && app.id === enforcedDefaultApp }))
/**
* Check if a custom app order is used or the default is shown

View file

@ -18,6 +18,7 @@ use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\IConfig;
use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IURLGenerator;
use PHPUnit\Framework\MockObject\MockObject;
@ -42,6 +43,8 @@ class ThemingControllerTest extends TestCase {
private $urlGenerator;
/** @var ThemesService|MockObject */
private $themesService;
/** @var INavigationManager|MockObject */
private $navigationManager;
protected function setUp(): void {
$this->request = $this->createMock(IRequest::class);
@ -52,6 +55,7 @@ class ThemingControllerTest extends TestCase {
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->imageManager = $this->createMock(ImageManager::class);
$this->themesService = $this->createMock(ThemesService::class);
$this->navigationManager = $this->createMock(INavigationManager::class);
$timeFactory = $this->createMock(ITimeFactory::class);
$timeFactory->expects($this->any())
@ -70,6 +74,7 @@ class ThemingControllerTest extends TestCase {
$this->appManager,
$this->imageManager,
$this->themesService,
$this->navigationManager,
);
parent::setUp();

View file

@ -13,6 +13,7 @@ use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IURLGenerator;
use Test\TestCase;
@ -24,6 +25,7 @@ class AdminTest extends TestCase {
private IURLGenerator $urlGenerator;
private ImageManager $imageManager;
private IL10N $l10n;
private INavigationManager $navigationManager;
protected function setUp(): void {
parent::setUp();
@ -33,6 +35,7 @@ class AdminTest extends TestCase {
$this->initialState = $this->createMock(IInitialState::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->imageManager = $this->createMock(ImageManager::class);
$this->navigationManager = $this->createMock(INavigationManager::class);
$this->admin = new Admin(
Application::APP_ID,
@ -41,7 +44,8 @@ class AdminTest extends TestCase {
$this->themingDefaults,
$this->initialState,
$this->urlGenerator,
$this->imageManager
$this->imageManager,
$this->navigationManager,
);
}

View file

@ -24,6 +24,7 @@ use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IURLGenerator;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
@ -34,7 +35,7 @@ class PersonalTest extends TestCase {
private ThemesService&MockObject $themesService;
private IInitialState&MockObject $initialStateService;
private ThemingDefaults&MockObject $themingDefaults;
private IAppManager&MockObject $appManager;
private INavigationManager&MockObject $navigationManager;
private Personal $admin;
/** @var ITheme[] */
@ -46,7 +47,7 @@ class PersonalTest extends TestCase {
$this->themesService = $this->createMock(ThemesService::class);
$this->initialStateService = $this->createMock(IInitialState::class);
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->navigationManager = $this->createMock(INavigationManager::class);
$this->initThemes();
@ -62,7 +63,7 @@ class PersonalTest extends TestCase {
$this->themesService,
$this->initialStateService,
$this->themingDefaults,
$this->appManager,
$this->navigationManager,
);
}
@ -103,9 +104,9 @@ class PersonalTest extends TestCase {
['admin', 'theming', 'background_image', BackgroundService::BACKGROUND_DEFAULT],
]);
$this->appManager->expects($this->once())
->method('getDefaultAppForUser')
->willReturn('forcedapp');
$this->navigationManager->expects($this->once())
->method('getDefaultEntryIdForUser')
->willReturn('forced_id');
$this->initialStateService->expects($this->exactly(8))
->method('provideInitialState')
@ -117,7 +118,7 @@ class PersonalTest extends TestCase {
['themes', $themesState],
['enforceTheme', $enforcedTheme],
['isUserThemingDisabled', false],
['navigationBar', ['userAppOrder' => [], 'enforcedDefaultApp' => 'forcedapp']],
['navigationBar', ['userAppOrder' => [], 'enforcedDefaultApp' => 'forced_id']],
]);
$expected = new TemplateResponse('theming', 'settings-personal');

View file

@ -493,7 +493,7 @@ $CONFIG = [
/**
* Enable SMTP class debugging.
* NOTE: ``loglevel`` will likely need to be adjusted too. See docs:
* NOTE: ``loglevel`` will likely need to be adjusted too. See docs:
* https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/email_configuration.html#enabling-debug-mode
*
* Defaults to ``false``
@ -1167,9 +1167,9 @@ $CONFIG = [
*/
/**
* Set the default app to open on login. Use the app names as they appear in the
* URL after clicking them in the Apps menu, such as documents, calendar, and
* gallery. You can use a comma-separated list of app names, so if the first
* Set the default app to open on login. The entry IDs can be retrieved from
* the Navigations OCS API endpoint: https://docs.nextcloud.com/server/latest/develper_manual/_static/openapi.html#/operations/core-navigation-get-apps-navigation.
* You can use a comma-separated list of app names, so if the first
* app is not enabled for a user then Nextcloud will try the second one, and so
* on. If no enabled apps are found it defaults to the dashboard app.
*
@ -1307,18 +1307,18 @@ $CONFIG = [
/**
* custom path for ffmpeg binary
*
* Defaults to ``null`` and falls back to searching ``avconv`` and ``ffmpeg``
* Defaults to ``null`` and falls back to searching ``avconv`` and ``ffmpeg``
* in the configured ``PATH`` environment
*/
'preview_ffmpeg_path' => '/usr/bin/ffmpeg',
/**
* Set the URL of the Imaginary service to send image previews to.
* Also requires the ``OC\Preview\Imaginary`` provider to be enabled in the
* ``enabledPreviewProviders`` array, to create previews for these mimetypes: bmp,
* Also requires the ``OC\Preview\Imaginary`` provider to be enabled in the
* ``enabledPreviewProviders`` array, to create previews for these mimetypes: bmp,
* x-bitmap, png, jpeg, gif, heic, heif, svg+xml, tiff, webp and illustrator.
*
* If you want Imaginary to also create preview images from PDF Documents, you
* If you want Imaginary to also create preview images from PDF Documents, you
* have to add the ``OC\Preview\ImaginaryPDF`` provider as well.
*
* See https://github.com/h2non/imaginary
@ -2050,9 +2050,9 @@ $CONFIG = [
/**
* Deny extensions from being used for filenames.
* Matching existing files can no longer be updated and in matching folders no files can be created anymore.
*
*
* The '.part' extension is always forbidden, as this is used internally by Nextcloud.
*
*
* Defaults to ``array('.filepart', '.part')``
*/
'forbidden_filename_extensions' => ['.part', '.filepart'],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,6 @@
*/
namespace OC\App;
use InvalidArgumentException;
use OC\AppConfig;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\ServerNotAvailableException;
@ -24,6 +23,7 @@ use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\INavigationManager;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
@ -67,6 +67,7 @@ class AppManager implements IAppManager {
private ?AppConfig $appConfig = null;
private ?IURLGenerator $urlGenerator = null;
private ?INavigationManager $navigationManager = null;
/**
* Be extremely careful when injecting classes here. The AppManager is used by the installer,
@ -82,6 +83,13 @@ class AppManager implements IAppManager {
) {
}
private function getNavigationManager(): INavigationManager {
if ($this->navigationManager === null) {
$this->navigationManager = \OCP\Server::get(INavigationManager::class);
}
return $this->navigationManager;
}
public function getAppIcon(string $appId, bool $dark = false): ?string {
$possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg'];
$icon = null;
@ -820,60 +828,42 @@ class AppManager implements IAppManager {
return $this->defaultEnabled;
}
/**
* @inheritdoc
*/
public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
// Set fallback to always-enabled files app
$appId = $withFallbacks ? 'files' : '';
$defaultApps = explode(',', $this->config->getSystemValueString('defaultapp', ''));
$defaultApps = array_filter($defaultApps);
$id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks);
$entry = $this->getNavigationManager()->get($id);
return (string)$entry['app'];
}
$user ??= $this->userSession->getUser();
/**
* @inheritdoc
*/
public function getDefaultApps(): array {
$ids = $this->getNavigationManager()->getDefaultEntryIds();
if ($user !== null) {
$userDefaultApps = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp'));
$defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps));
if (empty($defaultApps) && $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 guranteed to be set)
$customOrders = array_filter($customOrders, fn ($entry) => isset($entry['app']));
// sort apps by order
usort($customOrders, fn ($a, $b) => $a['order'] - $b['order']);
// set default apps to sorted apps
$defaultApps = array_map(fn ($entry) => $entry['app'], $customOrders);
return array_values(array_unique(array_map(function (string $id) {
$entry = $this->getNavigationManager()->get($id);
return (string)$entry['app'];
}, $ids)));
}
/**
* @inheritdoc
*/
public function setDefaultApps(array $defaultApps): void {
$entries = $this->getNavigationManager()->getAll();
$ids = [];
foreach ($defaultApps as $defaultApp) {
foreach ($entries as $entry) {
if ((string)$entry['app'] === $defaultApp) {
$ids[] = (string)$entry['id'];
break;
}
}
}
if (empty($defaultApps) && $withFallbacks) {
$defaultApps = ['dashboard','files'];
}
// Find the first app that is enabled for the current user
foreach ($defaultApps as $defaultApp) {
$defaultApp = \OC_App::cleanAppId(strip_tags($defaultApp));
if ($this->isEnabledForUser($defaultApp, $user)) {
$appId = $defaultApp;
break;
}
}
return $appId;
}
public function getDefaultApps(): array {
return explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files'));
}
public function setDefaultApps(array $defaultApps): void {
foreach ($defaultApps as $app) {
if (!$this->isInstalled($app)) {
$this->logger->debug('Can not set not installed app as default app', ['missing_app' => $app]);
throw new InvalidArgumentException('App is not installed');
}
}
$this->config->setSystemValue('defaultapp', join(',', $defaultApps));
$this->getNavigationManager()->setDefaultEntryIds($ids);
}
public function isBackendRequired(string $backend): bool {

View 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,26 @@ 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;
/** 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->defaultApp = null;
$this->logger = $logger;
}
/**
@ -92,13 +96,22 @@ class NavigationManager implements INavigationManager {
$entry['app'] = $id;
}
// This is the default app that will always be shown first
$entry['default'] = ($entry['app'] ?? false) === $this->defaultApp;
// Set order from user defined app order
$entry['order'] = (int)($this->customAppOrder[$id]['order'] ?? $entry['order'] ?? 100);
}
$this->entries[$id] = $entry;
// Needs to be done after adding the new entry to account for the default entries containing this new entry.
$this->updateDefaultEntries();
}
private function updateDefaultEntries() {
foreach ($this->entries as $id => $entry) {
if ($entry['type'] === 'link') {
$this->entries[$id]['default'] = $id === $this->getDefaultEntryIdForUser($this->userSession->getUser(), false);
}
}
}
/**
@ -156,10 +169,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,8 +226,6 @@ class NavigationManager implements INavigationManager {
]);
}
$this->defaultApp = $this->appManager->getDefaultAppForUser($this->userSession->getUser(), false);
if ($this->userSession->isLoggedIn()) {
// Profile
$this->add([
@ -401,4 +412,83 @@ 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();
// Disable fallbacks here, as we need to override them with the user defaults if none are configured.
$defaultEntryIds = $this->getDefaultEntryIds(false);
$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(bool $withFallbacks = true): array {
$this->init();
$storedIds = explode(',', $this->config->getSystemValueString('defaultapp', $withFallbacks ? 'dashboard,files' : ''));
$ids = [];
$entryIds = array_keys($this->entries);
foreach ($storedIds as $id) {
if (in_array($id, $entryIds, true)) {
$ids[] = $id;
break;
}
}
return array_filter($ids);
}
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));
}
}

View file

@ -99,11 +99,10 @@ class TemplateLayout extends \OC_Template {
$logoUrl = $this->config->getSystemValueString('logo_url', '');
$this->assign('logoUrl', $logoUrl);
// Set default app name
$defaultApp = \OC::$server->getAppManager()->getDefaultAppForUser();
$defaultAppInfo = \OC::$server->getAppManager()->getAppInfo($defaultApp);
$l10n = \OC::$server->get(IFactory::class)->get($defaultApp);
$this->assign('defaultAppName', $l10n->t($defaultAppInfo['name']));
// Set default entry name
$defaultEntryId = \OCP\Server::get(INavigationManager::class)->getDefaultEntryIdForUser();
$defaultEntry = \OCP\Server::get(INavigationManager::class)->get($defaultEntryId);
$this->assign('defaultAppName', $defaultEntry['name']);
// Add navigation entry
$this->assign('application', '');

View file

@ -14,6 +14,7 @@ use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
@ -36,6 +37,7 @@ class URLGenerator implements IURLGenerator {
/** @var null|string */
private $baseUrl = null;
private ?IAppManager $appManager = null;
private ?INavigationManager $navigationManager = null;
public function __construct(IConfig $config,
IUserSession $userSession,
@ -58,6 +60,14 @@ class URLGenerator implements IURLGenerator {
return $this->appManager;
}
private function getNavigationManager(): INavigationManager {
if ($this->navigationManager !== null) {
return $this->navigationManager;
}
$this->navigationManager = \OCP\Server::get(INavigationManager::class);
return $this->navigationManager;
}
/**
* Creates an url using a defined route
*
@ -288,14 +298,17 @@ class URLGenerator implements IURLGenerator {
return $this->getAbsoluteURL($defaultPage);
}
$appId = $this->getAppManager()->getDefaultAppForUser();
if ($this->config->getSystemValueBool('htaccess.IgnoreFrontController', false)
|| getenv('front_controller_active') === 'true') {
return $this->getAbsoluteURL('/apps/' . $appId . '/');
$entryId = $this->getNavigationManager()->getDefaultEntryIdForUser();
$entry = $this->getNavigationManager()->get($entryId);
$href = (string)$entry['href'];
if ($href === '') {
throw new \InvalidArgumentException('Default navigation entry is missing href: ' . $entryId);
}
if (str_starts_with($href, '/index.php/') && ($this->config->getSystemValueBool('htaccess.IgnoreFrontController', false) || getenv('front_controller_active') === 'true')) {
$href = substr($href, 10);
}
return $this->getAbsoluteURL('/index.php/apps/' . $appId . '/');
return $this->getAbsoluteURL($href);
}
/**

View file

@ -247,6 +247,8 @@ interface IAppManager {
*
* @since 25.0.6
* @since 28.0.0 Added optional $withFallbacks parameter
* @deprecated 31.0.0
* Use @see \OCP\INavigationManager::getDefaultEntryIdForUser() instead
*/
public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string;
@ -255,6 +257,8 @@ interface IAppManager {
*
* @return string[] The default applications
* @since 28.0.0
* @deprecated 31.0.0
* Use @see \OCP\INavigationManager::getDefaultEntryIds() instead
*/
public function getDefaultApps(): array;
@ -264,6 +268,8 @@ interface IAppManager {
* @param string[] $appId
* @throws \InvalidArgumentException If any of the apps is not installed
* @since 28.0.0
* @deprecated 31.0.0
* Use @see \OCP\INavigationManager::setDefaultEntryIds() instead
*/
public function setDefaultApps(array $defaultApps): void;

View 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;
}

View file

@ -733,159 +733,6 @@ class AppManagerTest extends TestCase {
$this->assertEquals(['foo'], $this->manager->getAppRestriction('test3'));
}
public function provideDefaultApps(): 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 provideDefaultApps
*/
public function testGetDefaultAppForUser($defaultApps, $userDefaultApps, $userApporder, $withFallbacks, $expectedApp) {
$user = $this->newUser('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->manager->getDefaultAppForUser(null, $withFallbacks));
}
public static function isBackendRequiredDataProvider(): array {
return [
// backend available

View 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,224 @@ 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));
}
public function testDefaultEntryUpdated() {
$this->appManager->method('getInstalledApps')->willReturn([]);
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('user1');
$this->userSession
->method('getUser')
->willReturn($user);
$this->config
->method('getSystemValueString')
->with('defaultapp', $this->anything())
->willReturn('app4,app3,app2,app1');
$this->config
->method('getUserValue')
->willReturnMap([
['user1', 'core', 'defaultapp', '', ''],
['user1', 'core', 'apporder', '[]', ''],
]);
$this->navigationManager->add([
'id' => 'app1',
]);
$this->assertEquals('app1', $this->navigationManager->getDefaultEntryIdForUser(null, false));
$this->assertEquals(true, $this->navigationManager->get('app1')['default']);
$this->navigationManager->add([
'id' => 'app3',
]);
$this->assertEquals('app3', $this->navigationManager->getDefaultEntryIdForUser(null, false));
$this->assertEquals(false, $this->navigationManager->get('app1')['default']);
$this->assertEquals(true, $this->navigationManager->get('app3')['default']);
$this->navigationManager->add([
'id' => 'app2',
]);
$this->assertEquals('app3', $this->navigationManager->getDefaultEntryIdForUser(null, false));
$this->assertEquals(false, $this->navigationManager->get('app1')['default']);
$this->assertEquals(false, $this->navigationManager->get('app2')['default']);
$this->assertEquals(true, $this->navigationManager->get('app3')['default']);
$this->navigationManager->add([
'id' => 'app4',
]);
$this->assertEquals('app4', $this->navigationManager->getDefaultEntryIdForUser(null, false));
$this->assertEquals(false, $this->navigationManager->get('app1')['default']);
$this->assertEquals(false, $this->navigationManager->get('app2')['default']);
$this->assertEquals(false, $this->navigationManager->get('app3')['default']);
$this->assertEquals(true, $this->navigationManager->get('app4')['default']);
}
}