Completed
Push — master ( 0d19a1...40e52e )
by Joas
24:45 queued 15s
created
tests/lib/NavigationManagerTest.php 2 patches
Indentation   +808 added lines, -808 removed lines patch added patch discarded remove patch
@@ -24,812 +24,812 @@
 block discarded – undo
24 24
 use Psr\Log\LoggerInterface;
25 25
 
26 26
 class NavigationManagerTest extends TestCase {
27
-	/** @var AppManager|\PHPUnit\Framework\MockObject\MockObject */
28
-	protected $appManager;
29
-	/** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
30
-	protected $urlGenerator;
31
-	/** @var IFactory|\PHPUnit\Framework\MockObject\MockObject */
32
-	protected $l10nFac;
33
-	/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
34
-	protected $userSession;
35
-	/** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
36
-	protected $groupManager;
37
-	/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
38
-	protected $config;
39
-
40
-	protected IEVentDispatcher|MockObject $dispatcher;
41
-
42
-	/** @var \OC\NavigationManager */
43
-	protected $navigationManager;
44
-	protected LoggerInterface $logger;
45
-
46
-	protected function setUp(): void {
47
-		parent::setUp();
48
-
49
-		$this->appManager = $this->createMock(AppManager::class);
50
-		$this->urlGenerator = $this->createMock(IURLGenerator::class);
51
-		$this->l10nFac = $this->createMock(IFactory::class);
52
-		$this->userSession = $this->createMock(IUserSession::class);
53
-		$this->groupManager = $this->createMock(Manager::class);
54
-		$this->config = $this->createMock(IConfig::class);
55
-		$this->logger = $this->createMock(LoggerInterface::class);
56
-		$this->dispatcher = $this->createMock(IEventDispatcher::class);
57
-		$this->navigationManager = new NavigationManager(
58
-			$this->appManager,
59
-			$this->urlGenerator,
60
-			$this->l10nFac,
61
-			$this->userSession,
62
-			$this->groupManager,
63
-			$this->config,
64
-			$this->logger,
65
-			$this->dispatcher,
66
-		);
67
-
68
-		$this->navigationManager->clear(false);
69
-	}
70
-
71
-	public function addArrayData() {
72
-		return [
73
-			[
74
-				'entry id' => [
75
-					'id' => 'entry id',
76
-					'name' => 'link text',
77
-					'order' => 1,
78
-					'icon' => 'optional',
79
-					'href' => 'url',
80
-					'type' => 'settings',
81
-					'classes' => '',
82
-					'unread' => 0
83
-				],
84
-				'entry id2' => [
85
-					'id' => 'entry id',
86
-					'name' => 'link text',
87
-					'order' => 1,
88
-					'icon' => 'optional',
89
-					'href' => 'url',
90
-					'active' => false,
91
-					'type' => 'settings',
92
-					'classes' => '',
93
-					'unread' => 0
94
-				]
95
-			],
96
-			[
97
-				'entry id' => [
98
-					'id' => 'entry id',
99
-					'name' => 'link text',
100
-					'order' => 1,
101
-					//'icon'	=> 'optional',
102
-					'href' => 'url',
103
-					'active' => true,
104
-					'unread' => 0,
105
-				],
106
-				'entry id2' => [
107
-					'id' => 'entry id',
108
-					'name' => 'link text',
109
-					'order' => 1,
110
-					'icon' => '',
111
-					'href' => 'url',
112
-					'active' => false,
113
-					'type' => 'link',
114
-					'classes' => '',
115
-					'unread' => 0,
116
-					'default' => true,
117
-				]
118
-			]
119
-		];
120
-	}
121
-
122
-	/**
123
-	 * @dataProvider addArrayData
124
-	 *
125
-	 * @param array $entry
126
-	 * @param array $expectedEntry
127
-	 */
128
-	public function testAddArray(array $entry, array $expectedEntry): void {
129
-		$this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists');
130
-		$this->navigationManager->add($entry);
131
-
132
-		$navigationEntries = $this->navigationManager->getAll('all');
133
-		$this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists');
134
-		$this->assertEquals($expectedEntry, $navigationEntries['entry id']);
135
-
136
-		$this->navigationManager->clear(false);
137
-		$this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists after clear()');
138
-	}
139
-
140
-	/**
141
-	 * @dataProvider addArrayData
142
-	 *
143
-	 * @param array $entry
144
-	 * @param array $expectedEntry
145
-	 */
146
-	public function testAddClosure(array $entry, array $expectedEntry): void {
147
-		global $testAddClosureNumberOfCalls;
148
-		$testAddClosureNumberOfCalls = 0;
149
-
150
-		$this->navigationManager->add(function () use ($entry) {
151
-			global $testAddClosureNumberOfCalls;
152
-			$testAddClosureNumberOfCalls++;
153
-
154
-			return $entry;
155
-		});
156
-
157
-		$this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by add()');
158
-
159
-		$navigationEntries = $this->navigationManager->getAll('all');
160
-		$this->assertEquals(1, $testAddClosureNumberOfCalls, 'Expected that the closure is called by getAll()');
161
-		$this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists');
162
-		$this->assertEquals($expectedEntry, $navigationEntries['entry id']);
163
-
164
-		$navigationEntries = $this->navigationManager->getAll('all');
165
-		$this->assertEquals(1, $testAddClosureNumberOfCalls, 'Expected that the closure is only called once for getAll()');
166
-		$this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists');
167
-		$this->assertEquals($expectedEntry, $navigationEntries['entry id']);
168
-
169
-		$this->navigationManager->clear(false);
170
-		$this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists after clear()');
171
-	}
172
-
173
-	public function testAddArrayClearGetAll(): void {
174
-		$entry = [
175
-			'id' => 'entry id',
176
-			'name' => 'link text',
177
-			'order' => 1,
178
-			'icon' => 'optional',
179
-			'href' => 'url'
180
-		];
181
-
182
-		$this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists');
183
-		$this->navigationManager->add($entry);
184
-		$this->navigationManager->clear(false);
185
-		$this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists after clear()');
186
-	}
187
-
188
-	public function testAddClosureClearGetAll(): void {
189
-		$this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists');
190
-
191
-		$entry = [
192
-			'id' => 'entry id',
193
-			'name' => 'link text',
194
-			'order' => 1,
195
-			'icon' => 'optional',
196
-			'href' => 'url'
197
-		];
198
-
199
-		global $testAddClosureNumberOfCalls;
200
-		$testAddClosureNumberOfCalls = 0;
201
-
202
-		$this->navigationManager->add(function () use ($entry) {
203
-			global $testAddClosureNumberOfCalls;
204
-			$testAddClosureNumberOfCalls++;
205
-
206
-			return $entry;
207
-		});
208
-
209
-		$this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by add()');
210
-		$this->navigationManager->clear(false);
211
-		$this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by clear()');
212
-		$this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists after clear()');
213
-		$this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by getAll()');
214
-	}
215
-
216
-	/**
217
-	 * @dataProvider providesNavigationConfig
218
-	 */
219
-	public function testWithAppManager($expected, $navigation, $isAdmin = false): void {
220
-		$l = $this->createMock(IL10N::class);
221
-		$l->expects($this->any())->method('t')->willReturnCallback(function ($text, $parameters = []) {
222
-			return vsprintf($text, $parameters);
223
-		});
224
-
225
-		/* Return default value */
226
-		$this->config->method('getUserValue')
227
-			->willReturnArgument(3);
228
-
229
-		$this->appManager->expects($this->any())
230
-			->method('isEnabledForUser')
231
-			->with('theming')
232
-			->willReturn(true);
233
-		$this->appManager->expects($this->once())
234
-			->method('getAppInfo')
235
-			->with('test')
236
-			->willReturn($navigation);
237
-		$this->urlGenerator->expects($this->any())
238
-			->method('imagePath')
239
-			->willReturnCallback(function ($appName, $file) {
240
-				return "/apps/$appName/img/$file";
241
-			});
242
-		$this->appManager->expects($this->any())
243
-			->method('getAppIcon')
244
-			->willReturnCallback(fn (string $appName) => "/apps/$appName/img/app.svg");
245
-		$this->l10nFac->expects($this->any())->method('get')->willReturn($l);
246
-		$this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function ($route) {
247
-			if ($route === 'core.login.logout') {
248
-				return 'https://example.com/logout';
249
-			}
250
-			return '/apps/test/';
251
-		});
252
-		$user = $this->createMock(IUser::class);
253
-		$user->expects($this->any())->method('getUID')->willReturn('user001');
254
-		$this->userSession->expects($this->any())->method('getUser')->willReturn($user);
255
-		$this->userSession->expects($this->any())->method('isLoggedIn')->willReturn(true);
256
-		$this->appManager->expects($this->any())
257
-			->method('getEnabledAppsForUser')
258
-			->with($user)
259
-			->willReturn(['test']);
260
-		$this->groupManager->expects($this->any())->method('isAdmin')->willReturn($isAdmin);
261
-		$subadmin = $this->createMock(SubAdmin::class);
262
-		$subadmin->expects($this->any())->method('isSubAdmin')->with($user)->willReturn(false);
263
-		$this->groupManager->expects($this->any())->method('getSubAdmin')->willReturn($subadmin);
264
-
265
-		$this->navigationManager->clear();
266
-		$this->dispatcher->expects($this->once())
267
-			->method('dispatchTyped')
268
-			->willReturnCallback(function ($event) {
269
-				$this->assertInstanceOf(LoadAdditionalEntriesEvent::class, $event);
270
-			});
271
-		$entries = $this->navigationManager->getAll('all');
272
-		$this->assertEquals($expected, $entries);
273
-	}
274
-
275
-	public function providesNavigationConfig() {
276
-		$apps = [
277
-			'core_apps' => [
278
-				'id' => 'core_apps',
279
-				'order' => 5,
280
-				'href' => '/apps/test/',
281
-				'icon' => '/apps/settings/img/apps.svg',
282
-				'name' => 'Apps',
283
-				'active' => false,
284
-				'type' => 'settings',
285
-				'classes' => '',
286
-				'unread' => 0
287
-			]
288
-		];
289
-		$defaults = [
290
-			'profile' => [
291
-				'type' => 'settings',
292
-				'id' => 'profile',
293
-				'order' => 1,
294
-				'href' => '/apps/test/',
295
-				'name' => 'View profile',
296
-				'icon' => '',
297
-				'active' => false,
298
-				'classes' => '',
299
-				'unread' => 0,
300
-			],
301
-			'accessibility_settings' => [
302
-				'type' => 'settings',
303
-				'id' => 'accessibility_settings',
304
-				'order' => 2,
305
-				'href' => '/apps/test/',
306
-				'name' => 'Appearance and accessibility',
307
-				'icon' => '/apps/theming/img/accessibility-dark.svg',
308
-				'active' => false,
309
-				'classes' => '',
310
-				'unread' => 0,
311
-			],
312
-			'settings' => [
313
-				'id' => 'settings',
314
-				'order' => 3,
315
-				'href' => '/apps/test/',
316
-				'icon' => '/apps/settings/img/admin.svg',
317
-				'name' => 'Settings',
318
-				'active' => false,
319
-				'type' => 'settings',
320
-				'classes' => '',
321
-				'unread' => 0
322
-			],
323
-			'logout' => [
324
-				'id' => 'logout',
325
-				'order' => 99999,
326
-				'href' => 'https://example.com/logout?requesttoken=' . urlencode(\OCP\Util::callRegister()),
327
-				'icon' => '/apps/core/img/actions/logout.svg',
328
-				'name' => 'Log out',
329
-				'active' => false,
330
-				'type' => 'settings',
331
-				'classes' => '',
332
-				'unread' => 0
333
-			]
334
-		];
335
-		$adminSettings = [
336
-			'accessibility_settings' => $defaults['accessibility_settings'],
337
-			'settings' => [
338
-				'id' => 'settings',
339
-				'order' => 3,
340
-				'href' => '/apps/test/',
341
-				'icon' => '/apps/settings/img/personal.svg',
342
-				'name' => 'Personal settings',
343
-				'active' => false,
344
-				'type' => 'settings',
345
-				'classes' => '',
346
-				'unread' => 0
347
-			],
348
-			'admin_settings' => [
349
-				'id' => 'admin_settings',
350
-				'order' => 4,
351
-				'href' => '/apps/test/',
352
-				'icon' => '/apps/settings/img/admin.svg',
353
-				'name' => 'Administration settings',
354
-				'active' => false,
355
-				'type' => 'settings',
356
-				'classes' => '',
357
-				'unread' => 0
358
-			]
359
-		];
360
-
361
-		return [
362
-			'minimalistic' => [
363
-				array_merge(
364
-					['profile' => $defaults['profile']],
365
-					['accessibility_settings' => $defaults['accessibility_settings']],
366
-					['settings' => $defaults['settings']],
367
-					['test' => [
368
-						'id' => 'test',
369
-						'order' => 100,
370
-						'href' => '/apps/test/',
371
-						'icon' => '/apps/test/img/app.svg',
372
-						'name' => 'Test',
373
-						'active' => false,
374
-						'type' => 'link',
375
-						'classes' => '',
376
-						'unread' => 0,
377
-						'default' => true,
378
-						'app' => 'test',
379
-					]],
380
-					['logout' => $defaults['logout']]
381
-				),
382
-				['navigations' => [
383
-					'navigation' => [
384
-						['route' => 'test.page.index', 'name' => 'Test']
385
-					]
386
-				]]
387
-			],
388
-			'minimalistic-settings' => [
389
-				array_merge(
390
-					['profile' => $defaults['profile']],
391
-					['accessibility_settings' => $defaults['accessibility_settings']],
392
-					['settings' => $defaults['settings']],
393
-					['test' => [
394
-						'id' => 'test',
395
-						'order' => 100,
396
-						'href' => '/apps/test/',
397
-						'icon' => '/apps/test/img/app.svg',
398
-						'name' => 'Test',
399
-						'active' => false,
400
-						'type' => 'settings',
401
-						'classes' => '',
402
-						'unread' => 0,
403
-					]],
404
-					['logout' => $defaults['logout']]
405
-				),
406
-				['navigations' => [
407
-					'navigation' => [
408
-						['route' => 'test.page.index', 'name' => 'Test', 'type' => 'settings']
409
-					],
410
-				]]
411
-			],
412
-			'with-multiple' => [
413
-				array_merge(
414
-					['profile' => $defaults['profile']],
415
-					['accessibility_settings' => $defaults['accessibility_settings']],
416
-					['settings' => $defaults['settings']],
417
-					['test' => [
418
-						'id' => 'test',
419
-						'order' => 100,
420
-						'href' => '/apps/test/',
421
-						'icon' => '/apps/test/img/app.svg',
422
-						'name' => 'Test',
423
-						'active' => false,
424
-						'type' => 'link',
425
-						'classes' => '',
426
-						'unread' => 0,
427
-						'default' => false,
428
-						'app' => 'test',
429
-					],
430
-						'test1' => [
431
-							'id' => 'test1',
432
-							'order' => 50,
433
-							'href' => '/apps/test/',
434
-							'icon' => '/apps/test/img/app.svg',
435
-							'name' => 'Other test',
436
-							'active' => false,
437
-							'type' => 'link',
438
-							'classes' => '',
439
-							'unread' => 0,
440
-							'default' => true, // because of order
441
-							'app' => 'test',
442
-						]],
443
-					['logout' => $defaults['logout']]
444
-				),
445
-				['navigations' => [
446
-					'navigation' => [
447
-						['route' => 'test.page.index', 'name' => 'Test'],
448
-						['route' => 'test.page.index', 'name' => 'Other test', 'order' => 50],
449
-					]
450
-				]]
451
-			],
452
-			'admin' => [
453
-				array_merge(
454
-					['profile' => $defaults['profile']],
455
-					$adminSettings,
456
-					$apps,
457
-					['test' => [
458
-						'id' => 'test',
459
-						'order' => 100,
460
-						'href' => '/apps/test/',
461
-						'icon' => '/apps/test/img/app.svg',
462
-						'name' => 'Test',
463
-						'active' => false,
464
-						'type' => 'link',
465
-						'classes' => '',
466
-						'unread' => 0,
467
-						'default' => true,
468
-						'app' => 'test',
469
-					]],
470
-					['logout' => $defaults['logout']]
471
-				),
472
-				['navigations' => [
473
-					'navigation' => [
474
-						['@attributes' => ['role' => 'admin'], 'route' => 'test.page.index', 'name' => 'Test']
475
-					],
476
-				]],
477
-				true
478
-			],
479
-			'no name' => [
480
-				array_merge(
481
-					['profile' => $defaults['profile']],
482
-					$adminSettings,
483
-					$apps,
484
-					['logout' => $defaults['logout']]
485
-				),
486
-				['navigations' => [
487
-					'navigation' => [
488
-						['@attributes' => ['role' => 'admin'], 'route' => 'test.page.index']
489
-					],
490
-				]],
491
-				true
492
-			],
493
-			'no admin' => [
494
-				$defaults,
495
-				['navigations' => [
496
-					'navigation' => [
497
-						['@attributes' => ['role' => 'admin'], 'route' => 'test.page.index', 'name' => 'Test']
498
-					],
499
-				]],
500
-			]
501
-		];
502
-	}
503
-
504
-	public function testWithAppManagerAndApporder(): void {
505
-		$l = $this->createMock(IL10N::class);
506
-		$l->expects($this->any())->method('t')->willReturnCallback(function ($text, $parameters = []) {
507
-			return vsprintf($text, $parameters);
508
-		});
509
-
510
-		$testOrder = 12;
511
-		$expected = [
512
-			'test' => [
513
-				'type' => 'link',
514
-				'id' => 'test',
515
-				'order' => $testOrder,
516
-				'href' => '/apps/test/',
517
-				'name' => 'Test',
518
-				'icon' => '/apps/test/img/app.svg',
519
-				'active' => false,
520
-				'classes' => '',
521
-				'unread' => 0,
522
-				'default' => true,
523
-				'app' => 'test',
524
-			],
525
-		];
526
-		$navigation = ['navigations' => [
527
-			'navigation' => [
528
-				['route' => 'test.page.index', 'name' => 'Test']
529
-			],
530
-		]];
531
-
532
-		$this->config->method('getUserValue')
533
-			->willReturnCallback(
534
-				function (string $userId, string $appName, string $key, mixed $default = '') use ($testOrder) {
535
-					$this->assertEquals('user001', $userId);
536
-					if ($key === 'apporder') {
537
-						return json_encode(['test' => ['app' => 'test', 'order' => $testOrder]]);
538
-					}
539
-					return $default;
540
-				}
541
-			);
542
-
543
-		$this->appManager->expects($this->any())
544
-			->method('isEnabledForUser')
545
-			->with('theming')
546
-			->willReturn(true);
547
-		$this->appManager->expects($this->once())->method('getAppInfo')->with('test')->willReturn($navigation);
548
-		$this->appManager->expects($this->once())->method('getAppIcon')->with('test')->willReturn('/apps/test/img/app.svg');
549
-		$this->l10nFac->expects($this->any())->method('get')->willReturn($l);
550
-		$this->urlGenerator->expects($this->any())->method('imagePath')->willReturnCallback(function ($appName, $file) {
551
-			return "/apps/$appName/img/$file";
552
-		});
553
-		$this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function ($route) {
554
-			if ($route === 'core.login.logout') {
555
-				return 'https://example.com/logout';
556
-			}
557
-			return '/apps/test/';
558
-		});
559
-		$user = $this->createMock(IUser::class);
560
-		$user->expects($this->any())->method('getUID')->willReturn('user001');
561
-		$this->userSession->expects($this->any())->method('getUser')->willReturn($user);
562
-		$this->userSession->expects($this->any())->method('isLoggedIn')->willReturn(true);
563
-		$this->appManager->expects($this->any())
564
-			->method('getEnabledAppsForUser')
565
-			->with($user)
566
-			->willReturn(['test']);
567
-		$this->groupManager->expects($this->any())->method('isAdmin')->willReturn(false);
568
-		$subadmin = $this->createMock(SubAdmin::class);
569
-		$subadmin->expects($this->any())->method('isSubAdmin')->with($user)->willReturn(false);
570
-		$this->groupManager->expects($this->any())->method('getSubAdmin')->willReturn($subadmin);
571
-
572
-		$this->navigationManager->clear();
573
-		$this->dispatcher->expects($this->once())
574
-			->method('dispatchTyped')
575
-			->willReturnCallback(function ($event) {
576
-				$this->assertInstanceOf(LoadAdditionalEntriesEvent::class, $event);
577
-			});
578
-		$entries = $this->navigationManager->getAll();
579
-		$this->assertEquals($expected, $entries);
580
-	}
581
-
582
-	public static function provideDefaultEntries(): array {
583
-		return [
584
-			// none specified, default to files
585
-			[
586
-				'',
587
-				'',
588
-				'{}',
589
-				true,
590
-				'files',
591
-			],
592
-			// none specified, without fallback
593
-			[
594
-				'',
595
-				'',
596
-				'{}',
597
-				false,
598
-				'',
599
-			],
600
-			// unexisting or inaccessible app specified, default to files
601
-			[
602
-				'unexist',
603
-				'',
604
-				'{}',
605
-				true,
606
-				'files',
607
-			],
608
-			// unexisting or inaccessible app specified, without fallbacks
609
-			[
610
-				'unexist',
611
-				'',
612
-				'{}',
613
-				false,
614
-				'',
615
-			],
616
-			// non-standard app
617
-			[
618
-				'settings',
619
-				'',
620
-				'{}',
621
-				true,
622
-				'settings',
623
-			],
624
-			// non-standard app, without fallback
625
-			[
626
-				'settings',
627
-				'',
628
-				'{}',
629
-				false,
630
-				'settings',
631
-			],
632
-			// non-standard app with fallback
633
-			[
634
-				'unexist,settings',
635
-				'',
636
-				'{}',
637
-				true,
638
-				'settings',
639
-			],
640
-			// system default app and user apporder
641
-			[
642
-				// system default is settings
643
-				'unexist,settings',
644
-				'',
645
-				// apporder says default app is files (order is lower)
646
-				'{"files_id":{"app":"files","order":1},"settings_id":{"app":"settings","order":2}}',
647
-				true,
648
-				// system default should override apporder
649
-				'settings'
650
-			],
651
-			// user-customized defaultapp
652
-			[
653
-				'',
654
-				'files',
655
-				'',
656
-				true,
657
-				'files',
658
-			],
659
-			// user-customized defaultapp with systemwide
660
-			[
661
-				'unexist,settings',
662
-				'files',
663
-				'',
664
-				true,
665
-				'files',
666
-			],
667
-			// user-customized defaultapp with system wide and apporder
668
-			[
669
-				'unexist,settings',
670
-				'files',
671
-				'{"settings_id":{"app":"settings","order":1},"files_id":{"app":"files","order":2}}',
672
-				true,
673
-				'files',
674
-			],
675
-			// user-customized apporder fallback
676
-			[
677
-				'',
678
-				'',
679
-				'{"settings_id":{"app":"settings","order":1},"files":{"app":"files","order":2}}',
680
-				true,
681
-				'settings',
682
-			],
683
-			// 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))
684
-			[
685
-				'',
686
-				'',
687
-				'{"spreed":{"order":1},"files":{"app":"files","order":2}}',
688
-				true,
689
-				'files',
690
-			],
691
-			// user-customized apporder, but called without fallback
692
-			[
693
-				'',
694
-				'',
695
-				'{"settings":{"app":"settings","order":1},"files":{"app":"files","order":2}}',
696
-				false,
697
-				'',
698
-			],
699
-			// user-customized apporder with an app that has multiple routes
700
-			[
701
-				'',
702
-				'',
703
-				'{"settings_id":{"app":"settings","order":1},"settings_id_2":{"app":"settings","order":3},"id_files":{"app":"files","order":2}}',
704
-				true,
705
-				'settings',
706
-			],
707
-			// closure navigation entries are also resolved
708
-			[
709
-				'closure2',
710
-				'',
711
-				'',
712
-				true,
713
-				'closure2',
714
-			],
715
-			[
716
-				'',
717
-				'closure2',
718
-				'',
719
-				true,
720
-				'closure2',
721
-			],
722
-			[
723
-				'',
724
-				'',
725
-				'{"closure2":{"order":1,"app":"closure2","href":"/closure2"}}',
726
-				true,
727
-				'closure2',
728
-			],
729
-		];
730
-	}
731
-
732
-	/**
733
-	 * @dataProvider provideDefaultEntries
734
-	 */
735
-	public function testGetDefaultEntryIdForUser(string $defaultApps, string $userDefaultApps, string $userApporder, bool $withFallbacks, string $expectedApp): void {
736
-		$this->navigationManager->add([
737
-			'id' => 'files',
738
-		]);
739
-		$this->navigationManager->add([
740
-			'id' => 'settings',
741
-		]);
742
-		$this->navigationManager->add(static function (): array {
743
-			return [
744
-				'id' => 'closure1',
745
-				'href' => '/closure1',
746
-			];
747
-		});
748
-		$this->navigationManager->add(static function (): array {
749
-			return [
750
-				'id' => 'closure2',
751
-				'href' => '/closure2',
752
-			];
753
-		});
754
-
755
-		$this->appManager->method('getEnabledApps')->willReturn([]);
756
-
757
-		$user = $this->createMock(IUser::class);
758
-		$user->method('getUID')->willReturn('user1');
759
-
760
-		$this->userSession->expects($this->atLeastOnce())
761
-			->method('getUser')
762
-			->willReturn($user);
763
-
764
-		$this->config->expects($this->atLeastOnce())
765
-			->method('getSystemValueString')
766
-			->with('defaultapp', $this->anything())
767
-			->willReturn($defaultApps);
768
-
769
-		$this->config->expects($this->atLeastOnce())
770
-			->method('getUserValue')
771
-			->willReturnMap([
772
-				['user1', 'core', 'defaultapp', '', $userDefaultApps],
773
-				['user1', 'core', 'apporder', '[]', $userApporder],
774
-			]);
775
-
776
-		$this->assertEquals($expectedApp, $this->navigationManager->getDefaultEntryIdForUser(null, $withFallbacks));
777
-	}
778
-
779
-	public function testDefaultEntryUpdated(): void {
780
-		$this->appManager->method('getEnabledApps')->willReturn([]);
781
-
782
-		$user = $this->createMock(IUser::class);
783
-		$user->method('getUID')->willReturn('user1');
784
-
785
-		$this->userSession
786
-			->method('getUser')
787
-			->willReturn($user);
788
-
789
-		$this->config
790
-			->method('getSystemValueString')
791
-			->with('defaultapp', $this->anything())
792
-			->willReturn('app4,app3,app2,app1');
793
-
794
-		$this->config
795
-			->method('getUserValue')
796
-			->willReturnMap([
797
-				['user1', 'core', 'defaultapp', '', ''],
798
-				['user1', 'core', 'apporder', '[]', ''],
799
-			]);
800
-
801
-		$this->navigationManager->add([
802
-			'id' => 'app1',
803
-		]);
804
-
805
-		$this->assertEquals('app1', $this->navigationManager->getDefaultEntryIdForUser(null, false));
806
-		$this->assertEquals(true, $this->navigationManager->get('app1')['default']);
807
-
808
-		$this->navigationManager->add([
809
-			'id' => 'app3',
810
-		]);
811
-
812
-		$this->assertEquals('app3', $this->navigationManager->getDefaultEntryIdForUser(null, false));
813
-		$this->assertEquals(false, $this->navigationManager->get('app1')['default']);
814
-		$this->assertEquals(true, $this->navigationManager->get('app3')['default']);
815
-
816
-		$this->navigationManager->add([
817
-			'id' => 'app2',
818
-		]);
819
-
820
-		$this->assertEquals('app3', $this->navigationManager->getDefaultEntryIdForUser(null, false));
821
-		$this->assertEquals(false, $this->navigationManager->get('app1')['default']);
822
-		$this->assertEquals(false, $this->navigationManager->get('app2')['default']);
823
-		$this->assertEquals(true, $this->navigationManager->get('app3')['default']);
824
-
825
-		$this->navigationManager->add([
826
-			'id' => 'app4',
827
-		]);
828
-
829
-		$this->assertEquals('app4', $this->navigationManager->getDefaultEntryIdForUser(null, false));
830
-		$this->assertEquals(false, $this->navigationManager->get('app1')['default']);
831
-		$this->assertEquals(false, $this->navigationManager->get('app2')['default']);
832
-		$this->assertEquals(false, $this->navigationManager->get('app3')['default']);
833
-		$this->assertEquals(true, $this->navigationManager->get('app4')['default']);
834
-	}
27
+    /** @var AppManager|\PHPUnit\Framework\MockObject\MockObject */
28
+    protected $appManager;
29
+    /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
30
+    protected $urlGenerator;
31
+    /** @var IFactory|\PHPUnit\Framework\MockObject\MockObject */
32
+    protected $l10nFac;
33
+    /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
34
+    protected $userSession;
35
+    /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
36
+    protected $groupManager;
37
+    /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
38
+    protected $config;
39
+
40
+    protected IEVentDispatcher|MockObject $dispatcher;
41
+
42
+    /** @var \OC\NavigationManager */
43
+    protected $navigationManager;
44
+    protected LoggerInterface $logger;
45
+
46
+    protected function setUp(): void {
47
+        parent::setUp();
48
+
49
+        $this->appManager = $this->createMock(AppManager::class);
50
+        $this->urlGenerator = $this->createMock(IURLGenerator::class);
51
+        $this->l10nFac = $this->createMock(IFactory::class);
52
+        $this->userSession = $this->createMock(IUserSession::class);
53
+        $this->groupManager = $this->createMock(Manager::class);
54
+        $this->config = $this->createMock(IConfig::class);
55
+        $this->logger = $this->createMock(LoggerInterface::class);
56
+        $this->dispatcher = $this->createMock(IEventDispatcher::class);
57
+        $this->navigationManager = new NavigationManager(
58
+            $this->appManager,
59
+            $this->urlGenerator,
60
+            $this->l10nFac,
61
+            $this->userSession,
62
+            $this->groupManager,
63
+            $this->config,
64
+            $this->logger,
65
+            $this->dispatcher,
66
+        );
67
+
68
+        $this->navigationManager->clear(false);
69
+    }
70
+
71
+    public function addArrayData() {
72
+        return [
73
+            [
74
+                'entry id' => [
75
+                    'id' => 'entry id',
76
+                    'name' => 'link text',
77
+                    'order' => 1,
78
+                    'icon' => 'optional',
79
+                    'href' => 'url',
80
+                    'type' => 'settings',
81
+                    'classes' => '',
82
+                    'unread' => 0
83
+                ],
84
+                'entry id2' => [
85
+                    'id' => 'entry id',
86
+                    'name' => 'link text',
87
+                    'order' => 1,
88
+                    'icon' => 'optional',
89
+                    'href' => 'url',
90
+                    'active' => false,
91
+                    'type' => 'settings',
92
+                    'classes' => '',
93
+                    'unread' => 0
94
+                ]
95
+            ],
96
+            [
97
+                'entry id' => [
98
+                    'id' => 'entry id',
99
+                    'name' => 'link text',
100
+                    'order' => 1,
101
+                    //'icon'	=> 'optional',
102
+                    'href' => 'url',
103
+                    'active' => true,
104
+                    'unread' => 0,
105
+                ],
106
+                'entry id2' => [
107
+                    'id' => 'entry id',
108
+                    'name' => 'link text',
109
+                    'order' => 1,
110
+                    'icon' => '',
111
+                    'href' => 'url',
112
+                    'active' => false,
113
+                    'type' => 'link',
114
+                    'classes' => '',
115
+                    'unread' => 0,
116
+                    'default' => true,
117
+                ]
118
+            ]
119
+        ];
120
+    }
121
+
122
+    /**
123
+     * @dataProvider addArrayData
124
+     *
125
+     * @param array $entry
126
+     * @param array $expectedEntry
127
+     */
128
+    public function testAddArray(array $entry, array $expectedEntry): void {
129
+        $this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists');
130
+        $this->navigationManager->add($entry);
131
+
132
+        $navigationEntries = $this->navigationManager->getAll('all');
133
+        $this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists');
134
+        $this->assertEquals($expectedEntry, $navigationEntries['entry id']);
135
+
136
+        $this->navigationManager->clear(false);
137
+        $this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists after clear()');
138
+    }
139
+
140
+    /**
141
+     * @dataProvider addArrayData
142
+     *
143
+     * @param array $entry
144
+     * @param array $expectedEntry
145
+     */
146
+    public function testAddClosure(array $entry, array $expectedEntry): void {
147
+        global $testAddClosureNumberOfCalls;
148
+        $testAddClosureNumberOfCalls = 0;
149
+
150
+        $this->navigationManager->add(function () use ($entry) {
151
+            global $testAddClosureNumberOfCalls;
152
+            $testAddClosureNumberOfCalls++;
153
+
154
+            return $entry;
155
+        });
156
+
157
+        $this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by add()');
158
+
159
+        $navigationEntries = $this->navigationManager->getAll('all');
160
+        $this->assertEquals(1, $testAddClosureNumberOfCalls, 'Expected that the closure is called by getAll()');
161
+        $this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists');
162
+        $this->assertEquals($expectedEntry, $navigationEntries['entry id']);
163
+
164
+        $navigationEntries = $this->navigationManager->getAll('all');
165
+        $this->assertEquals(1, $testAddClosureNumberOfCalls, 'Expected that the closure is only called once for getAll()');
166
+        $this->assertCount(1, $navigationEntries, 'Expected that 1 navigation entry exists');
167
+        $this->assertEquals($expectedEntry, $navigationEntries['entry id']);
168
+
169
+        $this->navigationManager->clear(false);
170
+        $this->assertEmpty($this->navigationManager->getAll('all'), 'Expected no navigation entry exists after clear()');
171
+    }
172
+
173
+    public function testAddArrayClearGetAll(): void {
174
+        $entry = [
175
+            'id' => 'entry id',
176
+            'name' => 'link text',
177
+            'order' => 1,
178
+            'icon' => 'optional',
179
+            'href' => 'url'
180
+        ];
181
+
182
+        $this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists');
183
+        $this->navigationManager->add($entry);
184
+        $this->navigationManager->clear(false);
185
+        $this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists after clear()');
186
+    }
187
+
188
+    public function testAddClosureClearGetAll(): void {
189
+        $this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists');
190
+
191
+        $entry = [
192
+            'id' => 'entry id',
193
+            'name' => 'link text',
194
+            'order' => 1,
195
+            'icon' => 'optional',
196
+            'href' => 'url'
197
+        ];
198
+
199
+        global $testAddClosureNumberOfCalls;
200
+        $testAddClosureNumberOfCalls = 0;
201
+
202
+        $this->navigationManager->add(function () use ($entry) {
203
+            global $testAddClosureNumberOfCalls;
204
+            $testAddClosureNumberOfCalls++;
205
+
206
+            return $entry;
207
+        });
208
+
209
+        $this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by add()');
210
+        $this->navigationManager->clear(false);
211
+        $this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by clear()');
212
+        $this->assertEmpty($this->navigationManager->getAll(), 'Expected no navigation entry exists after clear()');
213
+        $this->assertEquals(0, $testAddClosureNumberOfCalls, 'Expected that the closure is not called by getAll()');
214
+    }
215
+
216
+    /**
217
+     * @dataProvider providesNavigationConfig
218
+     */
219
+    public function testWithAppManager($expected, $navigation, $isAdmin = false): void {
220
+        $l = $this->createMock(IL10N::class);
221
+        $l->expects($this->any())->method('t')->willReturnCallback(function ($text, $parameters = []) {
222
+            return vsprintf($text, $parameters);
223
+        });
224
+
225
+        /* Return default value */
226
+        $this->config->method('getUserValue')
227
+            ->willReturnArgument(3);
228
+
229
+        $this->appManager->expects($this->any())
230
+            ->method('isEnabledForUser')
231
+            ->with('theming')
232
+            ->willReturn(true);
233
+        $this->appManager->expects($this->once())
234
+            ->method('getAppInfo')
235
+            ->with('test')
236
+            ->willReturn($navigation);
237
+        $this->urlGenerator->expects($this->any())
238
+            ->method('imagePath')
239
+            ->willReturnCallback(function ($appName, $file) {
240
+                return "/apps/$appName/img/$file";
241
+            });
242
+        $this->appManager->expects($this->any())
243
+            ->method('getAppIcon')
244
+            ->willReturnCallback(fn (string $appName) => "/apps/$appName/img/app.svg");
245
+        $this->l10nFac->expects($this->any())->method('get')->willReturn($l);
246
+        $this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function ($route) {
247
+            if ($route === 'core.login.logout') {
248
+                return 'https://example.com/logout';
249
+            }
250
+            return '/apps/test/';
251
+        });
252
+        $user = $this->createMock(IUser::class);
253
+        $user->expects($this->any())->method('getUID')->willReturn('user001');
254
+        $this->userSession->expects($this->any())->method('getUser')->willReturn($user);
255
+        $this->userSession->expects($this->any())->method('isLoggedIn')->willReturn(true);
256
+        $this->appManager->expects($this->any())
257
+            ->method('getEnabledAppsForUser')
258
+            ->with($user)
259
+            ->willReturn(['test']);
260
+        $this->groupManager->expects($this->any())->method('isAdmin')->willReturn($isAdmin);
261
+        $subadmin = $this->createMock(SubAdmin::class);
262
+        $subadmin->expects($this->any())->method('isSubAdmin')->with($user)->willReturn(false);
263
+        $this->groupManager->expects($this->any())->method('getSubAdmin')->willReturn($subadmin);
264
+
265
+        $this->navigationManager->clear();
266
+        $this->dispatcher->expects($this->once())
267
+            ->method('dispatchTyped')
268
+            ->willReturnCallback(function ($event) {
269
+                $this->assertInstanceOf(LoadAdditionalEntriesEvent::class, $event);
270
+            });
271
+        $entries = $this->navigationManager->getAll('all');
272
+        $this->assertEquals($expected, $entries);
273
+    }
274
+
275
+    public function providesNavigationConfig() {
276
+        $apps = [
277
+            'core_apps' => [
278
+                'id' => 'core_apps',
279
+                'order' => 5,
280
+                'href' => '/apps/test/',
281
+                'icon' => '/apps/settings/img/apps.svg',
282
+                'name' => 'Apps',
283
+                'active' => false,
284
+                'type' => 'settings',
285
+                'classes' => '',
286
+                'unread' => 0
287
+            ]
288
+        ];
289
+        $defaults = [
290
+            'profile' => [
291
+                'type' => 'settings',
292
+                'id' => 'profile',
293
+                'order' => 1,
294
+                'href' => '/apps/test/',
295
+                'name' => 'View profile',
296
+                'icon' => '',
297
+                'active' => false,
298
+                'classes' => '',
299
+                'unread' => 0,
300
+            ],
301
+            'accessibility_settings' => [
302
+                'type' => 'settings',
303
+                'id' => 'accessibility_settings',
304
+                'order' => 2,
305
+                'href' => '/apps/test/',
306
+                'name' => 'Appearance and accessibility',
307
+                'icon' => '/apps/theming/img/accessibility-dark.svg',
308
+                'active' => false,
309
+                'classes' => '',
310
+                'unread' => 0,
311
+            ],
312
+            'settings' => [
313
+                'id' => 'settings',
314
+                'order' => 3,
315
+                'href' => '/apps/test/',
316
+                'icon' => '/apps/settings/img/admin.svg',
317
+                'name' => 'Settings',
318
+                'active' => false,
319
+                'type' => 'settings',
320
+                'classes' => '',
321
+                'unread' => 0
322
+            ],
323
+            'logout' => [
324
+                'id' => 'logout',
325
+                'order' => 99999,
326
+                'href' => 'https://example.com/logout?requesttoken=' . urlencode(\OCP\Util::callRegister()),
327
+                'icon' => '/apps/core/img/actions/logout.svg',
328
+                'name' => 'Log out',
329
+                'active' => false,
330
+                'type' => 'settings',
331
+                'classes' => '',
332
+                'unread' => 0
333
+            ]
334
+        ];
335
+        $adminSettings = [
336
+            'accessibility_settings' => $defaults['accessibility_settings'],
337
+            'settings' => [
338
+                'id' => 'settings',
339
+                'order' => 3,
340
+                'href' => '/apps/test/',
341
+                'icon' => '/apps/settings/img/personal.svg',
342
+                'name' => 'Personal settings',
343
+                'active' => false,
344
+                'type' => 'settings',
345
+                'classes' => '',
346
+                'unread' => 0
347
+            ],
348
+            'admin_settings' => [
349
+                'id' => 'admin_settings',
350
+                'order' => 4,
351
+                'href' => '/apps/test/',
352
+                'icon' => '/apps/settings/img/admin.svg',
353
+                'name' => 'Administration settings',
354
+                'active' => false,
355
+                'type' => 'settings',
356
+                'classes' => '',
357
+                'unread' => 0
358
+            ]
359
+        ];
360
+
361
+        return [
362
+            'minimalistic' => [
363
+                array_merge(
364
+                    ['profile' => $defaults['profile']],
365
+                    ['accessibility_settings' => $defaults['accessibility_settings']],
366
+                    ['settings' => $defaults['settings']],
367
+                    ['test' => [
368
+                        'id' => 'test',
369
+                        'order' => 100,
370
+                        'href' => '/apps/test/',
371
+                        'icon' => '/apps/test/img/app.svg',
372
+                        'name' => 'Test',
373
+                        'active' => false,
374
+                        'type' => 'link',
375
+                        'classes' => '',
376
+                        'unread' => 0,
377
+                        'default' => true,
378
+                        'app' => 'test',
379
+                    ]],
380
+                    ['logout' => $defaults['logout']]
381
+                ),
382
+                ['navigations' => [
383
+                    'navigation' => [
384
+                        ['route' => 'test.page.index', 'name' => 'Test']
385
+                    ]
386
+                ]]
387
+            ],
388
+            'minimalistic-settings' => [
389
+                array_merge(
390
+                    ['profile' => $defaults['profile']],
391
+                    ['accessibility_settings' => $defaults['accessibility_settings']],
392
+                    ['settings' => $defaults['settings']],
393
+                    ['test' => [
394
+                        'id' => 'test',
395
+                        'order' => 100,
396
+                        'href' => '/apps/test/',
397
+                        'icon' => '/apps/test/img/app.svg',
398
+                        'name' => 'Test',
399
+                        'active' => false,
400
+                        'type' => 'settings',
401
+                        'classes' => '',
402
+                        'unread' => 0,
403
+                    ]],
404
+                    ['logout' => $defaults['logout']]
405
+                ),
406
+                ['navigations' => [
407
+                    'navigation' => [
408
+                        ['route' => 'test.page.index', 'name' => 'Test', 'type' => 'settings']
409
+                    ],
410
+                ]]
411
+            ],
412
+            'with-multiple' => [
413
+                array_merge(
414
+                    ['profile' => $defaults['profile']],
415
+                    ['accessibility_settings' => $defaults['accessibility_settings']],
416
+                    ['settings' => $defaults['settings']],
417
+                    ['test' => [
418
+                        'id' => 'test',
419
+                        'order' => 100,
420
+                        'href' => '/apps/test/',
421
+                        'icon' => '/apps/test/img/app.svg',
422
+                        'name' => 'Test',
423
+                        'active' => false,
424
+                        'type' => 'link',
425
+                        'classes' => '',
426
+                        'unread' => 0,
427
+                        'default' => false,
428
+                        'app' => 'test',
429
+                    ],
430
+                        'test1' => [
431
+                            'id' => 'test1',
432
+                            'order' => 50,
433
+                            'href' => '/apps/test/',
434
+                            'icon' => '/apps/test/img/app.svg',
435
+                            'name' => 'Other test',
436
+                            'active' => false,
437
+                            'type' => 'link',
438
+                            'classes' => '',
439
+                            'unread' => 0,
440
+                            'default' => true, // because of order
441
+                            'app' => 'test',
442
+                        ]],
443
+                    ['logout' => $defaults['logout']]
444
+                ),
445
+                ['navigations' => [
446
+                    'navigation' => [
447
+                        ['route' => 'test.page.index', 'name' => 'Test'],
448
+                        ['route' => 'test.page.index', 'name' => 'Other test', 'order' => 50],
449
+                    ]
450
+                ]]
451
+            ],
452
+            'admin' => [
453
+                array_merge(
454
+                    ['profile' => $defaults['profile']],
455
+                    $adminSettings,
456
+                    $apps,
457
+                    ['test' => [
458
+                        'id' => 'test',
459
+                        'order' => 100,
460
+                        'href' => '/apps/test/',
461
+                        'icon' => '/apps/test/img/app.svg',
462
+                        'name' => 'Test',
463
+                        'active' => false,
464
+                        'type' => 'link',
465
+                        'classes' => '',
466
+                        'unread' => 0,
467
+                        'default' => true,
468
+                        'app' => 'test',
469
+                    ]],
470
+                    ['logout' => $defaults['logout']]
471
+                ),
472
+                ['navigations' => [
473
+                    'navigation' => [
474
+                        ['@attributes' => ['role' => 'admin'], 'route' => 'test.page.index', 'name' => 'Test']
475
+                    ],
476
+                ]],
477
+                true
478
+            ],
479
+            'no name' => [
480
+                array_merge(
481
+                    ['profile' => $defaults['profile']],
482
+                    $adminSettings,
483
+                    $apps,
484
+                    ['logout' => $defaults['logout']]
485
+                ),
486
+                ['navigations' => [
487
+                    'navigation' => [
488
+                        ['@attributes' => ['role' => 'admin'], 'route' => 'test.page.index']
489
+                    ],
490
+                ]],
491
+                true
492
+            ],
493
+            'no admin' => [
494
+                $defaults,
495
+                ['navigations' => [
496
+                    'navigation' => [
497
+                        ['@attributes' => ['role' => 'admin'], 'route' => 'test.page.index', 'name' => 'Test']
498
+                    ],
499
+                ]],
500
+            ]
501
+        ];
502
+    }
503
+
504
+    public function testWithAppManagerAndApporder(): void {
505
+        $l = $this->createMock(IL10N::class);
506
+        $l->expects($this->any())->method('t')->willReturnCallback(function ($text, $parameters = []) {
507
+            return vsprintf($text, $parameters);
508
+        });
509
+
510
+        $testOrder = 12;
511
+        $expected = [
512
+            'test' => [
513
+                'type' => 'link',
514
+                'id' => 'test',
515
+                'order' => $testOrder,
516
+                'href' => '/apps/test/',
517
+                'name' => 'Test',
518
+                'icon' => '/apps/test/img/app.svg',
519
+                'active' => false,
520
+                'classes' => '',
521
+                'unread' => 0,
522
+                'default' => true,
523
+                'app' => 'test',
524
+            ],
525
+        ];
526
+        $navigation = ['navigations' => [
527
+            'navigation' => [
528
+                ['route' => 'test.page.index', 'name' => 'Test']
529
+            ],
530
+        ]];
531
+
532
+        $this->config->method('getUserValue')
533
+            ->willReturnCallback(
534
+                function (string $userId, string $appName, string $key, mixed $default = '') use ($testOrder) {
535
+                    $this->assertEquals('user001', $userId);
536
+                    if ($key === 'apporder') {
537
+                        return json_encode(['test' => ['app' => 'test', 'order' => $testOrder]]);
538
+                    }
539
+                    return $default;
540
+                }
541
+            );
542
+
543
+        $this->appManager->expects($this->any())
544
+            ->method('isEnabledForUser')
545
+            ->with('theming')
546
+            ->willReturn(true);
547
+        $this->appManager->expects($this->once())->method('getAppInfo')->with('test')->willReturn($navigation);
548
+        $this->appManager->expects($this->once())->method('getAppIcon')->with('test')->willReturn('/apps/test/img/app.svg');
549
+        $this->l10nFac->expects($this->any())->method('get')->willReturn($l);
550
+        $this->urlGenerator->expects($this->any())->method('imagePath')->willReturnCallback(function ($appName, $file) {
551
+            return "/apps/$appName/img/$file";
552
+        });
553
+        $this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function ($route) {
554
+            if ($route === 'core.login.logout') {
555
+                return 'https://example.com/logout';
556
+            }
557
+            return '/apps/test/';
558
+        });
559
+        $user = $this->createMock(IUser::class);
560
+        $user->expects($this->any())->method('getUID')->willReturn('user001');
561
+        $this->userSession->expects($this->any())->method('getUser')->willReturn($user);
562
+        $this->userSession->expects($this->any())->method('isLoggedIn')->willReturn(true);
563
+        $this->appManager->expects($this->any())
564
+            ->method('getEnabledAppsForUser')
565
+            ->with($user)
566
+            ->willReturn(['test']);
567
+        $this->groupManager->expects($this->any())->method('isAdmin')->willReturn(false);
568
+        $subadmin = $this->createMock(SubAdmin::class);
569
+        $subadmin->expects($this->any())->method('isSubAdmin')->with($user)->willReturn(false);
570
+        $this->groupManager->expects($this->any())->method('getSubAdmin')->willReturn($subadmin);
571
+
572
+        $this->navigationManager->clear();
573
+        $this->dispatcher->expects($this->once())
574
+            ->method('dispatchTyped')
575
+            ->willReturnCallback(function ($event) {
576
+                $this->assertInstanceOf(LoadAdditionalEntriesEvent::class, $event);
577
+            });
578
+        $entries = $this->navigationManager->getAll();
579
+        $this->assertEquals($expected, $entries);
580
+    }
581
+
582
+    public static function provideDefaultEntries(): array {
583
+        return [
584
+            // none specified, default to files
585
+            [
586
+                '',
587
+                '',
588
+                '{}',
589
+                true,
590
+                'files',
591
+            ],
592
+            // none specified, without fallback
593
+            [
594
+                '',
595
+                '',
596
+                '{}',
597
+                false,
598
+                '',
599
+            ],
600
+            // unexisting or inaccessible app specified, default to files
601
+            [
602
+                'unexist',
603
+                '',
604
+                '{}',
605
+                true,
606
+                'files',
607
+            ],
608
+            // unexisting or inaccessible app specified, without fallbacks
609
+            [
610
+                'unexist',
611
+                '',
612
+                '{}',
613
+                false,
614
+                '',
615
+            ],
616
+            // non-standard app
617
+            [
618
+                'settings',
619
+                '',
620
+                '{}',
621
+                true,
622
+                'settings',
623
+            ],
624
+            // non-standard app, without fallback
625
+            [
626
+                'settings',
627
+                '',
628
+                '{}',
629
+                false,
630
+                'settings',
631
+            ],
632
+            // non-standard app with fallback
633
+            [
634
+                'unexist,settings',
635
+                '',
636
+                '{}',
637
+                true,
638
+                'settings',
639
+            ],
640
+            // system default app and user apporder
641
+            [
642
+                // system default is settings
643
+                'unexist,settings',
644
+                '',
645
+                // apporder says default app is files (order is lower)
646
+                '{"files_id":{"app":"files","order":1},"settings_id":{"app":"settings","order":2}}',
647
+                true,
648
+                // system default should override apporder
649
+                'settings'
650
+            ],
651
+            // user-customized defaultapp
652
+            [
653
+                '',
654
+                'files',
655
+                '',
656
+                true,
657
+                'files',
658
+            ],
659
+            // user-customized defaultapp with systemwide
660
+            [
661
+                'unexist,settings',
662
+                'files',
663
+                '',
664
+                true,
665
+                'files',
666
+            ],
667
+            // user-customized defaultapp with system wide and apporder
668
+            [
669
+                'unexist,settings',
670
+                'files',
671
+                '{"settings_id":{"app":"settings","order":1},"files_id":{"app":"files","order":2}}',
672
+                true,
673
+                'files',
674
+            ],
675
+            // user-customized apporder fallback
676
+            [
677
+                '',
678
+                '',
679
+                '{"settings_id":{"app":"settings","order":1},"files":{"app":"files","order":2}}',
680
+                true,
681
+                'settings',
682
+            ],
683
+            // 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))
684
+            [
685
+                '',
686
+                '',
687
+                '{"spreed":{"order":1},"files":{"app":"files","order":2}}',
688
+                true,
689
+                'files',
690
+            ],
691
+            // user-customized apporder, but called without fallback
692
+            [
693
+                '',
694
+                '',
695
+                '{"settings":{"app":"settings","order":1},"files":{"app":"files","order":2}}',
696
+                false,
697
+                '',
698
+            ],
699
+            // user-customized apporder with an app that has multiple routes
700
+            [
701
+                '',
702
+                '',
703
+                '{"settings_id":{"app":"settings","order":1},"settings_id_2":{"app":"settings","order":3},"id_files":{"app":"files","order":2}}',
704
+                true,
705
+                'settings',
706
+            ],
707
+            // closure navigation entries are also resolved
708
+            [
709
+                'closure2',
710
+                '',
711
+                '',
712
+                true,
713
+                'closure2',
714
+            ],
715
+            [
716
+                '',
717
+                'closure2',
718
+                '',
719
+                true,
720
+                'closure2',
721
+            ],
722
+            [
723
+                '',
724
+                '',
725
+                '{"closure2":{"order":1,"app":"closure2","href":"/closure2"}}',
726
+                true,
727
+                'closure2',
728
+            ],
729
+        ];
730
+    }
731
+
732
+    /**
733
+     * @dataProvider provideDefaultEntries
734
+     */
735
+    public function testGetDefaultEntryIdForUser(string $defaultApps, string $userDefaultApps, string $userApporder, bool $withFallbacks, string $expectedApp): void {
736
+        $this->navigationManager->add([
737
+            'id' => 'files',
738
+        ]);
739
+        $this->navigationManager->add([
740
+            'id' => 'settings',
741
+        ]);
742
+        $this->navigationManager->add(static function (): array {
743
+            return [
744
+                'id' => 'closure1',
745
+                'href' => '/closure1',
746
+            ];
747
+        });
748
+        $this->navigationManager->add(static function (): array {
749
+            return [
750
+                'id' => 'closure2',
751
+                'href' => '/closure2',
752
+            ];
753
+        });
754
+
755
+        $this->appManager->method('getEnabledApps')->willReturn([]);
756
+
757
+        $user = $this->createMock(IUser::class);
758
+        $user->method('getUID')->willReturn('user1');
759
+
760
+        $this->userSession->expects($this->atLeastOnce())
761
+            ->method('getUser')
762
+            ->willReturn($user);
763
+
764
+        $this->config->expects($this->atLeastOnce())
765
+            ->method('getSystemValueString')
766
+            ->with('defaultapp', $this->anything())
767
+            ->willReturn($defaultApps);
768
+
769
+        $this->config->expects($this->atLeastOnce())
770
+            ->method('getUserValue')
771
+            ->willReturnMap([
772
+                ['user1', 'core', 'defaultapp', '', $userDefaultApps],
773
+                ['user1', 'core', 'apporder', '[]', $userApporder],
774
+            ]);
775
+
776
+        $this->assertEquals($expectedApp, $this->navigationManager->getDefaultEntryIdForUser(null, $withFallbacks));
777
+    }
778
+
779
+    public function testDefaultEntryUpdated(): void {
780
+        $this->appManager->method('getEnabledApps')->willReturn([]);
781
+
782
+        $user = $this->createMock(IUser::class);
783
+        $user->method('getUID')->willReturn('user1');
784
+
785
+        $this->userSession
786
+            ->method('getUser')
787
+            ->willReturn($user);
788
+
789
+        $this->config
790
+            ->method('getSystemValueString')
791
+            ->with('defaultapp', $this->anything())
792
+            ->willReturn('app4,app3,app2,app1');
793
+
794
+        $this->config
795
+            ->method('getUserValue')
796
+            ->willReturnMap([
797
+                ['user1', 'core', 'defaultapp', '', ''],
798
+                ['user1', 'core', 'apporder', '[]', ''],
799
+            ]);
800
+
801
+        $this->navigationManager->add([
802
+            'id' => 'app1',
803
+        ]);
804
+
805
+        $this->assertEquals('app1', $this->navigationManager->getDefaultEntryIdForUser(null, false));
806
+        $this->assertEquals(true, $this->navigationManager->get('app1')['default']);
807
+
808
+        $this->navigationManager->add([
809
+            'id' => 'app3',
810
+        ]);
811
+
812
+        $this->assertEquals('app3', $this->navigationManager->getDefaultEntryIdForUser(null, false));
813
+        $this->assertEquals(false, $this->navigationManager->get('app1')['default']);
814
+        $this->assertEquals(true, $this->navigationManager->get('app3')['default']);
815
+
816
+        $this->navigationManager->add([
817
+            'id' => 'app2',
818
+        ]);
819
+
820
+        $this->assertEquals('app3', $this->navigationManager->getDefaultEntryIdForUser(null, false));
821
+        $this->assertEquals(false, $this->navigationManager->get('app1')['default']);
822
+        $this->assertEquals(false, $this->navigationManager->get('app2')['default']);
823
+        $this->assertEquals(true, $this->navigationManager->get('app3')['default']);
824
+
825
+        $this->navigationManager->add([
826
+            'id' => 'app4',
827
+        ]);
828
+
829
+        $this->assertEquals('app4', $this->navigationManager->getDefaultEntryIdForUser(null, false));
830
+        $this->assertEquals(false, $this->navigationManager->get('app1')['default']);
831
+        $this->assertEquals(false, $this->navigationManager->get('app2')['default']);
832
+        $this->assertEquals(false, $this->navigationManager->get('app3')['default']);
833
+        $this->assertEquals(true, $this->navigationManager->get('app4')['default']);
834
+    }
835 835
 }
