Merge pull request #59089 from nextcloud/fix/expand-theming-capabilities

pull/59179/head
John Molakvoæ 2 months ago committed by GitHub
commit d677a3a5e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 36
      apps/theming/lib/Capabilities.php
  2. 7
      apps/theming/lib/Service/JSDataService.php
  3. 5
      apps/theming/lib/Service/ThemesService.php
  4. 2
      apps/theming/lib/ThemingDefaults.php
  5. 41
      apps/theming/openapi.json
  6. 173
      apps/theming/tests/CapabilitiesTest.php
  7. 9
      build/psalm-baseline.xml
  8. 41
      openapi.json

@ -8,8 +8,10 @@ namespace OCA\Theming;
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 @@ use OCP\IUserSession;
*/
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 @@ class Capabilities implements IPublicCapability {
* name: string,
* productName: string,
* url: string,
* imprintUrl: string,
* privacyUrl: string,
* slogan: string,
* color: string,
* color-text: string,
@ -57,6 +57,13 @@ class Capabilities implements IPublicCapability {
* background-default: bool,
* logoheader: string,
* favicon: string,
* primaryColor: string,
* backgroundColor: string,
* defaultPrimaryColor: string,
* defaultBackgroundColor: string,
* inverted: bool,
* cacheBuster: string,
* enabledThemes: list<string>,
* },
* }
*/
@ -64,7 +71,7 @@ class Capabilities implements IPublicCapability {
$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 @@ class Capabilities implements IPublicCapability {
$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 @@ class Capabilities implements IPublicCapability {
'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 @@ class Capabilities implements IPublicCapability {
'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(),
],
];
}

@ -11,6 +11,9 @@ namespace OCA\Theming\Service;
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 @@ class JSDataService implements \JsonSerializable {
'cacheBuster' => $this->util->getCacheBuster(),
'enabledThemes' => $this->themesService->getEnabledThemes(),
// deprecated use primaryColor
'color' => $this->themingDefaults->getColorPrimary(),
'' => 'color is deprecated since Nextcloud 29, use primaryColor instead'
];
}
}

@ -151,9 +151,9 @@ class ThemesService {
/**
* Get the list of all enabled themes IDs for the current user.
*
* @return string[]
* @return list<string>
*/
public function getEnabledThemes(): array {
public function getEnabledThemes() {
$enforcedTheme = $this->config->getSystemValueString('enforce_theme', '');
$user = $this->userSession->getUser();
if ($user === null) {
@ -163,6 +163,7 @@ class ThemesService {
return [];
}
/** @var list<string> */
$enabledThemes = json_decode($this->config->getUserValue($user->getUID(), Application::APP_ID, 'enabled-themes', '["default"]'));
if ($enforcedTheme !== '') {
return array_merge([$enforcedTheme], $enabledThemes);

@ -84,7 +84,7 @@ class ThemingDefaults extends \OC_Defaults {
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));
}

@ -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"
}
}
}
}

@ -9,12 +9,17 @@ namespace OCA\Theming\Tests;
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 @@ use Test\TestCase;
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 @@ class CapabilitiesTest extends TestCase {
$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 @@ class CapabilitiesTest extends TestCase {
'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 @@ class CapabilitiesTest extends TestCase {
'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 @@ class CapabilitiesTest extends TestCase {
'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 @@ class CapabilitiesTest extends TestCase {
'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<string, string> $expected
* @param array<string, mixed> $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 @@ class CapabilitiesTest extends TestCase {
$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 @@ class CapabilitiesTest extends TestCase {
$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 @@ class CapabilitiesTest extends TestCase {
->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 @@ class CapabilitiesTest extends TestCase {
$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']);
}
}

@ -2364,12 +2364,6 @@
<code><![CDATA[FakeTranslationProvider]]></code>
</DeprecatedInterface>
</file>
<file src="apps/theming/lib/Capabilities.php">
<DeprecatedMethod>
<code><![CDATA[getAppValue]]></code>
<code><![CDATA[getUserValue]]></code>
</DeprecatedMethod>
</file>
<file src="apps/theming/lib/Command/UpdateConfig.php">
<DeprecatedMethod>
<code><![CDATA[getAppValue]]></code>
@ -2414,6 +2408,9 @@
</DeprecatedMethod>
</file>
<file src="apps/theming/lib/Listener/BeforeTemplateRenderedListener.php">
<DeprecatedClass>
<code><![CDATA[JSDataService::class]]></code>
</DeprecatedClass>
<DeprecatedMethod>
<code><![CDATA[getUserValue]]></code>
</DeprecatedMethod>

@ -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"
}
}
}
}

Loading…
Cancel
Save