diff --git a/apps/dav/openapi.json b/apps/dav/openapi.json index c6ad08730c512..c0c6ed1f6af34 100644 --- a/apps/dav/openapi.json +++ b/apps/dav/openapi.json @@ -228,7 +228,8 @@ "schema": { "type": "object", "required": [ - "fileId" + "fileId", + "expirationTime" ], "properties": { "fileId": { diff --git a/apps/theming/lib/Capabilities.php b/apps/theming/lib/Capabilities.php index d5d6e415e75f3..9a9166c162881 100644 --- a/apps/theming/lib/Capabilities.php +++ b/apps/theming/lib/Capabilities.php @@ -8,8 +8,10 @@ use OCA\Theming\AppInfo\Application; use OCA\Theming\Service\BackgroundService; +use OCA\Theming\Service\ThemesService; use OCP\Capabilities\IPublicCapability; -use OCP\IConfig; +use OCP\Config\IUserConfig; +use OCP\IAppConfig; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; @@ -21,18 +23,14 @@ */ class Capabilities implements IPublicCapability { - /** - * @param ThemingDefaults $theming - * @param Util $util - * @param IURLGenerator $url - * @param IConfig $config - */ public function __construct( protected ThemingDefaults $theming, protected Util $util, protected IURLGenerator $url, - protected IConfig $config, + protected IAppConfig $appConfig, + protected IUserConfig $userConfig, protected IUserSession $userSession, + protected ThemesService $themesService, ) { } @@ -44,6 +42,8 @@ public function __construct( * name: string, * productName: string, * url: string, + * imprintUrl: string, + * privacyUrl: string, * slogan: string, * color: string, * color-text: string, @@ -57,6 +57,13 @@ public function __construct( * background-default: bool, * logoheader: string, * favicon: string, + * primaryColor: string, + * backgroundColor: string, + * defaultPrimaryColor: string, + * defaultBackgroundColor: string, + * inverted: bool, + * cacheBuster: string, + * enabledThemes: list, * }, * } */ @@ -64,7 +71,7 @@ public function getCapabilities() { $color = $this->theming->getDefaultColorPrimary(); $colorText = $this->util->invertTextColor($color) ? '#000000' : '#ffffff'; - $backgroundLogo = $this->config->getAppValue('theming', 'backgroundMime', ''); + $backgroundLogo = $this->appConfig->getValueString('theming', 'backgroundMime', ''); $backgroundColor = $this->theming->getColorBackground(); $backgroundText = $this->theming->getTextColorBackground(); $backgroundPlain = $backgroundLogo === 'backgroundColor' || ($backgroundLogo === '' && $backgroundColor !== BackgroundService::DEFAULT_COLOR); @@ -80,7 +87,7 @@ public function getCapabilities() { $color = $this->theming->getColorPrimary(); $colorText = $this->theming->getTextColorPrimary(); - $backgroundImage = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT); + $backgroundImage = $this->userConfig->getValueString($user->getUID(), Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT); if ($backgroundImage === BackgroundService::BACKGROUND_CUSTOM) { $backgroundPlain = false; $background = $this->url->linkToRouteAbsolute('theming.userTheme.getBackground'); @@ -98,6 +105,8 @@ public function getCapabilities() { 'name' => $this->theming->getName(), 'productName' => $this->theming->getProductName(), 'url' => $this->theming->getBaseUrl(), + 'imprintUrl' => $this->theming->getImprintUrl(), + 'privacyUrl' => $this->theming->getPrivacyUrl(), 'slogan' => $this->theming->getSlogan(), 'color' => $color, 'color-text' => $colorText, @@ -111,6 +120,13 @@ public function getCapabilities() { 'background-default' => !$this->util->isBackgroundThemed(), 'logoheader' => $this->url->getAbsoluteURL($this->theming->getLogo()), 'favicon' => $this->url->getAbsoluteURL($this->theming->getLogo()), + 'primaryColor' => $color, + 'backgroundColor' => $backgroundColor, + 'defaultPrimaryColor' => $this->theming->getDefaultColorPrimary(), + 'defaultBackgroundColor' => $this->theming->getDefaultColorBackground(), + 'inverted' => $this->util->invertTextColor($color), + 'cacheBuster' => $this->util->getCacheBuster(), + 'enabledThemes' => $this->themesService->getEnabledThemes(), ], ]; } diff --git a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php index 18ab9392b975e..9f54fdf247516 100644 --- a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php +++ b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php @@ -37,6 +37,7 @@ public function __construct( public function handle(Event $event): void { $this->initialState->provideLazyInitialState( 'data', + // @psalm-suppress DeprecatedClass - deprecated since 34 fn () => $this->container->get(JSDataService::class), ); diff --git a/apps/theming/lib/Service/JSDataService.php b/apps/theming/lib/Service/JSDataService.php index 81198f8b3f533..72bc7ab6be307 100644 --- a/apps/theming/lib/Service/JSDataService.php +++ b/apps/theming/lib/Service/JSDataService.php @@ -11,6 +11,9 @@ use OCA\Theming\ThemingDefaults; use OCA\Theming\Util; +/** + * @deprecated since Nextcloud 34 — all properties are now exposed via Capabilities + */ class JSDataService implements \JsonSerializable { public function __construct( @@ -40,10 +43,6 @@ public function jsonSerialize(): array { 'cacheBuster' => $this->util->getCacheBuster(), 'enabledThemes' => $this->themesService->getEnabledThemes(), - - // deprecated use primaryColor - 'color' => $this->themingDefaults->getColorPrimary(), - '' => 'color is deprecated since Nextcloud 29, use primaryColor instead' ]; } } diff --git a/apps/theming/lib/Service/ThemesService.php b/apps/theming/lib/Service/ThemesService.php index f49524cb62caf..3777f2ca2374b 100644 --- a/apps/theming/lib/Service/ThemesService.php +++ b/apps/theming/lib/Service/ThemesService.php @@ -151,7 +151,7 @@ public function isEnabled(ITheme $theme): bool { /** * Get the list of all enabled themes IDs for the current user. * - * @return string[] + * @return list */ public function getEnabledThemes(): array { $enforcedTheme = $this->config->getSystemValueString('enforce_theme', ''); diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php index c5b211a5b194a..b48adf0f38dca 100644 --- a/apps/theming/lib/ThemingDefaults.php +++ b/apps/theming/lib/ThemingDefaults.php @@ -84,7 +84,7 @@ public function getEntity() { return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::INSTANCE_NAME, $this->entity)); } - public function getProductName() { + public function getProductName(): string { return strip_tags($this->appConfig->getAppValueString(ConfigLexicon::PRODUCT_NAME, $this->productName)); } diff --git a/apps/theming/openapi.json b/apps/theming/openapi.json index 0ab33e2cd8135..beaf92477cda4 100644 --- a/apps/theming/openapi.json +++ b/apps/theming/openapi.json @@ -81,6 +81,8 @@ "name", "productName", "url", + "imprintUrl", + "privacyUrl", "slogan", "color", "color-text", @@ -93,7 +95,14 @@ "background-plain", "background-default", "logoheader", - "favicon" + "favicon", + "primaryColor", + "backgroundColor", + "defaultPrimaryColor", + "defaultBackgroundColor", + "inverted", + "cacheBuster", + "enabledThemes" ], "properties": { "name": { @@ -105,6 +114,12 @@ "url": { "type": "string" }, + "imprintUrl": { + "type": "string" + }, + "privacyUrl": { + "type": "string" + }, "slogan": { "type": "string" }, @@ -143,6 +158,30 @@ }, "favicon": { "type": "string" + }, + "primaryColor": { + "type": "string" + }, + "backgroundColor": { + "type": "string" + }, + "defaultPrimaryColor": { + "type": "string" + }, + "defaultBackgroundColor": { + "type": "string" + }, + "inverted": { + "type": "boolean" + }, + "cacheBuster": { + "type": "string" + }, + "enabledThemes": { + "type": "array", + "items": { + "type": "string" + } } } } diff --git a/apps/theming/tests/CapabilitiesTest.php b/apps/theming/tests/CapabilitiesTest.php index 7201301d1087e..a3ae8191efaf5 100644 --- a/apps/theming/tests/CapabilitiesTest.php +++ b/apps/theming/tests/CapabilitiesTest.php @@ -9,12 +9,17 @@ use OCA\Theming\Capabilities; use OCA\Theming\ImageManager; +use OCA\Theming\Service\BackgroundService; +use OCA\Theming\Service\ThemesService; use OCA\Theming\ThemingDefaults; use OCA\Theming\Util; use OCP\App\IAppManager; +use OCP\Config\IUserConfig; use OCP\Files\IAppData; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IURLGenerator; +use OCP\IUser; use OCP\IUserSession; use OCP\ServerVersion; use PHPUnit\Framework\MockObject\MockObject; @@ -28,9 +33,11 @@ class CapabilitiesTest extends TestCase { protected ThemingDefaults&MockObject $theming; protected IURLGenerator&MockObject $url; - protected IConfig&MockObject $config; + protected IAppConfig&MockObject $appConfig; + protected IUserConfig&MockObject $userConfig; protected Util&MockObject $util; - protected IUserSession $userSession; + protected IUserSession&MockObject $userSession; + protected ThemesService&MockObject $themesService; protected Capabilities $capabilities; protected function setUp(): void { @@ -38,24 +45,30 @@ protected function setUp(): void { $this->theming = $this->createMock(ThemingDefaults::class); $this->url = $this->createMock(IURLGenerator::class); - $this->config = $this->createMock(IConfig::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->userConfig = $this->createMock(IUserConfig::class); $this->util = $this->createMock(Util::class); $this->userSession = $this->createMock(IUserSession::class); + $this->themesService = $this->createMock(ThemesService::class); $this->capabilities = new Capabilities( $this->theming, $this->util, $this->url, - $this->config, + $this->appConfig, + $this->userConfig, $this->userSession, + $this->themesService, ); } public static function dataGetCapabilities(): array { return [ - ['name', 'url', 'slogan', '#FFFFFF', '#000000', 'logo', 'background', '#fff', '#000', 'http://absolute/', true, [ + ['name', 'url', 'slogan', '#FFFFFF', '#000000', 'logo', 'background', '#fff', '#000', 'http://absolute/', true, 'https://imprint.example.com/', 'https://privacy.example.com/', '#0082c9', [ 'name' => 'name', 'productName' => 'name', 'url' => 'url', + 'imprintUrl' => 'https://imprint.example.com/', + 'privacyUrl' => 'https://privacy.example.com/', 'slogan' => 'slogan', 'color' => '#FFFFFF', 'color-text' => '#000000', @@ -69,11 +82,20 @@ public static function dataGetCapabilities(): array { 'background-default' => false, 'logoheader' => 'http://absolute/logo', 'favicon' => 'http://absolute/logo', + 'primaryColor' => '#FFFFFF', + 'backgroundColor' => '#fff', + 'defaultPrimaryColor' => '#FFFFFF', + 'defaultBackgroundColor' => '#0082c9', + 'inverted' => true, + 'cacheBuster' => 'v1', + 'enabledThemes' => ['default'], ]], - ['name1', 'url2', 'slogan3', '#01e4a0', '#ffffff', 'logo5', 'background6', '#fff', '#000', 'http://localhost/', false, [ + ['name1', 'url2', 'slogan3', '#01e4a0', '#ffffff', 'logo5', 'background6', '#fff', '#000', 'http://localhost/', false, '', '', '#0082c9', [ 'name' => 'name1', 'productName' => 'name1', 'url' => 'url2', + 'imprintUrl' => '', + 'privacyUrl' => '', 'slogan' => 'slogan3', 'color' => '#01e4a0', 'color-text' => '#ffffff', @@ -87,11 +109,20 @@ public static function dataGetCapabilities(): array { 'background-default' => true, 'logoheader' => 'http://localhost/logo5', 'favicon' => 'http://localhost/logo5', + 'primaryColor' => '#01e4a0', + 'backgroundColor' => '#fff', + 'defaultPrimaryColor' => '#01e4a0', + 'defaultBackgroundColor' => '#0082c9', + 'inverted' => false, + 'cacheBuster' => 'v1', + 'enabledThemes' => ['default'], ]], - ['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', '#000000', '#ffffff', 'http://localhost/', true, [ + ['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', '#000000', '#ffffff', 'http://localhost/', true, '', '', '#0082c9', [ 'name' => 'name1', 'productName' => 'name1', 'url' => 'url2', + 'imprintUrl' => '', + 'privacyUrl' => '', 'slogan' => 'slogan3', 'color' => '#000000', 'color-text' => '#ffffff', @@ -105,11 +136,20 @@ public static function dataGetCapabilities(): array { 'background-default' => false, 'logoheader' => 'http://localhost/logo5', 'favicon' => 'http://localhost/logo5', + 'primaryColor' => '#000000', + 'backgroundColor' => '#000000', + 'defaultPrimaryColor' => '#000000', + 'defaultBackgroundColor' => '#0082c9', + 'inverted' => false, + 'cacheBuster' => 'v1', + 'enabledThemes' => ['default'], ]], - ['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', '#000000', '#ffffff', 'http://localhost/', false, [ + ['name1', 'url2', 'slogan3', '#000000', '#ffffff', 'logo5', 'backgroundColor', '#000000', '#ffffff', 'http://localhost/', false, '', '', '#0082c9', [ 'name' => 'name1', 'productName' => 'name1', 'url' => 'url2', + 'imprintUrl' => '', + 'privacyUrl' => '', 'slogan' => 'slogan3', 'color' => '#000000', 'color-text' => '#ffffff', @@ -123,17 +163,25 @@ public static function dataGetCapabilities(): array { 'background-default' => true, 'logoheader' => 'http://localhost/logo5', 'favicon' => 'http://localhost/logo5', + 'primaryColor' => '#000000', + 'backgroundColor' => '#000000', + 'defaultPrimaryColor' => '#000000', + 'defaultBackgroundColor' => '#0082c9', + 'inverted' => false, + 'cacheBuster' => 'v1', + 'enabledThemes' => ['default'], ]], ]; } /** - * @param non-empty-array $expected + * @param array $expected */ #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataGetCapabilities')] - public function testGetCapabilities(string $name, string $url, string $slogan, string $color, string $textColor, string $logo, string $background, string $backgroundColor, string $backgroundTextColor, string $baseUrl, bool $backgroundThemed, array $expected): void { - $this->config->expects($this->once()) - ->method('getAppValue') + public function testGetCapabilities(string $name, string $url, string $slogan, string $color, string $textColor, string $logo, string $background, string $backgroundColor, string $backgroundTextColor, string $baseUrl, bool $backgroundThemed, string $imprintUrl, string $privacyUrl, string $defaultBackgroundColor, array $expected): void { + $this->appConfig->expects($this->once()) + ->method('getValueString') + ->with('theming', 'backgroundMime', '') ->willReturn($background); $this->theming->expects($this->once()) ->method('getName') @@ -144,6 +192,12 @@ public function testGetCapabilities(string $name, string $url, string $slogan, s $this->theming->expects($this->once()) ->method('getBaseUrl') ->willReturn($url); + $this->theming->expects($this->once()) + ->method('getImprintUrl') + ->willReturn($imprintUrl); + $this->theming->expects($this->once()) + ->method('getPrivacyUrl') + ->willReturn($privacyUrl); $this->theming->expects($this->once()) ->method('getSlogan') ->willReturn($slogan); @@ -153,6 +207,9 @@ public function testGetCapabilities(string $name, string $url, string $slogan, s $this->theming->expects($this->once()) ->method('getTextColorBackground') ->willReturn($backgroundTextColor); + $this->theming->expects($this->once()) + ->method('getDefaultColorBackground') + ->willReturn($defaultBackgroundColor); $this->theming->expects($this->atLeast(1)) ->method('getDefaultColorPrimary') ->willReturn($color); @@ -160,20 +217,25 @@ public function testGetCapabilities(string $name, string $url, string $slogan, s ->method('getLogo') ->willReturn($logo); - $util = new Util($this->createMock(ServerVersion::class), $this->config, $this->createMock(IAppManager::class), $this->createMock(IAppData::class), $this->createMock(ImageManager::class)); + $util = new Util($this->createMock(ServerVersion::class), $this->createMock(IConfig::class), $this->createMock(IAppManager::class), $this->createMock(IAppData::class), $this->createMock(ImageManager::class)); $this->util->expects($this->exactly(3)) ->method('elementColor') ->with($color) ->willReturnCallback(static function (string $color, ?bool $brightBackground = null) use ($util) { return $util->elementColor($color, $brightBackground); }); - $this->util->expects($this->any()) ->method('invertTextColor') ->willReturnCallback(fn () => $textColor === '#000000'); $this->util->expects($this->once()) ->method('isBackgroundThemed') ->willReturn($backgroundThemed); + $this->util->expects($this->once()) + ->method('getCacheBuster') + ->willReturn('v1'); + $this->themesService->expects($this->once()) + ->method('getEnabledThemes') + ->willReturn(['default']); if ($background !== 'backgroundColor') { $this->theming->expects($this->once()) @@ -194,4 +256,87 @@ public function testGetCapabilities(string $name, string $url, string $slogan, s $this->assertEquals(['theming' => $expected], $this->capabilities->getCapabilities()); } + + public static function dataGetCapabilitiesWithUser(): array { + return [ + 'default background' => [ + BackgroundService::BACKGROUND_DEFAULT, + false, + 'http://localhost/background', + ], + 'custom background' => [ + BackgroundService::BACKGROUND_CUSTOM, + false, + 'http://localhost/route', + ], + 'shipped background' => [ + 'jo-myoung-hee-fluid.webp', + false, + 'http://localhost/img', + ], + 'solid color background' => [ + 'solid', + true, + BackgroundService::DEFAULT_COLOR, + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataGetCapabilitiesWithUser')] + public function testGetCapabilitiesWithUser(string $backgroundImage, bool $expectedBackgroundPlain, string $expectedBackground): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user1'); + $this->userSession->method('getUser')->willReturn($user); + + $userColor = '#00679e'; + $defaultColor = '#0082c9'; + + $this->theming->method('getDefaultColorPrimary')->willReturn($defaultColor); + $this->theming->method('getColorPrimary')->willReturn($userColor); + $this->theming->method('getTextColorPrimary')->willReturn('#ffffff'); + $this->theming->method('getName')->willReturn('Name'); + $this->theming->method('getProductName')->willReturn('Name'); + $this->theming->method('getBaseUrl')->willReturn('http://example.com/'); + $this->theming->method('getImprintUrl')->willReturn(''); + $this->theming->method('getPrivacyUrl')->willReturn(''); + $this->theming->method('getSlogan')->willReturn('Slogan'); + $this->theming->method('getColorBackground')->willReturn(BackgroundService::DEFAULT_COLOR); + $this->theming->method('getTextColorBackground')->willReturn('#ffffff'); + $this->theming->method('getDefaultColorBackground')->willReturn('#0082c9'); + $this->theming->method('getLogo')->willReturn('/logo'); + $this->theming->method('getBackground')->willReturn('/background'); + + $this->appConfig->method('getValueString')->willReturn(''); + $this->userConfig->method('getValueString')->willReturn($backgroundImage); + + $this->util->method('invertTextColor')->willReturn(false); + $this->util->method('elementColor')->willReturn($userColor); + $this->util->method('isBackgroundThemed')->willReturn(false); + $this->util->method('getCacheBuster')->willReturn('v1'); + + $this->themesService->method('getEnabledThemes')->willReturn(['default']); + + $this->url->method('getAbsoluteURL')->willReturnCallback(fn (string $url) => 'http://localhost' . $url); + $this->url->method('linkToRouteAbsolute')->willReturn('http://localhost/route'); + $this->url->method('linkTo')->willReturn('http://localhost/img'); + + $result = $this->capabilities->getCapabilities(); + $theming = $result['theming']; + + // For logged-in users, color/primaryColor reflect getColorPrimary(), not getDefaultColorPrimary() + $this->assertSame($userColor, $theming['color']); + $this->assertSame($userColor, $theming['primaryColor']); + // color-text comes from getTextColorPrimary() directly, not invertTextColor() + $this->assertSame('#ffffff', $theming['color-text']); + // inverted uses invertTextColor() with the user's active color + $this->assertSame(false, $theming['inverted']); + // defaultPrimaryColor always reflects the admin-configured default + $this->assertSame($defaultColor, $theming['defaultPrimaryColor']); + // Background varies by user's background_image setting + $this->assertSame($expectedBackgroundPlain, $theming['background-plain']); + $this->assertSame($expectedBackground, $theming['background']); + // New fields are always present + $this->assertSame('v1', $theming['cacheBuster']); + $this->assertSame(['default'], $theming['enabledThemes']); + } } diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 217b1f8c278d5..67baf509378d1 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -2370,12 +2370,6 @@ - - - - - - diff --git a/openapi.json b/openapi.json index 51be5472bbc5a..45accaf8b1aca 100644 --- a/openapi.json +++ b/openapi.json @@ -4225,6 +4225,8 @@ "name", "productName", "url", + "imprintUrl", + "privacyUrl", "slogan", "color", "color-text", @@ -4237,7 +4239,14 @@ "background-plain", "background-default", "logoheader", - "favicon" + "favicon", + "primaryColor", + "backgroundColor", + "defaultPrimaryColor", + "defaultBackgroundColor", + "inverted", + "cacheBuster", + "enabledThemes" ], "properties": { "name": { @@ -4249,6 +4258,12 @@ "url": { "type": "string" }, + "imprintUrl": { + "type": "string" + }, + "privacyUrl": { + "type": "string" + }, "slogan": { "type": "string" }, @@ -4287,6 +4302,30 @@ }, "favicon": { "type": "string" + }, + "primaryColor": { + "type": "string" + }, + "backgroundColor": { + "type": "string" + }, + "defaultPrimaryColor": { + "type": "string" + }, + "defaultBackgroundColor": { + "type": "string" + }, + "inverted": { + "type": "boolean" + }, + "cacheBuster": { + "type": "string" + }, + "enabledThemes": { + "type": "array", + "items": { + "type": "string" + } } } } @@ -18027,7 +18066,8 @@ "schema": { "type": "object", "required": [ - "fileId" + "fileId", + "expirationTime" ], "properties": { "fileId": {