Please login to merge, or discard this patch.
Spacing   +15 added lines, -15 removed lines patch added patch discarded remove patch
@@ -37,7 +37,7 @@  discard block
 block discarded – undo
37 37
 	/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
38 38
 	protected $config;
39 39
 
40
-	protected IEVentDispatcher|MockObject $dispatcher;
40
+	protected IEVentDispatcher | MockObject $dispatcher;
41 41
 
42 42
 	/** @var \OC\NavigationManager */
43 43
 	protected $navigationManager;
@@ -147,7 +147,7 @@  discard block
 block discarded – undo
147 147
 		global $testAddClosureNumberOfCalls;
148 148
 		$testAddClosureNumberOfCalls = 0;
149 149
 
150
-		$this->navigationManager->add(function () use ($entry) {
150
+		$this->navigationManager->add(function() use ($entry) {
151 151
 			global $testAddClosureNumberOfCalls;
152 152
 			$testAddClosureNumberOfCalls++;
153 153
 
@@ -199,7 +199,7 @@  discard block
 block discarded – undo
199 199
 		global $testAddClosureNumberOfCalls;
200 200
 		$testAddClosureNumberOfCalls = 0;
201 201
 
202
-		$this->navigationManager->add(function () use ($entry) {
202
+		$this->navigationManager->add(function() use ($entry) {
203 203
 			global $testAddClosureNumberOfCalls;
204 204
 			$testAddClosureNumberOfCalls++;
205 205
 
@@ -218,7 +218,7 @@  discard block
 block discarded – undo
218 218
 	 */
219 219
 	public function testWithAppManager($expected, $navigation, $isAdmin = false): void {
220 220
 		$l = $this->createMock(IL10N::class);
221
-		$l->expects($this->any())->method('t')->willReturnCallback(function ($text, $parameters = []) {
221
+		$l->expects($this->any())->method('t')->willReturnCallback(function($text, $parameters = []) {
222 222
 			return vsprintf($text, $parameters);
223 223
 		});
224 224
 
@@ -236,14 +236,14 @@  discard block
 block discarded – undo
236 236
 			->willReturn($navigation);
237 237
 		$this->urlGenerator->expects($this->any())
238 238
 			->method('imagePath')
239
-			->willReturnCallback(function ($appName, $file) {
239
+			->willReturnCallback(function($appName, $file) {
240 240
 				return "/apps/$appName/img/$file";
241 241
 			});
242 242
 		$this->appManager->expects($this->any())
243 243
 			->method('getAppIcon')
244 244
 			->willReturnCallback(fn (string $appName) => "/apps/$appName/img/app.svg");
245 245
 		$this->l10nFac->expects($this->any())->method('get')->willReturn($l);
246
-		$this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function ($route) {
246
+		$this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function($route) {
247 247
 			if ($route === 'core.login.logout') {
248 248
 				return 'https://example.com/logout';
249 249
 			}
@@ -265,7 +265,7 @@  discard block
 block discarded – undo
265 265
 		$this->navigationManager->clear();
266 266
 		$this->dispatcher->expects($this->once())
267 267
 			->method('dispatchTyped')
268
-			->willReturnCallback(function ($event) {
268
+			->willReturnCallback(function($event) {
269 269
 				$this->assertInstanceOf(LoadAdditionalEntriesEvent::class, $event);
270 270
 			});
271 271
 		$entries = $this->navigationManager->getAll('all');
@@ -323,7 +323,7 @@  discard block
 block discarded – undo
323 323
 			'logout' => [
324 324
 				'id' => 'logout',
325 325
 				'order' => 99999,
326
-				'href' => 'https://example.com/logout?requesttoken=' . urlencode(\OCP\Util::callRegister()),
326
+				'href' => 'https://example.com/logout?requesttoken='.urlencode(\OCP\Util::callRegister()),
327 327
 				'icon' => '/apps/core/img/actions/logout.svg',
328 328
 				'name' => 'Log out',
329 329
 				'active' => false,
@@ -503,7 +503,7 @@  discard block
 block discarded – undo
503 503
 
504 504
 	public function testWithAppManagerAndApporder(): void {
505 505
 		$l = $this->createMock(IL10N::class);
506
-		$l->expects($this->any())->method('t')->willReturnCallback(function ($text, $parameters = []) {
506
+		$l->expects($this->any())->method('t')->willReturnCallback(function($text, $parameters = []) {
507 507
 			return vsprintf($text, $parameters);
508 508
 		});
509 509
 
@@ -531,7 +531,7 @@  discard block
 block discarded – undo
531 531
 
532 532
 		$this->config->method('getUserValue')
533 533
 			->willReturnCallback(
534
-				function (string $userId, string $appName, string $key, mixed $default = '') use ($testOrder) {
534
+				function(string $userId, string $appName, string $key, mixed $default = '') use ($testOrder) {
535 535
 					$this->assertEquals('user001', $userId);
536 536
 					if ($key === 'apporder') {
537 537
 						return json_encode(['test' => ['app' => 'test', 'order' => $testOrder]]);
@@ -547,10 +547,10 @@  discard block
 block discarded – undo
547 547
 		$this->appManager->expects($this->once())->method('getAppInfo')->with('test')->willReturn($navigation);
548 548
 		$this->appManager->expects($this->once())->method('getAppIcon')->with('test')->willReturn('/apps/test/img/app.svg');
549 549
 		$this->l10nFac->expects($this->any())->method('get')->willReturn($l);
550
-		$this->urlGenerator->expects($this->any())->method('imagePath')->willReturnCallback(function ($appName, $file) {
550
+		$this->urlGenerator->expects($this->any())->method('imagePath')->willReturnCallback(function($appName, $file) {
551 551
 			return "/apps/$appName/img/$file";
552 552
 		});
553
-		$this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function ($route) {
553
+		$this->urlGenerator->expects($this->any())->method('linkToRoute')->willReturnCallback(function($route) {
554 554
 			if ($route === 'core.login.logout') {
555 555
 				return 'https://example.com/logout';
556 556
 			}
@@ -572,7 +572,7 @@  discard block
 block discarded – undo
572 572
 		$this->navigationManager->clear();
573 573
 		$this->dispatcher->expects($this->once())
574 574
 			->method('dispatchTyped')
575
-			->willReturnCallback(function ($event) {
575
+			->willReturnCallback(function($event) {
576 576
 				$this->assertInstanceOf(LoadAdditionalEntriesEvent::class, $event);
577 577
 			});
578 578
 		$entries = $this->navigationManager->getAll();
@@ -739,13 +739,13 @@  discard block
 block discarded – undo
739 739
 		$this->navigationManager->add([
740 740
 			'id' => 'settings',
741 741
 		]);
742
-		$this->navigationManager->add(static function (): array {
742
+		$this->navigationManager->add(static function(): array {
743 743
 			return [
744 744
 				'id' => 'closure1',
745 745
 				'href' => '/closure1',
746 746
 			];
747 747
 		});
748
-		$this->navigationManager->add(static function (): array {
748
+		$this->navigationManager->add(static function(): array {
749 749
 			return [
750 750
 				'id' => 'closure2',
751 751
 				'href' => '/closure2',
Please login to merge, or discard this patch.
lib/private/NavigationManager.php 1 patch
Indentation   +466 added lines, -466 removed lines patch added patch discarded remove patch
@@ -27,470 +27,470 @@
 block discarded – undo
27 27
  */
28 28
 
29 29
 class NavigationManager implements INavigationManager {
30
-	protected $entries = [];
31
-	protected $closureEntries = [];
32
-	protected $activeEntry;
33
-	protected $unreadCounters = [];
34
-
35
-	/** @var bool */
36
-	protected $init = false;
37
-	/** @var IAppManager|AppManager */
38
-	protected $appManager;
39
-	/** @var IURLGenerator */
40
-	private $urlGenerator;
41
-	/** @var IFactory */
42
-	private $l10nFac;
43
-	/** @var IUserSession */
44
-	private $userSession;
45
-	/** @var Manager */
46
-	private $groupManager;
47
-	/** @var IConfig */
48
-	private $config;
49
-	/** User defined app order (cached for the `add` function) */
50
-	private array $customAppOrder;
51
-	private LoggerInterface $logger;
52
-
53
-	public function __construct(
54
-		IAppManager $appManager,
55
-		IURLGenerator $urlGenerator,
56
-		IFactory $l10nFac,
57
-		IUserSession $userSession,
58
-		IGroupManager $groupManager,
59
-		IConfig $config,
60
-		LoggerInterface $logger,
61
-		protected IEventDispatcher $eventDispatcher,
62
-	) {
63
-		$this->appManager = $appManager;
64
-		$this->urlGenerator = $urlGenerator;
65
-		$this->l10nFac = $l10nFac;
66
-		$this->userSession = $userSession;
67
-		$this->groupManager = $groupManager;
68
-		$this->config = $config;
69
-		$this->logger = $logger;
70
-	}
71
-
72
-	/**
73
-	 * @inheritDoc
74
-	 */
75
-	public function add($entry) {
76
-		if ($entry instanceof \Closure) {
77
-			$this->closureEntries[] = $entry;
78
-			return;
79
-		}
80
-		$this->init(false);
81
-
82
-		$id = $entry['id'];
83
-
84
-		$entry['active'] = false;
85
-		$entry['unread'] = $this->unreadCounters[$id] ?? 0;
86
-		if (!isset($entry['icon'])) {
87
-			$entry['icon'] = '';
88
-		}
89
-		if (!isset($entry['classes'])) {
90
-			$entry['classes'] = '';
91
-		}
92
-		if (!isset($entry['type'])) {
93
-			$entry['type'] = 'link';
94
-		}
95
-
96
-		if ($entry['type'] === 'link') {
97
-			// app might not be set when using closures, in this case try to fallback to ID
98
-			if (!isset($entry['app']) && $this->appManager->isEnabledForUser($id)) {
99
-				$entry['app'] = $id;
100
-			}
101
-
102
-			// Set order from user defined app order
103
-			$entry['order'] = (int)($this->customAppOrder[$id]['order'] ?? $entry['order'] ?? 100);
104
-		}
105
-
106
-		$this->entries[$id] = $entry;
107
-
108
-		// Needs to be done after adding the new entry to account for the default entries containing this new entry.
109
-		$this->updateDefaultEntries();
110
-	}
111
-
112
-	private function updateDefaultEntries() {
113
-		$defaultEntryId = $this->getDefaultEntryIdForUser($this->userSession->getUser(), false);
114
-		foreach ($this->entries as $id => $entry) {
115
-			if ($entry['type'] === 'link') {
116
-				$this->entries[$id]['default'] = $id === $defaultEntryId;
117
-			}
118
-		}
119
-	}
120
-
121
-	/**
122
-	 * @inheritDoc
123
-	 */
124
-	public function getAll(string $type = 'link'): array {
125
-		$this->init();
126
-
127
-		$result = $this->entries;
128
-		if ($type !== 'all') {
129
-			$result = array_filter($this->entries, function ($entry) use ($type) {
130
-				return $entry['type'] === $type;
131
-			});
132
-		}
133
-
134
-		return $this->proceedNavigation($result, $type);
135
-	}
136
-
137
-	/**
138
-	 * Sort navigation entries default app is always sorted first, then by order, name and set active flag
139
-	 *
140
-	 * @param array $list
141
-	 * @return array
142
-	 */
143
-	private function proceedNavigation(array $list, string $type): array {
144
-		uasort($list, function ($a, $b) {
145
-			if (($a['default'] ?? false) xor ($b['default'] ?? false)) {
146
-				// Always sort the default app first
147
-				return ($a['default'] ?? false) ? -1 : 1;
148
-			} elseif (isset($a['order']) && isset($b['order'])) {
149
-				// Sort by order
150
-				return ($a['order'] < $b['order']) ? -1 : 1;
151
-			} elseif (isset($a['order']) || isset($b['order'])) {
152
-				// Sort the one that has an order property first
153
-				return isset($a['order']) ? -1 : 1;
154
-			} else {
155
-				// Sort by name otherwise
156
-				return ($a['name'] < $b['name']) ? -1 : 1;
157
-			}
158
-		});
159
-
160
-		if ($type === 'all' || $type === 'link') {
161
-			// There might be the case that no default app was set, in this case the first app is the default app.
162
-			// Otherwise the default app is already the ordered first, so setting the default prop will make no difference.
163
-			foreach ($list as $index => &$navEntry) {
164
-				if ($navEntry['type'] === 'link') {
165
-					$navEntry['default'] = true;
166
-					break;
167
-				}
168
-			}
169
-			unset($navEntry);
170
-		}
171
-
172
-		$activeEntry = $this->getActiveEntry();
173
-		if ($activeEntry !== null) {
174
-			foreach ($list as $index => &$navEntry) {
175
-				if ($navEntry['id'] == $activeEntry) {
176
-					$navEntry['active'] = true;
177
-				} else {
178
-					$navEntry['active'] = false;
179
-				}
180
-			}
181
-			unset($navEntry);
182
-		}
183
-
184
-		return $list;
185
-	}
186
-
187
-
188
-	/**
189
-	 * removes all the entries
190
-	 */
191
-	public function clear($loadDefaultLinks = true) {
192
-		$this->entries = [];
193
-		$this->closureEntries = [];
194
-		$this->init = !$loadDefaultLinks;
195
-	}
196
-
197
-	/**
198
-	 * @inheritDoc
199
-	 */
200
-	public function setActiveEntry($appId) {
201
-		$this->activeEntry = $appId;
202
-	}
203
-
204
-	/**
205
-	 * @inheritDoc
206
-	 */
207
-	public function getActiveEntry() {
208
-		return $this->activeEntry;
209
-	}
210
-
211
-	private function init(bool $resolveClosures = true): void {
212
-		if ($resolveClosures) {
213
-			while ($c = array_pop($this->closureEntries)) {
214
-				$this->add($c());
215
-			}
216
-		}
217
-
218
-		if ($this->init) {
219
-			return;
220
-		}
221
-		$this->init = true;
222
-
223
-		$l = $this->l10nFac->get('lib');
224
-		if ($this->config->getSystemValueBool('knowledgebaseenabled', true)) {
225
-			$this->add([
226
-				'type' => 'settings',
227
-				'id' => 'help',
228
-				'order' => 99998,
229
-				'href' => $this->urlGenerator->linkToRoute('settings.Help.help'),
230
-				'name' => $l->t('Help & privacy'),
231
-				'icon' => $this->urlGenerator->imagePath('settings', 'help.svg'),
232
-			]);
233
-		}
234
-
235
-		if ($this->userSession->isLoggedIn()) {
236
-			// Profile
237
-			$this->add([
238
-				'type' => 'settings',
239
-				'id' => 'profile',
240
-				'order' => 1,
241
-				'href' => $this->urlGenerator->linkToRoute(
242
-					'profile.ProfilePage.index',
243
-					['targetUserId' => $this->userSession->getUser()->getUID()],
244
-				),
245
-				'name' => $l->t('View profile'),
246
-			]);
247
-
248
-			// Accessibility settings
249
-			if ($this->appManager->isEnabledForUser('theming', $this->userSession->getUser())) {
250
-				$this->add([
251
-					'type' => 'settings',
252
-					'id' => 'accessibility_settings',
253
-					'order' => 2,
254
-					'href' => $this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'theming']),
255
-					'name' => $l->t('Appearance and accessibility'),
256
-					'icon' => $this->urlGenerator->imagePath('theming', 'accessibility-dark.svg'),
257
-				]);
258
-			}
259
-
260
-			if ($this->isAdmin()) {
261
-				// App management
262
-				$this->add([
263
-					'type' => 'settings',
264
-					'id' => 'core_apps',
265
-					'order' => 5,
266
-					'href' => $this->urlGenerator->linkToRoute('settings.AppSettings.viewApps'),
267
-					'icon' => $this->urlGenerator->imagePath('settings', 'apps.svg'),
268
-					'name' => $l->t('Apps'),
269
-				]);
270
-
271
-				// Personal settings
272
-				$this->add([
273
-					'type' => 'settings',
274
-					'id' => 'settings',
275
-					'order' => 3,
276
-					'href' => $this->urlGenerator->linkToRoute('settings.PersonalSettings.index'),
277
-					'name' => $l->t('Personal settings'),
278
-					'icon' => $this->urlGenerator->imagePath('settings', 'personal.svg'),
279
-				]);
280
-
281
-				// Admin settings
282
-				$this->add([
283
-					'type' => 'settings',
284
-					'id' => 'admin_settings',
285
-					'order' => 4,
286
-					'href' => $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'overview']),
287
-					'name' => $l->t('Administration settings'),
288
-					'icon' => $this->urlGenerator->imagePath('settings', 'admin.svg'),
289
-				]);
290
-			} else {
291
-				// Personal settings
292
-				$this->add([
293
-					'type' => 'settings',
294
-					'id' => 'settings',
295
-					'order' => 3,
296
-					'href' => $this->urlGenerator->linkToRoute('settings.PersonalSettings.index'),
297
-					'name' => $l->t('Settings'),
298
-					'icon' => $this->urlGenerator->imagePath('settings', 'admin.svg'),
299
-				]);
300
-			}
301
-
302
-			$logoutUrl = \OC_User::getLogoutUrl($this->urlGenerator);
303
-			if ($logoutUrl !== '') {
304
-				// Logout
305
-				$this->add([
306
-					'type' => 'settings',
307
-					'id' => 'logout',
308
-					'order' => 99999,
309
-					'href' => $logoutUrl,
310
-					'name' => $l->t('Log out'),
311
-					'icon' => $this->urlGenerator->imagePath('core', 'actions/logout.svg'),
312
-				]);
313
-			}
314
-
315
-			if ($this->isSubadmin()) {
316
-				// User management
317
-				$this->add([
318
-					'type' => 'settings',
319
-					'id' => 'core_users',
320
-					'order' => 6,
321
-					'href' => $this->urlGenerator->linkToRoute('settings.Users.usersList'),
322
-					'name' => $l->t('Accounts'),
323
-					'icon' => $this->urlGenerator->imagePath('settings', 'users.svg'),
324
-				]);
325
-			}
326
-		}
327
-		$this->eventDispatcher->dispatchTyped(new LoadAdditionalEntriesEvent());
328
-
329
-		if ($this->userSession->isLoggedIn()) {
330
-			$user = $this->userSession->getUser();
331
-			$apps = $this->appManager->getEnabledAppsForUser($user);
332
-			$this->customAppOrder = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags:JSON_THROW_ON_ERROR);
333
-		} else {
334
-			$apps = $this->appManager->getEnabledApps();
335
-			$this->customAppOrder = [];
336
-		}
337
-
338
-		foreach ($apps as $app) {
339
-			if (!$this->userSession->isLoggedIn() && !$this->appManager->isEnabledForUser($app, $this->userSession->getUser())) {
340
-				continue;
341
-			}
342
-
343
-			// load plugins and collections from info.xml
344
-			$info = $this->appManager->getAppInfo($app);
345
-			if (!isset($info['navigations']['navigation'])) {
346
-				continue;
347
-			}
348
-			foreach ($info['navigations']['navigation'] as $key => $nav) {
349
-				$nav['type'] = $nav['type'] ?? 'link';
350
-				if (!isset($nav['name'])) {
351
-					continue;
352
-				}
353
-				// Allow settings navigation items with no route entry, all other types require one
354
-				if (!isset($nav['route']) && $nav['type'] !== 'settings') {
355
-					continue;
356
-				}
357
-				$role = $nav['@attributes']['role'] ?? 'all';
358
-				if ($role === 'admin' && !$this->isAdmin()) {
359
-					continue;
360
-				}
361
-				$l = $this->l10nFac->get($app);
362
-				$id = $nav['id'] ?? $app . ($key === 0 ? '' : $key);
363
-				$order = $nav['order'] ?? 100;
364
-				$type = $nav['type'];
365
-				$route = !empty($nav['route']) ? $this->urlGenerator->linkToRoute($nav['route']) : '';
366
-				$icon = $nav['icon'] ?? null;
367
-				if ($icon !== null) {
368
-					try {
369
-						$icon = $this->urlGenerator->imagePath($app, $icon);
370
-					} catch (\RuntimeException $ex) {
371
-						// ignore
372
-					}
373
-				}
374
-				if ($icon === null) {
375
-					$icon = $this->appManager->getAppIcon($app);
376
-				}
377
-				if ($icon === null) {
378
-					$icon = $this->urlGenerator->imagePath('core', 'default-app-icon');
379
-				}
380
-
381
-				$this->add(array_merge([
382
-					// Navigation id
383
-					'id' => $id,
384
-					// Order where this entry should be shown
385
-					'order' => $order,
386
-					// Target of the navigation entry
387
-					'href' => $route,
388
-					// The icon used for the naviation entry
389
-					'icon' => $icon,
390
-					// Type of the navigation entry ('link' vs 'settings')
391
-					'type' => $type,
392
-					// Localized name of the navigation entry
393
-					'name' => $l->t($nav['name']),
394
-				], $type === 'link' ? [
395
-					// App that registered this navigation entry (not necessarly the same as the id)
396
-					'app' => $app,
397
-				] : []
398
-				));
399
-			}
400
-		}
401
-	}
402
-
403
-	private function isAdmin() {
404
-		$user = $this->userSession->getUser();
405
-		if ($user !== null) {
406
-			return $this->groupManager->isAdmin($user->getUID());
407
-		}
408
-		return false;
409
-	}
410
-
411
-	private function isSubadmin() {
412
-		$user = $this->userSession->getUser();
413
-		if ($user !== null) {
414
-			return $this->groupManager->getSubAdmin()->isSubAdmin($user);
415
-		}
416
-		return false;
417
-	}
418
-
419
-	public function setUnreadCounter(string $id, int $unreadCounter): void {
420
-		$this->unreadCounters[$id] = $unreadCounter;
421
-	}
422
-
423
-	public function get(string $id): ?array {
424
-		$this->init();
425
-		return $this->entries[$id];
426
-	}
427
-
428
-	public function getDefaultEntryIdForUser(?IUser $user = null, bool $withFallbacks = true): string {
429
-		$this->init();
430
-		// Disable fallbacks here, as we need to override them with the user defaults if none are configured.
431
-		$defaultEntryIds = $this->getDefaultEntryIds(false);
432
-
433
-		$user ??= $this->userSession->getUser();
434
-
435
-		if ($user !== null) {
436
-			$userDefaultEntryIds = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp'));
437
-			$defaultEntryIds = array_filter(array_merge($userDefaultEntryIds, $defaultEntryIds));
438
-			if (empty($defaultEntryIds) && $withFallbacks) {
439
-				/* Fallback on user defined apporder */
440
-				$customOrders = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags: JSON_THROW_ON_ERROR);
441
-				if (!empty($customOrders)) {
442
-					// filter only entries with app key (when added using closures or NavigationManager::add the app is not guaranteed to be set)
443
-					$customOrders = array_filter($customOrders, static fn ($entry) => isset($entry['app']));
444
-					// sort apps by order
445
-					usort($customOrders, static fn ($a, $b) => $a['order'] - $b['order']);
446
-					// set default apps to sorted apps
447
-					$defaultEntryIds = array_map(static fn ($entry) => $entry['app'], $customOrders);
448
-				}
449
-			}
450
-		}
451
-
452
-		if (empty($defaultEntryIds) && $withFallbacks) {
453
-			$defaultEntryIds = ['dashboard','files'];
454
-		}
455
-
456
-		$entryIds = array_keys($this->entries);
457
-
458
-		// Find the first app that is enabled for the current user
459
-		foreach ($defaultEntryIds as $defaultEntryId) {
460
-			if (in_array($defaultEntryId, $entryIds, true)) {
461
-				return $defaultEntryId;
462
-			}
463
-		}
464
-
465
-		// Set fallback to always-enabled files app
466
-		return $withFallbacks ? 'files' : '';
467
-	}
468
-
469
-	public function getDefaultEntryIds(bool $withFallbacks = true): array {
470
-		$this->init();
471
-		$storedIds = explode(',', $this->config->getSystemValueString('defaultapp', $withFallbacks ? 'dashboard,files' : ''));
472
-		$ids = [];
473
-		$entryIds = array_keys($this->entries);
474
-		foreach ($storedIds as $id) {
475
-			if (in_array($id, $entryIds, true)) {
476
-				$ids[] = $id;
477
-				break;
478
-			}
479
-		}
480
-		return array_filter($ids);
481
-	}
482
-
483
-	public function setDefaultEntryIds(array $ids): void {
484
-		$this->init();
485
-		$entryIds = array_keys($this->entries);
486
-
487
-		foreach ($ids as $id) {
488
-			if (!in_array($id, $entryIds, true)) {
489
-				$this->logger->debug('Cannot set unavailable entry as default entry', ['missing_entry' => $id]);
490
-				throw new InvalidArgumentException('Entry not available');
491
-			}
492
-		}
493
-
494
-		$this->config->setSystemValue('defaultapp', join(',', $ids));
495
-	}
30
+    protected $entries = [];
31
+    protected $closureEntries = [];
32
+    protected $activeEntry;
33
+    protected $unreadCounters = [];
34
+
35
+    /** @var bool */
36
+    protected $init = false;
37
+    /** @var IAppManager|AppManager */
38
+    protected $appManager;
39
+    /** @var IURLGenerator */
40
+    private $urlGenerator;
41
+    /** @var IFactory */
42
+    private $l10nFac;
43
+    /** @var IUserSession */
44
+    private $userSession;
45
+    /** @var Manager */
46
+    private $groupManager;
47
+    /** @var IConfig */
48
+    private $config;
49
+    /** User defined app order (cached for the `add` function) */
50
+    private array $customAppOrder;
51
+    private LoggerInterface $logger;
52
+
53
+    public function __construct(
54
+        IAppManager $appManager,
55
+        IURLGenerator $urlGenerator,
56
+        IFactory $l10nFac,
57
+        IUserSession $userSession,
58
+        IGroupManager $groupManager,
59
+        IConfig $config,
60
+        LoggerInterface $logger,
61
+        protected IEventDispatcher $eventDispatcher,
62
+    ) {
63
+        $this->appManager = $appManager;
64
+        $this->urlGenerator = $urlGenerator;
65
+        $this->l10nFac = $l10nFac;
66
+        $this->userSession = $userSession;
67
+        $this->groupManager = $groupManager;
68
+        $this->config = $config;
69
+        $this->logger = $logger;
70
+    }
71
+
72
+    /**
73
+     * @inheritDoc
74
+     */
75
+    public function add($entry) {
76
+        if ($entry instanceof \Closure) {
77
+            $this->closureEntries[] = $entry;
78
+            return;
79
+        }
80
+        $this->init(false);
81
+
82
+        $id = $entry['id'];
83
+
84
+        $entry['active'] = false;
85
+        $entry['unread'] = $this->unreadCounters[$id] ?? 0;
86
+        if (!isset($entry['icon'])) {
87
+            $entry['icon'] = '';
88
+        }
89
+        if (!isset($entry['classes'])) {
90
+            $entry['classes'] = '';
91
+        }
92
+        if (!isset($entry['type'])) {
93
+            $entry['type'] = 'link';
94
+        }
95
+
96
+        if ($entry['type'] === 'link') {
97
+            // app might not be set when using closures, in this case try to fallback to ID
98
+            if (!isset($entry['app']) && $this->appManager->isEnabledForUser($id)) {
99
+                $entry['app'] = $id;
100
+            }
101
+
102
+            // Set order from user defined app order
103
+            $entry['order'] = (int)($this->customAppOrder[$id]['order'] ?? $entry['order'] ?? 100);
104
+        }
105
+
106
+        $this->entries[$id] = $entry;
107
+
108
+        // Needs to be done after adding the new entry to account for the default entries containing this new entry.
109
+        $this->updateDefaultEntries();
110
+    }
111
+
112
+    private function updateDefaultEntries() {
113
+        $defaultEntryId = $this->getDefaultEntryIdForUser($this->userSession->getUser(), false);
114
+        foreach ($this->entries as $id => $entry) {
115
+            if ($entry['type'] === 'link') {
116
+                $this->entries[$id]['default'] = $id === $defaultEntryId;
117
+            }
118
+        }
119
+    }
120
+
121
+    /**
122
+     * @inheritDoc
123
+     */
124
+    public function getAll(string $type = 'link'): array {
125
+        $this->init();
126
+
127
+        $result = $this->entries;
128
+        if ($type !== 'all') {
129
+            $result = array_filter($this->entries, function ($entry) use ($type) {
130
+                return $entry['type'] === $type;
131
+            });
132
+        }
133
+
134
+        return $this->proceedNavigation($result, $type);
135
+    }
136
+
137
+    /**
138
+     * Sort navigation entries default app is always sorted first, then by order, name and set active flag
139
+     *
140
+     * @param array $list
141
+     * @return array
142
+     */
143
+    private function proceedNavigation(array $list, string $type): array {
144
+        uasort($list, function ($a, $b) {
145
+            if (($a['default'] ?? false) xor ($b['default'] ?? false)) {
146
+                // Always sort the default app first
147
+                return ($a['default'] ?? false) ? -1 : 1;
148
+            } elseif (isset($a['order']) && isset($b['order'])) {
149
+                // Sort by order
150
+                return ($a['order'] < $b['order']) ? -1 : 1;
151
+            } elseif (isset($a['order']) || isset($b['order'])) {
152
+                // Sort the one that has an order property first
153
+                return isset($a['order']) ? -1 : 1;
154
+            } else {
155
+                // Sort by name otherwise
156
+                return ($a['name'] < $b['name']) ? -1 : 1;
157
+            }
158
+        });
159
+
160
+        if ($type === 'all' || $type === 'link') {
161
+            // There might be the case that no default app was set, in this case the first app is the default app.
162
+            // Otherwise the default app is already the ordered first, so setting the default prop will make no difference.
163
+            foreach ($list as $index => &$navEntry) {
164
+                if ($navEntry['type'] === 'link') {
165
+                    $navEntry['default'] = true;
166
+                    break;
167
+                }
168
+            }
169
+            unset($navEntry);
170
+        }
171
+
172
+        $activeEntry = $this->getActiveEntry();
173
+        if ($activeEntry !== null) {
174
+            foreach ($list as $index => &$navEntry) {
175
+                if ($navEntry['id'] == $activeEntry) {
176
+                    $navEntry['active'] = true;
177
+                } else {
178
+                    $navEntry['active'] = false;
179
+                }
180
+            }
181
+            unset($navEntry);
182
+        }
183
+
184
+        return $list;
185
+    }
186
+
187
+
188
+    /**
189
+     * removes all the entries
190
+     */
191
+    public function clear($loadDefaultLinks = true) {
192
+        $this->entries = [];
193
+        $this->closureEntries = [];
194
+        $this->init = !$loadDefaultLinks;
195
+    }
196
+
197
+    /**
198
+     * @inheritDoc
199
+     */
200
+    public function setActiveEntry($appId) {
201
+        $this->activeEntry = $appId;
202
+    }
203
+
204
+    /**
205
+     * @inheritDoc
206
+     */
207
+    public function getActiveEntry() {
208
+        return $this->activeEntry;
209
+    }
210
+
211
+    private function init(bool $resolveClosures = true): void {
212
+        if ($resolveClosures) {
213
+            while ($c = array_pop($this->closureEntries)) {
214
+                $this->add($c());
215
+            }
216
+        }
217
+
218
+        if ($this->init) {
219
+            return;
220
+        }
221
+        $this->init = true;
222
+
223
+        $l = $this->l10nFac->get('lib');
224
+        if ($this->config->getSystemValueBool('knowledgebaseenabled', true)) {
225
+            $this->add([
226
+                'type' => 'settings',
227
+                'id' => 'help',
228
+                'order' => 99998,
229
+                'href' => $this->urlGenerator->linkToRoute('settings.Help.help'),
230
+                'name' => $l->t('Help & privacy'),
231
+                'icon' => $this->urlGenerator->imagePath('settings', 'help.svg'),
232
+            ]);
233
+        }
234
+
235
+        if ($this->userSession->isLoggedIn()) {
236
+            // Profile
237
+            $this->add([
238
+                'type' => 'settings',
239
+                'id' => 'profile',
240
+                'order' => 1,
241
+                'href' => $this->urlGenerator->linkToRoute(
242
+                    'profile.ProfilePage.index',
243
+                    ['targetUserId' => $this->userSession->getUser()->getUID()],
244
+                ),
245
+                'name' => $l->t('View profile'),
246
+            ]);
247
+
248
+            // Accessibility settings
249
+            if ($this->appManager->isEnabledForUser('theming', $this->userSession->getUser())) {
250
+                $this->add([
251
+                    'type' => 'settings',
252
+                    'id' => 'accessibility_settings',
253
+                    'order' => 2,
254
+                    'href' => $this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'theming']),
255
+                    'name' => $l->t('Appearance and accessibility'),
256
+                    'icon' => $this->urlGenerator->imagePath('theming', 'accessibility-dark.svg'),
257
+                ]);
258
+            }
259
+
260
+            if ($this->isAdmin()) {
261
+                // App management
262
+                $this->add([
263
+                    'type' => 'settings',
264
+                    'id' => 'core_apps',
265
+                    'order' => 5,
266
+                    'href' => $this->urlGenerator->linkToRoute('settings.AppSettings.viewApps'),
267
+                    'icon' => $this->urlGenerator->imagePath('settings', 'apps.svg'),
268
+                    'name' => $l->t('Apps'),
269
+                ]);
270
+
271
+                // Personal settings
272
+                $this->add([
273
+                    'type' => 'settings',
274
+                    'id' => 'settings',
275
+                    'order' => 3,
276
+                    'href' => $this->urlGenerator->linkToRoute('settings.PersonalSettings.index'),
277
+                    'name' => $l->t('Personal settings'),
278
+                    'icon' => $this->urlGenerator->imagePath('settings', 'personal.svg'),
279
+                ]);
280
+
281
+                // Admin settings
282
+                $this->add([
283
+                    'type' => 'settings',
284
+                    'id' => 'admin_settings',
285
+                    'order' => 4,
286
+                    'href' => $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'overview']),
287
+                    'name' => $l->t('Administration settings'),
288
+                    'icon' => $this->urlGenerator->imagePath('settings', 'admin.svg'),
289
+                ]);
290
+            } else {
291
+                // Personal settings
292
+                $this->add([
293
+                    'type' => 'settings',
294
+                    'id' => 'settings',
295
+                    'order' => 3,
296
+                    'href' => $this->urlGenerator->linkToRoute('settings.PersonalSettings.index'),
297
+                    'name' => $l->t('Settings'),
298
+                    'icon' => $this->urlGenerator->imagePath('settings', 'admin.svg'),
299
+                ]);
300
+            }
301
+
302
+            $logoutUrl = \OC_User::getLogoutUrl($this->urlGenerator);
303
+            if ($logoutUrl !== '') {
304
+                // Logout
305
+                $this->add([
306
+                    'type' => 'settings',
307
+                    'id' => 'logout',
308
+                    'order' => 99999,
309
+                    'href' => $logoutUrl,
310
+                    'name' => $l->t('Log out'),
311
+                    'icon' => $this->urlGenerator->imagePath('core', 'actions/logout.svg'),
312
+                ]);
313
+            }
314
+
315
+            if ($this->isSubadmin()) {
316
+                // User management
317
+                $this->add([
318
+                    'type' => 'settings',
319
+                    'id' => 'core_users',
320
+                    'order' => 6,
321
+                    'href' => $this->urlGenerator->linkToRoute('settings.Users.usersList'),
322
+                    'name' => $l->t('Accounts'),
323
+                    'icon' => $this->urlGenerator->imagePath('settings', 'users.svg'),
324
+                ]);
325
+            }
326
+        }
327
+        $this->eventDispatcher->dispatchTyped(new LoadAdditionalEntriesEvent());
328
+
329
+        if ($this->userSession->isLoggedIn()) {
330
+            $user = $this->userSession->getUser();
331
+            $apps = $this->appManager->getEnabledAppsForUser($user);
332
+            $this->customAppOrder = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags:JSON_THROW_ON_ERROR);
333
+        } else {
334
+            $apps = $this->appManager->getEnabledApps();
335
+            $this->customAppOrder = [];
336
+        }
337
+
338
+        foreach ($apps as $app) {
339
+            if (!$this->userSession->isLoggedIn() && !$this->appManager->isEnabledForUser($app, $this->userSession->getUser())) {
340
+                continue;
341
+            }
342
+
343
+            // load plugins and collections from info.xml
344
+            $info = $this->appManager->getAppInfo($app);
345
+            if (!isset($info['navigations']['navigation'])) {
346
+                continue;
347
+            }
348
+            foreach ($info['navigations']['navigation'] as $key => $nav) {
349
+                $nav['type'] = $nav['type'] ?? 'link';
350
+                if (!isset($nav['name'])) {
351
+                    continue;
352
+                }
353
+                // Allow settings navigation items with no route entry, all other types require one
354
+                if (!isset($nav['route']) && $nav['type'] !== 'settings') {
355
+                    continue;
356
+                }
357
+                $role = $nav['@attributes']['role'] ?? 'all';
358
+                if ($role === 'admin' && !$this->isAdmin()) {
359
+                    continue;
360
+                }
361
+                $l = $this->l10nFac->get($app);
362
+                $id = $nav['id'] ?? $app . ($key === 0 ? '' : $key);
363
+                $order = $nav['order'] ?? 100;
364
+                $type = $nav['type'];
365
+                $route = !empty($nav['route']) ? $this->urlGenerator->linkToRoute($nav['route']) : '';
366
+                $icon = $nav['icon'] ?? null;
367
+                if ($icon !== null) {
368
+                    try {
369
+                        $icon = $this->urlGenerator->imagePath($app, $icon);
370
+                    } catch (\RuntimeException $ex) {
371
+                        // ignore
372
+                    }
373
+                }
374
+                if ($icon === null) {
375
+                    $icon = $this->appManager->getAppIcon($app);
376
+                }
377
+                if ($icon === null) {
378
+                    $icon = $this->urlGenerator->imagePath('core', 'default-app-icon');
379
+                }
380
+
381
+                $this->add(array_merge([
382
+                    // Navigation id
383
+                    'id' => $id,
384
+                    // Order where this entry should be shown
385
+                    'order' => $order,
386
+                    // Target of the navigation entry
387
+                    'href' => $route,
388
+                    // The icon used for the naviation entry
389
+                    'icon' => $icon,
390
+                    // Type of the navigation entry ('link' vs 'settings')
391
+                    'type' => $type,
392
+                    // Localized name of the navigation entry
393
+                    'name' => $l->t($nav['name']),
394
+                ], $type === 'link' ? [
395
+                    // App that registered this navigation entry (not necessarly the same as the id)
396
+                    'app' => $app,
397
+                ] : []
398
+                ));
399
+            }
400
+        }
401
+    }
402
+
403
+    private function isAdmin() {
404
+        $user = $this->userSession->getUser();
405
+        if ($user !== null) {
406
+            return $this->groupManager->isAdmin($user->getUID());
407
+        }
408
+        return false;
409
+    }
410
+
411
+    private function isSubadmin() {
412
+        $user = $this->userSession->getUser();
413
+        if ($user !== null) {
414
+            return $this->groupManager->getSubAdmin()->isSubAdmin($user);
415
+        }
416
+        return false;
417
+    }
418
+
419
+    public function setUnreadCounter(string $id, int $unreadCounter): void {
420
+        $this->unreadCounters[$id] = $unreadCounter;
421
+    }
422
+
423
+    public function get(string $id): ?array {
424
+        $this->init();
425
+        return $this->entries[$id];
426
+    }
427
+
428
+    public function getDefaultEntryIdForUser(?IUser $user = null, bool $withFallbacks = true): string {
429
+        $this->init();
430
+        // Disable fallbacks here, as we need to override them with the user defaults if none are configured.
431
+        $defaultEntryIds = $this->getDefaultEntryIds(false);
432
+
433
+        $user ??= $this->userSession->getUser();
434
+
435
+        if ($user !== null) {
436
+            $userDefaultEntryIds = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp'));
437
+            $defaultEntryIds = array_filter(array_merge($userDefaultEntryIds, $defaultEntryIds));
438
+            if (empty($defaultEntryIds) && $withFallbacks) {
439
+                /* Fallback on user defined apporder */
440
+                $customOrders = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags: JSON_THROW_ON_ERROR);
441
+                if (!empty($customOrders)) {
442
+                    // filter only entries with app key (when added using closures or NavigationManager::add the app is not guaranteed to be set)
443
+                    $customOrders = array_filter($customOrders, static fn ($entry) => isset($entry['app']));
444
+                    // sort apps by order
445
+                    usort($customOrders, static fn ($a, $b) => $a['order'] - $b['order']);
446
+                    // set default apps to sorted apps
447
+                    $defaultEntryIds = array_map(static fn ($entry) => $entry['app'], $customOrders);
448
+                }
449
+            }
450
+        }
451
+
452
+        if (empty($defaultEntryIds) && $withFallbacks) {
453
+            $defaultEntryIds = ['dashboard','files'];
454
+        }
455
+
456
+        $entryIds = array_keys($this->entries);
457
+
458
+        // Find the first app that is enabled for the current user
459
+        foreach ($defaultEntryIds as $defaultEntryId) {
460
+            if (in_array($defaultEntryId, $entryIds, true)) {
461
+                return $defaultEntryId;
462
+            }
463
+        }
464
+
465
+        // Set fallback to always-enabled files app
466
+        return $withFallbacks ? 'files' : '';
467
+    }
468
+
469
+    public function getDefaultEntryIds(bool $withFallbacks = true): array {
470
+        $this->init();
471
+        $storedIds = explode(',', $this->config->getSystemValueString('defaultapp', $withFallbacks ? 'dashboard,files' : ''));
472
+        $ids = [];
473
+        $entryIds = array_keys($this->entries);
474
+        foreach ($storedIds as $id) {
475
+            if (in_array($id, $entryIds, true)) {
476
+                $ids[] = $id;
477
+                break;
478
+            }
479
+        }
480
+        return array_filter($ids);
481
+    }
482
+
483
+    public function setDefaultEntryIds(array $ids): void {
484
+        $this->init();
485
+        $entryIds = array_keys($this->entries);
486
+
487
+        foreach ($ids as $id) {
488
+            if (!in_array($id, $entryIds, true)) {
489
+                $this->logger->debug('Cannot set unavailable entry as default entry', ['missing_entry' => $id]);
490
+                throw new InvalidArgumentException('Entry not available');
491
+            }
492
+        }
493
+
494
+        $this->config->setSystemValue('defaultapp', join(',', $ids));
495
+    }
496 496
 }
Please login to merge, or discard this patch.
lib/private/URLGenerator.php 2 patches
Indentation   +309 added lines, -309 removed lines patch added patch discarded remove patch
@@ -24,313 +24,313 @@
 block discarded – undo
24 24
  * Class to generate URLs
25 25
  */
26 26
 class URLGenerator implements IURLGenerator {
27
-	/** @var IConfig */
28
-	private $config;
29
-	/** @var IUserSession */
30
-	public $userSession;
31
-	/** @var ICacheFactory */
32
-	private $cacheFactory;
33
-	/** @var IRequest */
34
-	private $request;
35
-	/** @var Router */
36
-	private $router;
37
-	/** @var null|string */
38
-	private $baseUrl = null;
39
-	private ?IAppManager $appManager = null;
40
-	private ?INavigationManager $navigationManager = null;
41
-
42
-	public function __construct(IConfig $config,
43
-		IUserSession $userSession,
44
-		ICacheFactory $cacheFactory,
45
-		IRequest $request,
46
-		Router $router,
47
-	) {
48
-		$this->config = $config;
49
-		$this->userSession = $userSession;
50
-		$this->cacheFactory = $cacheFactory;
51
-		$this->request = $request;
52
-		$this->router = $router;
53
-	}
54
-
55
-	private function getAppManager(): IAppManager {
56
-		if ($this->appManager !== null) {
57
-			return $this->appManager;
58
-		}
59
-		$this->appManager = \OCP\Server::get(IAppManager::class);
60
-		return $this->appManager;
61
-	}
62
-
63
-	private function getNavigationManager(): INavigationManager {
64
-		if ($this->navigationManager !== null) {
65
-			return $this->navigationManager;
66
-		}
67
-		$this->navigationManager = \OCP\Server::get(INavigationManager::class);
68
-		return $this->navigationManager;
69
-	}
70
-
71
-	/**
72
-	 * Creates an url using a defined route
73
-	 *
74
-	 * @param string $routeName
75
-	 * @param array $arguments args with param=>value, will be appended to the returned url
76
-	 * @return string the url
77
-	 *
78
-	 * Returns a url to the given route.
79
-	 */
80
-	public function linkToRoute(string $routeName, array $arguments = []): string {
81
-		return $this->router->generate($routeName, $arguments);
82
-	}
83
-
84
-	/**
85
-	 * Creates an absolute url using a defined route
86
-	 * @param string $routeName
87
-	 * @param array $arguments args with param=>value, will be appended to the returned url
88
-	 * @return string the url
89
-	 *
90
-	 * Returns an absolute url to the given route.
91
-	 */
92
-	public function linkToRouteAbsolute(string $routeName, array $arguments = []): string {
93
-		return $this->getAbsoluteURL($this->linkToRoute($routeName, $arguments));
94
-	}
95
-
96
-	public function linkToOCSRouteAbsolute(string $routeName, array $arguments = []): string {
97
-		// Returns `/subfolder/index.php/ocsapp/…` with `'htaccess.IgnoreFrontController' => false` in config.php
98
-		// And `/subfolder/ocsapp/…` with `'htaccess.IgnoreFrontController' => true` in config.php
99
-		$route = $this->router->generate('ocs.' . $routeName, $arguments, false);
100
-
101
-		// Cut off `/subfolder`
102
-		if (\OC::$WEBROOT !== '' && str_starts_with($route, \OC::$WEBROOT)) {
103
-			$route = substr($route, \strlen(\OC::$WEBROOT));
104
-		}
105
-
106
-		if (str_starts_with($route, '/index.php/')) {
107
-			$route = substr($route, 10);
108
-		}
109
-
110
-		// Remove `ocsapp/` bit
111
-		$route = substr($route, 7);
112
-		// Prefix with ocs/v2.php endpoint
113
-		$route = '/ocs/v2.php' . $route;
114
-
115
-		// Turn into an absolute URL
116
-		return $this->getAbsoluteURL($route);
117
-	}
118
-
119
-	/**
120
-	 * Creates an url
121
-	 *
122
-	 * @param string $appName app
123
-	 * @param string $file file
124
-	 * @param array $args array with param=>value, will be appended to the returned url
125
-	 *                    The value of $args will be urlencoded
126
-	 * @return string the url
127
-	 *
128
-	 * Returns a url to the given app and file.
129
-	 */
130
-	public function linkTo(string $appName, string $file, array $args = []): string {
131
-		$frontControllerActive = ($this->config->getSystemValueBool('htaccess.IgnoreFrontController', false) || getenv('front_controller_active') === 'true');
132
-
133
-		if ($appName !== '') {
134
-			$app_path = $this->getAppManager()->getAppPath($appName);
135
-			// Check if the app is in the app folder
136
-			if (file_exists($app_path . '/' . $file)) {
137
-				if (str_ends_with($file, 'php')) {
138
-					$urlLinkTo = \OC::$WEBROOT . '/index.php/apps/' . $appName;
139
-					if ($frontControllerActive) {
140
-						$urlLinkTo = \OC::$WEBROOT . '/apps/' . $appName;
141
-					}
142
-					$urlLinkTo .= ($file !== 'index.php') ? '/' . $file : '';
143
-				} else {
144
-					$urlLinkTo = $this->getAppManager()->getAppWebPath($appName) . '/' . $file;
145
-				}
146
-			} else {
147
-				$urlLinkTo = \OC::$WEBROOT . '/' . $appName . '/' . $file;
148
-			}
149
-		} else {
150
-			if (file_exists(\OC::$SERVERROOT . '/core/' . $file)) {
151
-				$urlLinkTo = \OC::$WEBROOT . '/core/' . $file;
152
-			} else {
153
-				if ($frontControllerActive && $file === 'index.php') {
154
-					$urlLinkTo = \OC::$WEBROOT . '/';
155
-				} else {
156
-					$urlLinkTo = \OC::$WEBROOT . '/' . $file;
157
-				}
158
-			}
159
-		}
160
-
161
-		if ($args && $query = http_build_query($args, '', '&')) {
162
-			$urlLinkTo .= '?' . $query;
163
-		}
164
-
165
-		return $urlLinkTo;
166
-	}
167
-
168
-	/**
169
-	 * Creates path to an image
170
-	 *
171
-	 * @param string $appName app
172
-	 * @param string $file image name
173
-	 * @throws \RuntimeException If the image does not exist
174
-	 * @return string the url
175
-	 *
176
-	 * Returns the path to the image.
177
-	 */
178
-	public function imagePath(string $appName, string $file): string {
179
-		$cache = $this->cacheFactory->createDistributed('imagePath-' . md5($this->getBaseUrl()) . '-');
180
-		$cacheKey = $appName . '-' . $file;
181
-		if ($key = $cache->get($cacheKey)) {
182
-			return $key;
183
-		}
184
-
185
-		// Read the selected theme from the config file
186
-		$theme = \OC_Util::getTheme();
187
-
188
-		//if a theme has a png but not an svg always use the png
189
-		$basename = substr(basename($file), 0, -4);
190
-
191
-		try {
192
-			$appPath = $this->getAppManager()->getAppPath($appName);
193
-		} catch (AppPathNotFoundException $e) {
194
-			if ($appName === 'core' || $appName === '') {
195
-				$appName = 'core';
196
-				$appPath = false;
197
-			} else {
198
-				throw new RuntimeException('image not found: image: ' . $file . ' webroot: ' . \OC::$WEBROOT . ' serverroot: ' . \OC::$SERVERROOT);
199
-			}
200
-		}
201
-
202
-		// Check if the app is in the app folder
203
-		$path = '';
204
-		$themingEnabled = $this->config->getSystemValueBool('installed', false) && $this->getAppManager()->isEnabledForUser('theming');
205
-		$themingImagePath = false;
206
-		if ($themingEnabled) {
207
-			$themingDefaults = \OC::$server->get('ThemingDefaults');
208
-			if ($themingDefaults instanceof ThemingDefaults) {
209
-				$themingImagePath = $themingDefaults->replaceImagePath($appName, $file);
210
-			}
211
-		}
212
-
213
-		if (file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$file")) {
214
-			$path = \OC::$WEBROOT . "/themes/$theme/apps/$appName/img/$file";
215
-		} elseif (!file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$basename.svg")
216
-			&& file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$basename.png")) {
217
-			$path = \OC::$WEBROOT . "/themes/$theme/apps/$appName/img/$basename.png";
218
-		} elseif (!empty($appName) and file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$file")) {
219
-			$path = \OC::$WEBROOT . "/themes/$theme/$appName/img/$file";
220
-		} elseif (!empty($appName) and (!file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$basename.svg")
221
-			&& file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$basename.png"))) {
222
-			$path = \OC::$WEBROOT . "/themes/$theme/$appName/img/$basename.png";
223
-		} elseif (file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$file")) {
224
-			$path = \OC::$WEBROOT . "/themes/$theme/core/img/$file";
225
-		} elseif (!file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$basename.svg")
226
-			&& file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$basename.png")) {
227
-			$path = \OC::$WEBROOT . "/themes/$theme/core/img/$basename.png";
228
-		} elseif ($themingEnabled && $themingImagePath) {
229
-			$path = $themingImagePath;
230
-		} elseif ($appPath && file_exists($appPath . "/img/$file")) {
231
-			$path = $this->getAppManager()->getAppWebPath($appName) . "/img/$file";
232
-		} elseif ($appPath && !file_exists($appPath . "/img/$basename.svg")
233
-			&& file_exists($appPath . "/img/$basename.png")) {
234
-			$path = $this->getAppManager()->getAppWebPath($appName) . "/img/$basename.png";
235
-		} elseif (!empty($appName) and file_exists(\OC::$SERVERROOT . "/$appName/img/$file")) {
236
-			$path = \OC::$WEBROOT . "/$appName/img/$file";
237
-		} elseif (!empty($appName) and (!file_exists(\OC::$SERVERROOT . "/$appName/img/$basename.svg")
238
-				&& file_exists(\OC::$SERVERROOT . "/$appName/img/$basename.png"))) {
239
-			$path = \OC::$WEBROOT . "/$appName/img/$basename.png";
240
-		} elseif (file_exists(\OC::$SERVERROOT . "/core/img/$file")) {
241
-			$path = \OC::$WEBROOT . "/core/img/$file";
242
-		} elseif (!file_exists(\OC::$SERVERROOT . "/core/img/$basename.svg")
243
-			&& file_exists(\OC::$SERVERROOT . "/core/img/$basename.png")) {
244
-			$path = \OC::$WEBROOT . "/themes/$theme/core/img/$basename.png";
245
-		}
246
-
247
-		if ($path !== '') {
248
-			$cache->set($cacheKey, $path);
249
-			return $path;
250
-		}
251
-
252
-		throw new RuntimeException('image not found: image:' . $file . ' webroot:' . \OC::$WEBROOT . ' serverroot:' . \OC::$SERVERROOT);
253
-	}
254
-
255
-
256
-	/**
257
-	 * Makes an URL absolute
258
-	 * @param string $url the url in the Nextcloud host
259
-	 * @return string the absolute version of the url
260
-	 */
261
-	public function getAbsoluteURL(string $url): string {
262
-		$separator = str_starts_with($url, '/') ? '' : '/';
263
-
264
-		if (\OC::$CLI && !\defined('PHPUNIT_RUN')) {
265
-			return rtrim($this->config->getSystemValueString('overwrite.cli.url'), '/') . '/' . ltrim($url, '/');
266
-		}
267
-		// The Nextcloud web root could already be prepended.
268
-		if (\OC::$WEBROOT !== '' && str_starts_with($url, \OC::$WEBROOT)) {
269
-			$url = substr($url, \strlen(\OC::$WEBROOT));
270
-		}
271
-
272
-		return $this->getBaseUrl() . $separator . $url;
273
-	}
274
-
275
-	/**
276
-	 * @param string $key
277
-	 * @return string url to the online documentation
278
-	 */
279
-	public function linkToDocs(string $key): string {
280
-		$theme = \OC::$server->get('ThemingDefaults');
281
-		return $theme->buildDocLinkToKey($key);
282
-	}
283
-
284
-	/**
285
-	 * Returns the URL of the default page based on the system configuration
286
-	 * and the apps visible for the current user
287
-	 * @return string
288
-	 */
289
-	public function linkToDefaultPageUrl(): string {
290
-		// Deny the redirect if the URL contains a @
291
-		// This prevents unvalidated redirects like ?redirect_url=:[email protected]
292
-		if (isset($_REQUEST['redirect_url']) && !str_contains($_REQUEST['redirect_url'], '@')) {
293
-			return $this->getAbsoluteURL(urldecode($_REQUEST['redirect_url']));
294
-		}
295
-
296
-		$defaultPage = $this->config->getAppValue('core', 'defaultpage');
297
-		if ($defaultPage) {
298
-			return $this->getAbsoluteURL($defaultPage);
299
-		}
300
-
301
-		$entryId = $this->getNavigationManager()->getDefaultEntryIdForUser();
302
-		$entry = $this->getNavigationManager()->get($entryId);
303
-		$href = (string)$entry['href'];
304
-		if ($href === '') {
305
-			throw new \InvalidArgumentException('Default navigation entry is missing href: ' . $entryId);
306
-		}
307
-
308
-		if (str_starts_with($href, $this->getBaseUrl())) {
309
-			return $href;
310
-		}
311
-
312
-		if (str_starts_with($href, '/index.php/') && ($this->config->getSystemValueBool('htaccess.IgnoreFrontController', false) || getenv('front_controller_active') === 'true')) {
313
-			$href = substr($href, 10);
314
-		}
315
-
316
-		return $this->getAbsoluteURL($href);
317
-	}
318
-
319
-	/**
320
-	 * @return string base url of the current request
321
-	 */
322
-	public function getBaseUrl(): string {
323
-		// BaseUrl can be equal to 'http(s)://' during the first steps of the initial setup.
324
-		if ($this->baseUrl === null || $this->baseUrl === 'http://' || $this->baseUrl === 'https://') {
325
-			$this->baseUrl = $this->request->getServerProtocol() . '://' . $this->request->getServerHost() . \OC::$WEBROOT;
326
-		}
327
-		return $this->baseUrl;
328
-	}
329
-
330
-	/**
331
-	 * @return string webroot part of the base url
332
-	 */
333
-	public function getWebroot(): string {
334
-		return \OC::$WEBROOT;
335
-	}
27
+    /** @var IConfig */
28
+    private $config;
29
+    /** @var IUserSession */
30
+    public $userSession;
31
+    /** @var ICacheFactory */
32
+    private $cacheFactory;
33
+    /** @var IRequest */
34
+    private $request;
35
+    /** @var Router */
36
+    private $router;
37
+    /** @var null|string */
38
+    private $baseUrl = null;
39
+    private ?IAppManager $appManager = null;
40
+    private ?INavigationManager $navigationManager = null;
41
+
42
+    public function __construct(IConfig $config,
43
+        IUserSession $userSession,
44
+        ICacheFactory $cacheFactory,
45
+        IRequest $request,
46
+        Router $router,
47
+    ) {
48
+        $this->config = $config;
49
+        $this->userSession = $userSession;
50
+        $this->cacheFactory = $cacheFactory;
51
+        $this->request = $request;
52
+        $this->router = $router;
53
+    }
54
+
55
+    private function getAppManager(): IAppManager {
56
+        if ($this->appManager !== null) {
57
+            return $this->appManager;
58
+        }
59
+        $this->appManager = \OCP\Server::get(IAppManager::class);
60
+        return $this->appManager;
61
+    }
62
+
63
+    private function getNavigationManager(): INavigationManager {
64
+        if ($this->navigationManager !== null) {
65
+            return $this->navigationManager;
66
+        }
67
+        $this->navigationManager = \OCP\Server::get(INavigationManager::class);
68
+        return $this->navigationManager;
69
+    }
70
+
71
+    /**
72
+     * Creates an url using a defined route
73
+     *
74
+     * @param string $routeName
75
+     * @param array $arguments args with param=>value, will be appended to the returned url
76
+     * @return string the url
77
+     *
78
+     * Returns a url to the given route.
79
+     */
80
+    public function linkToRoute(string $routeName, array $arguments = []): string {
81
+        return $this->router->generate($routeName, $arguments);
82
+    }
83
+
84
+    /**
85
+     * Creates an absolute url using a defined route
86
+     * @param string $routeName
87
+     * @param array $arguments args with param=>value, will be appended to the returned url
88
+     * @return string the url
89
+     *
90
+     * Returns an absolute url to the given route.
91
+     */
92
+    public function linkToRouteAbsolute(string $routeName, array $arguments = []): string {
93
+        return $this->getAbsoluteURL($this->linkToRoute($routeName, $arguments));
94
+    }
95
+
96
+    public function linkToOCSRouteAbsolute(string $routeName, array $arguments = []): string {
97
+        // Returns `/subfolder/index.php/ocsapp/…` with `'htaccess.IgnoreFrontController' => false` in config.php
98
+        // And `/subfolder/ocsapp/…` with `'htaccess.IgnoreFrontController' => true` in config.php
99
+        $route = $this->router->generate('ocs.' . $routeName, $arguments, false);
100
+
101
+        // Cut off `/subfolder`
102
+        if (\OC::$WEBROOT !== '' && str_starts_with($route, \OC::$WEBROOT)) {
103
+            $route = substr($route, \strlen(\OC::$WEBROOT));
104
+        }
105
+
106
+        if (str_starts_with($route, '/index.php/')) {
107
+            $route = substr($route, 10);
108
+        }
109
+
110
+        // Remove `ocsapp/` bit
111
+        $route = substr($route, 7);
112
+        // Prefix with ocs/v2.php endpoint
113
+        $route = '/ocs/v2.php' . $route;
114
+
115
+        // Turn into an absolute URL
116
+        return $this->getAbsoluteURL($route);
117
+    }
118
+
119
+    /**
120
+     * Creates an url
121
+     *
122
+     * @param string $appName app
123
+     * @param string $file file
124
+     * @param array $args array with param=>value, will be appended to the returned url
125
+     *                    The value of $args will be urlencoded
126
+     * @return string the url
127
+     *
128
+     * Returns a url to the given app and file.
129
+     */
130
+    public function linkTo(string $appName, string $file, array $args = []): string {
131
+        $frontControllerActive = ($this->config->getSystemValueBool('htaccess.IgnoreFrontController', false) || getenv('front_controller_active') === 'true');
132
+
133
+        if ($appName !== '') {
134
+            $app_path = $this->getAppManager()->getAppPath($appName);
135
+            // Check if the app is in the app folder
136
+            if (file_exists($app_path . '/' . $file)) {
137
+                if (str_ends_with($file, 'php')) {
138
+                    $urlLinkTo = \OC::$WEBROOT . '/index.php/apps/' . $appName;
139
+                    if ($frontControllerActive) {
140
+                        $urlLinkTo = \OC::$WEBROOT . '/apps/' . $appName;
141
+                    }
142
+                    $urlLinkTo .= ($file !== 'index.php') ? '/' . $file : '';
143
+                } else {
144
+                    $urlLinkTo = $this->getAppManager()->getAppWebPath($appName) . '/' . $file;
145
+                }
146
+            } else {
147
+                $urlLinkTo = \OC::$WEBROOT . '/' . $appName . '/' . $file;
148
+            }
149
+        } else {
150
+            if (file_exists(\OC::$SERVERROOT . '/core/' . $file)) {
151
+                $urlLinkTo = \OC::$WEBROOT . '/core/' . $file;
152
+            } else {
153
+                if ($frontControllerActive && $file === 'index.php') {
154
+                    $urlLinkTo = \OC::$WEBROOT . '/';
155
+                } else {
156
+                    $urlLinkTo = \OC::$WEBROOT . '/' . $file;
157
+                }
158
+            }
159
+        }
160
+
161
+        if ($args && $query = http_build_query($args, '', '&')) {
162
+            $urlLinkTo .= '?' . $query;
163
+        }
164
+
165
+        return $urlLinkTo;
166
+    }
167
+
168
+    /**
169
+     * Creates path to an image
170
+     *
171
+     * @param string $appName app
172
+     * @param string $file image name
173
+     * @throws \RuntimeException If the image does not exist
174
+     * @return string the url
175
+     *
176
+     * Returns the path to the image.
177
+     */
178
+    public function imagePath(string $appName, string $file): string {
179
+        $cache = $this->cacheFactory->createDistributed('imagePath-' . md5($this->getBaseUrl()) . '-');
180
+        $cacheKey = $appName . '-' . $file;
181
+        if ($key = $cache->get($cacheKey)) {
182
+            return $key;
183
+        }
184
+
185
+        // Read the selected theme from the config file
186
+        $theme = \OC_Util::getTheme();
187
+
188
+        //if a theme has a png but not an svg always use the png
189
+        $basename = substr(basename($file), 0, -4);
190
+
191
+        try {
192
+            $appPath = $this->getAppManager()->getAppPath($appName);
193
+        } catch (AppPathNotFoundException $e) {
194
+            if ($appName === 'core' || $appName === '') {
195
+                $appName = 'core';
196
+                $appPath = false;
197
+            } else {
198
+                throw new RuntimeException('image not found: image: ' . $file . ' webroot: ' . \OC::$WEBROOT . ' serverroot: ' . \OC::$SERVERROOT);
199
+            }
200
+        }
201
+
202
+        // Check if the app is in the app folder
203
+        $path = '';
204
+        $themingEnabled = $this->config->getSystemValueBool('installed', false) && $this->getAppManager()->isEnabledForUser('theming');
205
+        $themingImagePath = false;
206
+        if ($themingEnabled) {
207
+            $themingDefaults = \OC::$server->get('ThemingDefaults');
208
+            if ($themingDefaults instanceof ThemingDefaults) {
209
+                $themingImagePath = $themingDefaults->replaceImagePath($appName, $file);
210
+            }
211
+        }
212
+
213
+        if (file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$file")) {
214
+            $path = \OC::$WEBROOT . "/themes/$theme/apps/$appName/img/$file";
215
+        } elseif (!file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$basename.svg")
216
+            && file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$basename.png")) {
217
+            $path = \OC::$WEBROOT . "/themes/$theme/apps/$appName/img/$basename.png";
218
+        } elseif (!empty($appName) and file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$file")) {
219
+            $path = \OC::$WEBROOT . "/themes/$theme/$appName/img/$file";
220
+        } elseif (!empty($appName) and (!file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$basename.svg")
221
+            && file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$basename.png"))) {
222
+            $path = \OC::$WEBROOT . "/themes/$theme/$appName/img/$basename.png";
223
+        } elseif (file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$file")) {
224
+            $path = \OC::$WEBROOT . "/themes/$theme/core/img/$file";
225
+        } elseif (!file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$basename.svg")
226
+            && file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$basename.png")) {
227
+            $path = \OC::$WEBROOT . "/themes/$theme/core/img/$basename.png";
228
+        } elseif ($themingEnabled && $themingImagePath) {
229
+            $path = $themingImagePath;
230
+        } elseif ($appPath && file_exists($appPath . "/img/$file")) {
231
+            $path = $this->getAppManager()->getAppWebPath($appName) . "/img/$file";
232
+        } elseif ($appPath && !file_exists($appPath . "/img/$basename.svg")
233
+            && file_exists($appPath . "/img/$basename.png")) {
234
+            $path = $this->getAppManager()->getAppWebPath($appName) . "/img/$basename.png";
235
+        } elseif (!empty($appName) and file_exists(\OC::$SERVERROOT . "/$appName/img/$file")) {
236
+            $path = \OC::$WEBROOT . "/$appName/img/$file";
237
+        } elseif (!empty($appName) and (!file_exists(\OC::$SERVERROOT . "/$appName/img/$basename.svg")
238
+                && file_exists(\OC::$SERVERROOT . "/$appName/img/$basename.png"))) {
239
+            $path = \OC::$WEBROOT . "/$appName/img/$basename.png";
240
+        } elseif (file_exists(\OC::$SERVERROOT . "/core/img/$file")) {
241
+            $path = \OC::$WEBROOT . "/core/img/$file";
242
+        } elseif (!file_exists(\OC::$SERVERROOT . "/core/img/$basename.svg")
243
+            && file_exists(\OC::$SERVERROOT . "/core/img/$basename.png")) {
244
+            $path = \OC::$WEBROOT . "/themes/$theme/core/img/$basename.png";
245
+        }
246
+
247
+        if ($path !== '') {
248
+            $cache->set($cacheKey, $path);
249
+            return $path;
250
+        }
251
+
252
+        throw new RuntimeException('image not found: image:' . $file . ' webroot:' . \OC::$WEBROOT . ' serverroot:' . \OC::$SERVERROOT);
253
+    }
254
+
255
+
256
+    /**
257
+     * Makes an URL absolute
258
+     * @param string $url the url in the Nextcloud host
259
+     * @return string the absolute version of the url
260
+     */
261
+    public function getAbsoluteURL(string $url): string {
262
+        $separator = str_starts_with($url, '/') ? '' : '/';
263
+
264
+        if (\OC::$CLI && !\defined('PHPUNIT_RUN')) {
265
+            return rtrim($this->config->getSystemValueString('overwrite.cli.url'), '/') . '/' . ltrim($url, '/');
266
+        }
267
+        // The Nextcloud web root could already be prepended.
268
+        if (\OC::$WEBROOT !== '' && str_starts_with($url, \OC::$WEBROOT)) {
269
+            $url = substr($url, \strlen(\OC::$WEBROOT));
270
+        }
271
+
272
+        return $this->getBaseUrl() . $separator . $url;
273
+    }
274
+
275
+    /**
276
+     * @param string $key
277
+     * @return string url to the online documentation
278
+     */
279
+    public function linkToDocs(string $key): string {
280
+        $theme = \OC::$server->get('ThemingDefaults');
281
+        return $theme->buildDocLinkToKey($key);
282
+    }
283
+
284
+    /**
285
+     * Returns the URL of the default page based on the system configuration
286
+     * and the apps visible for the current user
287
+     * @return string
288
+     */
289
+    public function linkToDefaultPageUrl(): string {
290
+        // Deny the redirect if the URL contains a @
291
+        // This prevents unvalidated redirects like ?redirect_url=:[email protected]
292
+        if (isset($_REQUEST['redirect_url']) && !str_contains($_REQUEST['redirect_url'], '@')) {
293
+            return $this->getAbsoluteURL(urldecode($_REQUEST['redirect_url']));
294
+        }
295
+
296
+        $defaultPage = $this->config->getAppValue('core', 'defaultpage');
297
+        if ($defaultPage) {
298
+            return $this->getAbsoluteURL($defaultPage);
299
+        }
300
+
301
+        $entryId = $this->getNavigationManager()->getDefaultEntryIdForUser();
302
+        $entry = $this->getNavigationManager()->get($entryId);
303
+        $href = (string)$entry['href'];
304
+        if ($href === '') {
305
+            throw new \InvalidArgumentException('Default navigation entry is missing href: ' . $entryId);
306
+        }
307
+
308
+        if (str_starts_with($href, $this->getBaseUrl())) {
309
+            return $href;
310
+        }
311
+
312
+        if (str_starts_with($href, '/index.php/') && ($this->config->getSystemValueBool('htaccess.IgnoreFrontController', false) || getenv('front_controller_active') === 'true')) {
313
+            $href = substr($href, 10);
314
+        }
315
+
316
+        return $this->getAbsoluteURL($href);
317
+    }
318
+
319
+    /**
320
+     * @return string base url of the current request
321
+     */
322
+    public function getBaseUrl(): string {
323
+        // BaseUrl can be equal to 'http(s)://' during the first steps of the initial setup.
324
+        if ($this->baseUrl === null || $this->baseUrl === 'http://' || $this->baseUrl === 'https://') {
325
+            $this->baseUrl = $this->request->getServerProtocol() . '://' . $this->request->getServerHost() . \OC::$WEBROOT;
326
+        }
327
+        return $this->baseUrl;
328
+    }
329
+
330
+    /**
331
+     * @return string webroot part of the base url
332
+     */
333
+    public function getWebroot(): string {
334
+        return \OC::$WEBROOT;
335
+    }
336 336
 }
Please login to merge, or discard this patch.
Spacing   +52 added lines, -52 removed lines patch added patch discarded remove patch
@@ -96,7 +96,7 @@  discard block
 block discarded – undo
96 96
 	public function linkToOCSRouteAbsolute(string $routeName, array $arguments = []): string {
97 97
 		// Returns `/subfolder/index.php/ocsapp/…` with `'htaccess.IgnoreFrontController' => false` in config.php
98 98
 		// And `/subfolder/ocsapp/…` with `'htaccess.IgnoreFrontController' => true` in config.php
99
-		$route = $this->router->generate('ocs.' . $routeName, $arguments, false);
99
+		$route = $this->router->generate('ocs.'.$routeName, $arguments, false);
100 100
 
101 101
 		// Cut off `/subfolder`
102 102
 		if (\OC::$WEBROOT !== '' && str_starts_with($route, \OC::$WEBROOT)) {
@@ -110,7 +110,7 @@  discard block
 block discarded – undo
110 110
 		// Remove `ocsapp/` bit
111 111
 		$route = substr($route, 7);
112 112
 		// Prefix with ocs/v2.php endpoint
113
-		$route = '/ocs/v2.php' . $route;
113
+		$route = '/ocs/v2.php'.$route;
114 114
 
115 115
 		// Turn into an absolute URL
116 116
 		return $this->getAbsoluteURL($route);
@@ -133,33 +133,33 @@  discard block
 block discarded – undo
133 133
 		if ($appName !== '') {
134 134
 			$app_path = $this->getAppManager()->getAppPath($appName);
135 135
 			// Check if the app is in the app folder
136
-			if (file_exists($app_path . '/' . $file)) {
136
+			if (file_exists($app_path.'/'.$file)) {
137 137
 				if (str_ends_with($file, 'php')) {
138
-					$urlLinkTo = \OC::$WEBROOT . '/index.php/apps/' . $appName;
138
+					$urlLinkTo = \OC::$WEBROOT.'/index.php/apps/'.$appName;
139 139
 					if ($frontControllerActive) {
140
-						$urlLinkTo = \OC::$WEBROOT . '/apps/' . $appName;
140
+						$urlLinkTo = \OC::$WEBROOT.'/apps/'.$appName;
141 141
 					}
142
-					$urlLinkTo .= ($file !== 'index.php') ? '/' . $file : '';
142
+					$urlLinkTo .= ($file !== 'index.php') ? '/'.$file : '';
143 143
 				} else {
144
-					$urlLinkTo = $this->getAppManager()->getAppWebPath($appName) . '/' . $file;
144
+					$urlLinkTo = $this->getAppManager()->getAppWebPath($appName).'/'.$file;
145 145
 				}
146 146
 			} else {
147
-				$urlLinkTo = \OC::$WEBROOT . '/' . $appName . '/' . $file;
147
+				$urlLinkTo = \OC::$WEBROOT.'/'.$appName.'/'.$file;
148 148
 			}
149 149
 		} else {
150
-			if (file_exists(\OC::$SERVERROOT . '/core/' . $file)) {
151
-				$urlLinkTo = \OC::$WEBROOT . '/core/' . $file;
150
+			if (file_exists(\OC::$SERVERROOT.'/core/'.$file)) {
151
+				$urlLinkTo = \OC::$WEBROOT.'/core/'.$file;
152 152
 			} else {
153 153
 				if ($frontControllerActive && $file === 'index.php') {
154
-					$urlLinkTo = \OC::$WEBROOT . '/';
154
+					$urlLinkTo = \OC::$WEBROOT.'/';
155 155
 				} else {
156
-					$urlLinkTo = \OC::$WEBROOT . '/' . $file;
156
+					$urlLinkTo = \OC::$WEBROOT.'/'.$file;
157 157
 				}
158 158
 			}
159 159
 		}
160 160
 
161 161
 		if ($args && $query = http_build_query($args, '', '&')) {
162
-			$urlLinkTo .= '?' . $query;
162
+			$urlLinkTo .= '?'.$query;
163 163
 		}
164 164
 
165 165
 		return $urlLinkTo;
@@ -176,8 +176,8 @@  discard block
 block discarded – undo
176 176
 	 * Returns the path to the image.
177 177
 	 */
178 178
 	public function imagePath(string $appName, string $file): string {
179
-		$cache = $this->cacheFactory->createDistributed('imagePath-' . md5($this->getBaseUrl()) . '-');
180
-		$cacheKey = $appName . '-' . $file;
179
+		$cache = $this->cacheFactory->createDistributed('imagePath-'.md5($this->getBaseUrl()).'-');
180
+		$cacheKey = $appName.'-'.$file;
181 181
 		if ($key = $cache->get($cacheKey)) {
182 182
 			return $key;
183 183
 		}
@@ -195,7 +195,7 @@  discard block
 block discarded – undo
195 195
 				$appName = 'core';
196 196
 				$appPath = false;
197 197
 			} else {
198
-				throw new RuntimeException('image not found: image: ' . $file . ' webroot: ' . \OC::$WEBROOT . ' serverroot: ' . \OC::$SERVERROOT);
198
+				throw new RuntimeException('image not found: image: '.$file.' webroot: '.\OC::$WEBROOT.' serverroot: '.\OC::$SERVERROOT);
199 199
 			}
200 200
 		}
201 201
 
@@ -210,38 +210,38 @@  discard block
 block discarded – undo
210 210
 			}
211 211
 		}
212 212
 
213
-		if (file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$file")) {
214
-			$path = \OC::$WEBROOT . "/themes/$theme/apps/$appName/img/$file";
215
-		} elseif (!file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$basename.svg")
216
-			&& file_exists(\OC::$SERVERROOT . "/themes/$theme/apps/$appName/img/$basename.png")) {
217
-			$path = \OC::$WEBROOT . "/themes/$theme/apps/$appName/img/$basename.png";
218
-		} elseif (!empty($appName) and file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$file")) {
219
-			$path = \OC::$WEBROOT . "/themes/$theme/$appName/img/$file";
220
-		} elseif (!empty($appName) and (!file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$basename.svg")
221
-			&& file_exists(\OC::$SERVERROOT . "/themes/$theme/$appName/img/$basename.png"))) {
222
-			$path = \OC::$WEBROOT . "/themes/$theme/$appName/img/$basename.png";
223
-		} elseif (file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$file")) {
224
-			$path = \OC::$WEBROOT . "/themes/$theme/core/img/$file";
225
-		} elseif (!file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$basename.svg")
226
-			&& file_exists(\OC::$SERVERROOT . "/themes/$theme/core/img/$basename.png")) {
227
-			$path = \OC::$WEBROOT . "/themes/$theme/core/img/$basename.png";
213
+		if (file_exists(\OC::$SERVERROOT."/themes/$theme/apps/$appName/img/$file")) {
214
+			$path = \OC::$WEBROOT."/themes/$theme/apps/$appName/img/$file";
215
+		} elseif (!file_exists(\OC::$SERVERROOT."/themes/$theme/apps/$appName/img/$basename.svg")
216
+			&& file_exists(\OC::$SERVERROOT."/themes/$theme/apps/$appName/img/$basename.png")) {
217
+			$path = \OC::$WEBROOT."/themes/$theme/apps/$appName/img/$basename.png";
218
+		} elseif (!empty($appName) and file_exists(\OC::$SERVERROOT."/themes/$theme/$appName/img/$file")) {
219
+			$path = \OC::$WEBROOT."/themes/$theme/$appName/img/$file";
220
+		} elseif (!empty($appName) and (!file_exists(\OC::$SERVERROOT."/themes/$theme/$appName/img/$basename.svg")
221
+			&& file_exists(\OC::$SERVERROOT."/themes/$theme/$appName/img/$basename.png"))) {
222
+			$path = \OC::$WEBROOT."/themes/$theme/$appName/img/$basename.png";
223
+		} elseif (file_exists(\OC::$SERVERROOT."/themes/$theme/core/img/$file")) {
224
+			$path = \OC::$WEBROOT."/themes/$theme/core/img/$file";
225
+		} elseif (!file_exists(\OC::$SERVERROOT."/themes/$theme/core/img/$basename.svg")
226
+			&& file_exists(\OC::$SERVERROOT."/themes/$theme/core/img/$basename.png")) {
227
+			$path = \OC::$WEBROOT."/themes/$theme/core/img/$basename.png";
228 228
 		} elseif ($themingEnabled && $themingImagePath) {
229 229
 			$path = $themingImagePath;
230
-		} elseif ($appPath && file_exists($appPath . "/img/$file")) {
231
-			$path = $this->getAppManager()->getAppWebPath($appName) . "/img/$file";
232
-		} elseif ($appPath && !file_exists($appPath . "/img/$basename.svg")
233
-			&& file_exists($appPath . "/img/$basename.png")) {
234
-			$path = $this->getAppManager()->getAppWebPath($appName) . "/img/$basename.png";
235
-		} elseif (!empty($appName) and file_exists(\OC::$SERVERROOT . "/$appName/img/$file")) {
236
-			$path = \OC::$WEBROOT . "/$appName/img/$file";
237
-		} elseif (!empty($appName) and (!file_exists(\OC::$SERVERROOT . "/$appName/img/$basename.svg")
238
-				&& file_exists(\OC::$SERVERROOT . "/$appName/img/$basename.png"))) {
239
-			$path = \OC::$WEBROOT . "/$appName/img/$basename.png";
240
-		} elseif (file_exists(\OC::$SERVERROOT . "/core/img/$file")) {
241
-			$path = \OC::$WEBROOT . "/core/img/$file";
242
-		} elseif (!file_exists(\OC::$SERVERROOT . "/core/img/$basename.svg")
243
-			&& file_exists(\OC::$SERVERROOT . "/core/img/$basename.png")) {
244
-			$path = \OC::$WEBROOT . "/themes/$theme/core/img/$basename.png";
230
+		} elseif ($appPath && file_exists($appPath."/img/$file")) {
231
+			$path = $this->getAppManager()->getAppWebPath($appName)."/img/$file";
232
+		} elseif ($appPath && !file_exists($appPath."/img/$basename.svg")
233
+			&& file_exists($appPath."/img/$basename.png")) {
234
+			$path = $this->getAppManager()->getAppWebPath($appName)."/img/$basename.png";
235
+		} elseif (!empty($appName) and file_exists(\OC::$SERVERROOT."/$appName/img/$file")) {
236
+			$path = \OC::$WEBROOT."/$appName/img/$file";
237
+		} elseif (!empty($appName) and (!file_exists(\OC::$SERVERROOT."/$appName/img/$basename.svg")
238
+				&& file_exists(\OC::$SERVERROOT."/$appName/img/$basename.png"))) {
239
+			$path = \OC::$WEBROOT."/$appName/img/$basename.png";
240
+		} elseif (file_exists(\OC::$SERVERROOT."/core/img/$file")) {
241
+			$path = \OC::$WEBROOT."/core/img/$file";
242
+		} elseif (!file_exists(\OC::$SERVERROOT."/core/img/$basename.svg")
243
+			&& file_exists(\OC::$SERVERROOT."/core/img/$basename.png")) {
244
+			$path = \OC::$WEBROOT."/themes/$theme/core/img/$basename.png";
245 245
 		}
246 246
 
247 247
 		if ($path !== '') {
@@ -249,7 +249,7 @@  discard block
 block discarded – undo
249 249
 			return $path;
250 250
 		}
251 251
 
252
-		throw new RuntimeException('image not found: image:' . $file . ' webroot:' . \OC::$WEBROOT . ' serverroot:' . \OC::$SERVERROOT);
252
+		throw new RuntimeException('image not found: image:'.$file.' webroot:'.\OC::$WEBROOT.' serverroot:'.\OC::$SERVERROOT);
253 253
 	}
254 254
 
255 255
 
@@ -262,14 +262,14 @@  discard block
 block discarded – undo
262 262
 		$separator = str_starts_with($url, '/') ? '' : '/';
263 263
 
264 264
 		if (\OC::$CLI && !\defined('PHPUNIT_RUN')) {
265
-			return rtrim($this->config->getSystemValueString('overwrite.cli.url'), '/') . '/' . ltrim($url, '/');
265
+			return rtrim($this->config->getSystemValueString('overwrite.cli.url'), '/').'/'.ltrim($url, '/');
266 266
 		}
267 267
 		// The Nextcloud web root could already be prepended.
268 268
 		if (\OC::$WEBROOT !== '' && str_starts_with($url, \OC::$WEBROOT)) {
269 269
 			$url = substr($url, \strlen(\OC::$WEBROOT));
270 270
 		}
271 271
 
272
-		return $this->getBaseUrl() . $separator . $url;
272
+		return $this->getBaseUrl().$separator.$url;
273 273
 	}
274 274
 
275 275
 	/**
@@ -300,9 +300,9 @@  discard block
 block discarded – undo
300 300
 
301 301
 		$entryId = $this->getNavigationManager()->getDefaultEntryIdForUser();
302 302
 		$entry = $this->getNavigationManager()->get($entryId);
303
-		$href = (string)$entry['href'];
303
+		$href = (string) $entry['href'];
304 304
 		if ($href === '') {
305
-			throw new \InvalidArgumentException('Default navigation entry is missing href: ' . $entryId);
305
+			throw new \InvalidArgumentException('Default navigation entry is missing href: '.$entryId);
306 306
 		}
307 307
 
308 308
 		if (str_starts_with($href, $this->getBaseUrl())) {
@@ -322,7 +322,7 @@  discard block
 block discarded – undo
322 322
 	public function getBaseUrl(): string {
323 323
 		// BaseUrl can be equal to 'http(s)://' during the first steps of the initial setup.
324 324
 		if ($this->baseUrl === null || $this->baseUrl === 'http://' || $this->baseUrl === 'https://') {
325
-			$this->baseUrl = $this->request->getServerProtocol() . '://' . $this->request->getServerHost() . \OC::$WEBROOT;
325
+			$this->baseUrl = $this->request->getServerProtocol().'://'.$this->request->getServerHost().\OC::$WEBROOT;
326 326
 		}
327 327
 		return $this->baseUrl;
328 328
 	}
Please login to merge, or discard this patch.