Skip to content

Commit 531862d

Browse files
committed
fix: Use migration instead of repair step for restoring custom color
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent c7d9068 commit 531862d

File tree

7 files changed

+315
-74
lines changed

7 files changed

+315
-74
lines changed

apps/theming/appinfo/info.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
<repair-steps>
2828
<post-migration>
2929
<step>OCA\Theming\Migration\InitBackgroundImagesMigration</step>
30-
<step>OCA\Theming\Migration\SeparatePrimaryColorAndBackground</step>
3130
</post-migration>
3231
</repair-steps>
3332

apps/theming/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717
'OCA\\Theming\\IconBuilder' => $baseDir . '/../lib/IconBuilder.php',
1818
'OCA\\Theming\\ImageManager' => $baseDir . '/../lib/ImageManager.php',
1919
'OCA\\Theming\\Jobs\\MigrateBackgroundImages' => $baseDir . '/../lib/Jobs/MigrateBackgroundImages.php',
20+
'OCA\\Theming\\Jobs\\RestoreBackgroundImageColor' => $baseDir . '/../lib/Jobs/RestoreBackgroundImageColor.php',
2021
'OCA\\Theming\\Listener\\BeforePreferenceListener' => $baseDir . '/../lib/Listener/BeforePreferenceListener.php',
2122
'OCA\\Theming\\Listener\\BeforeTemplateRenderedListener' => $baseDir . '/../lib/Listener/BeforeTemplateRenderedListener.php',
2223
'OCA\\Theming\\Migration\\InitBackgroundImagesMigration' => $baseDir . '/../lib/Migration/InitBackgroundImagesMigration.php',
23-
'OCA\\Theming\\Migration\\SeparatePrimaryColorAndBackground' => $baseDir . '/../lib/Migration/SeparatePrimaryColorAndBackground.php',
24+
'OCA\\Theming\\Migration\\Version2006Date20240905111627' => $baseDir . '/../lib/Migration/Version2006Date20240905111627.php',
2425
'OCA\\Theming\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
2526
'OCA\\Theming\\Service\\BackgroundService' => $baseDir . '/../lib/Service/BackgroundService.php',
2627
'OCA\\Theming\\Service\\JSDataService' => $baseDir . '/../lib/Service/JSDataService.php',

apps/theming/composer/composer/autoload_static.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ class ComposerStaticInitTheming
3232
'OCA\\Theming\\IconBuilder' => __DIR__ . '/..' . '/../lib/IconBuilder.php',
3333
'OCA\\Theming\\ImageManager' => __DIR__ . '/..' . '/../lib/ImageManager.php',
3434
'OCA\\Theming\\Jobs\\MigrateBackgroundImages' => __DIR__ . '/..' . '/../lib/Jobs/MigrateBackgroundImages.php',
35+
'OCA\\Theming\\Jobs\\RestoreBackgroundImageColor' => __DIR__ . '/..' . '/../lib/Jobs/RestoreBackgroundImageColor.php',
3536
'OCA\\Theming\\Listener\\BeforePreferenceListener' => __DIR__ . '/..' . '/../lib/Listener/BeforePreferenceListener.php',
3637
'OCA\\Theming\\Listener\\BeforeTemplateRenderedListener' => __DIR__ . '/..' . '/../lib/Listener/BeforeTemplateRenderedListener.php',
3738
'OCA\\Theming\\Migration\\InitBackgroundImagesMigration' => __DIR__ . '/..' . '/../lib/Migration/InitBackgroundImagesMigration.php',
38-
'OCA\\Theming\\Migration\\SeparatePrimaryColorAndBackground' => __DIR__ . '/..' . '/../lib/Migration/SeparatePrimaryColorAndBackground.php',
39+
'OCA\\Theming\\Migration\\Version2006Date20240905111627' => __DIR__ . '/..' . '/../lib/Migration/Version2006Date20240905111627.php',
3940
'OCA\\Theming\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
4041
'OCA\\Theming\\Service\\BackgroundService' => __DIR__ . '/..' . '/../lib/Service/BackgroundService.php',
4142
'OCA\\Theming\\Service\\JSDataService' => __DIR__ . '/..' . '/../lib/Service/JSDataService.php',
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Theming\Jobs;
11+
12+
use OCA\Theming\AppInfo\Application;
13+
use OCA\Theming\Service\BackgroundService;
14+
use OCP\AppFramework\Utility\ITimeFactory;
15+
use OCP\BackgroundJob\IJobList;
16+
use OCP\BackgroundJob\QueuedJob;
17+
use OCP\Files\IAppData;
18+
use OCP\Files\NotFoundException;
19+
use OCP\Files\NotPermittedException;
20+
use OCP\IConfig;
21+
use OCP\IDBConnection;
22+
use Psr\Log\LoggerInterface;
23+
24+
class RestoreBackgroundImageColor extends QueuedJob {
25+
26+
public const STAGE_PREPARE = 'prepare';
27+
public const STAGE_EXECUTE = 'execute';
28+
// will be saved in appdata/theming/global/
29+
protected const STATE_FILE_NAME = '30_background_image_color_restoration.json';
30+
31+
public function __construct(
32+
ITimeFactory $time,
33+
private IConfig $config,
34+
private IAppData $appData,
35+
private IJobList $jobList,
36+
private IDBConnection $dbc,
37+
private LoggerInterface $logger,
38+
private BackgroundService $service,
39+
) {
40+
parent::__construct($time);
41+
}
42+
43+
protected function run(mixed $argument): void {
44+
if (!is_array($argument) || !isset($argument['stage'])) {
45+
throw new \Exception('Job '.self::class.' called with wrong argument');
46+
}
47+
48+
switch ($argument['stage']) {
49+
case self::STAGE_PREPARE:
50+
$this->runPreparation();
51+
break;
52+
case self::STAGE_EXECUTE:
53+
$this->runMigration();
54+
break;
55+
default:
56+
break;
57+
}
58+
}
59+
60+
protected function runPreparation(): void {
61+
try {
62+
$qb = $this->dbc->getQueryBuilder();
63+
$qb2 = $this->dbc->getQueryBuilder();
64+
65+
$innerSQL = $qb2->select('userid')
66+
->from('preferences')
67+
->where($qb2->expr()->eq('configkey', $qb->createNamedParameter('background_color')));
68+
69+
// Get those users, that have a background_image set - not the default, but no background_color.
70+
$result = $qb->selectDistinct('a.userid')
71+
->from('preferences', 'a')
72+
->leftJoin('a', $qb->createFunction('('.$innerSQL->getSQL().')'), 'b', 'a.userid = b.userid')
73+
->where($qb2->expr()->eq('a.configkey', $qb->createNamedParameter('background_image')))
74+
->andWhere($qb2->expr()->neq('a.configvalue', $qb->createNamedParameter(BackgroundService::BACKGROUND_DEFAULT)))
75+
->andWhere($qb2->expr()->isNull('b.userid'))
76+
->executeQuery();
77+
78+
$userIds = $result->fetchAll(\PDO::FETCH_COLUMN);
79+
$this->logger->info('Prepare to restore background information for {users} users', ['users' => count($userIds)]);
80+
$this->storeUserIdsToProcess($userIds);
81+
} catch (\Throwable $t) {
82+
$this->jobList->add(self::class, ['stage' => self::STAGE_PREPARE]);
83+
throw $t;
84+
}
85+
$this->jobList->add(self::class, ['stage' => self::STAGE_EXECUTE]);
86+
}
87+
88+
/**
89+
* @throws NotPermittedException
90+
* @throws NotFoundException
91+
*/
92+
protected function runMigration(): void {
93+
$allUserIds = $this->readUserIdsToProcess();
94+
$notSoFastMode = count($allUserIds) > 1000;
95+
96+
$userIds = array_slice($allUserIds, 0, 1000);
97+
foreach ($userIds as $userId) {
98+
$backgroundColor = $this->config->getUserValue($userId, Application::APP_ID, 'background_color');
99+
if ($backgroundColor !== '') {
100+
continue;
101+
}
102+
103+
$background = $this->config->getUserValue($userId, Application::APP_ID, 'background_image');
104+
switch($background) {
105+
case BackgroundService::BACKGROUND_DEFAULT:
106+
$this->service->setDefaultBackground($userId);
107+
break;
108+
case BackgroundService::BACKGROUND_COLOR:
109+
break;
110+
case BackgroundService::BACKGROUND_CUSTOM:
111+
$this->service->recalculateMeanColor($userId);
112+
break;
113+
default:
114+
// shipped backgrounds
115+
// do not alter primary color
116+
$primary = $this->config->getUserValue($userId, Application::APP_ID, 'primary_color');
117+
if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$background])) {
118+
$this->service->setShippedBackground($background, $userId);
119+
} else {
120+
$this->service->setDefaultBackground($userId);
121+
}
122+
// Restore primary
123+
if ($primary !== '') {
124+
$this->config->setUserValue($userId, Application::APP_ID, 'primary_color', $primary);
125+
}
126+
}
127+
}
128+
129+
if ($notSoFastMode) {
130+
$remainingUserIds = array_slice($allUserIds, 1000);
131+
$this->storeUserIdsToProcess($remainingUserIds);
132+
$this->jobList->add(self::class, ['stage' => self::STAGE_EXECUTE]);
133+
} else {
134+
$this->deleteStateFile();
135+
}
136+
}
137+
138+
/**
139+
* @throws NotPermittedException
140+
* @throws NotFoundException
141+
*/
142+
protected function readUserIdsToProcess(): array {
143+
$globalFolder = $this->appData->getFolder('global');
144+
if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
145+
$file = $globalFolder->getFile(self::STATE_FILE_NAME);
146+
try {
147+
$userIds = \json_decode($file->getContent(), true);
148+
} catch (NotFoundException $e) {
149+
$userIds = [];
150+
}
151+
if ($userIds === null) {
152+
$userIds = [];
153+
}
154+
} else {
155+
$userIds = [];
156+
}
157+
return $userIds;
158+
}
159+
160+
/**
161+
* @throws NotFoundException
162+
*/
163+
protected function storeUserIdsToProcess(array $userIds): void {
164+
$storableUserIds = \json_encode($userIds);
165+
$globalFolder = $this->appData->getFolder('global');
166+
try {
167+
if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
168+
$file = $globalFolder->getFile(self::STATE_FILE_NAME);
169+
} else {
170+
$file = $globalFolder->newFile(self::STATE_FILE_NAME);
171+
}
172+
$file->putContent($storableUserIds);
173+
} catch (NotFoundException $e) {
174+
} catch (NotPermittedException $e) {
175+
$this->logger->warning('Lacking permissions to create {file}',
176+
[
177+
'app' => 'theming',
178+
'file' => self::STATE_FILE_NAME,
179+
'exception' => $e,
180+
]
181+
);
182+
}
183+
}
184+
185+
/**
186+
* @throws NotFoundException
187+
*/
188+
protected function deleteStateFile(): void {
189+
$globalFolder = $this->appData->getFolder('global');
190+
if ($globalFolder->fileExists(self::STATE_FILE_NAME)) {
191+
$file = $globalFolder->getFile(self::STATE_FILE_NAME);
192+
try {
193+
$file->delete();
194+
} catch (NotPermittedException $e) {
195+
$this->logger->info('Could not delete {file} due to permissions. It is safe to delete manually inside data -> appdata -> theming -> global.',
196+
[
197+
'app' => 'theming',
198+
'file' => $file->getName(),
199+
'exception' => $e,
200+
]
201+
);
202+
}
203+
}
204+
}
205+
}

apps/theming/lib/Migration/SeparatePrimaryColorAndBackground.php

Lines changed: 0 additions & 63 deletions
This file was deleted.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Theming\Migration;
11+
12+
use Closure;
13+
use OCA\Theming\AppInfo\Application;
14+
use OCA\Theming\Jobs\RestoreBackgroundImageColor;
15+
use OCP\BackgroundJob\IJobList;
16+
use OCP\IAppConfig;
17+
use OCP\IDBConnection;
18+
use OCP\Migration\IOutput;
19+
20+
// This can only be executed once because `background_color` is again used with Nextcloud 30,
21+
// so this part only works when updating -> Nextcloud 29 -> 30
22+
class Version2006Date20240905111627 implements \OCP\Migration\IMigrationStep {
23+
24+
public function __construct(
25+
private IJobList $jobList,
26+
private IAppConfig $appConfig,
27+
private IDBConnection $connection,
28+
) {
29+
}
30+
31+
public function name(): string {
32+
return 'Restore custom primary color';
33+
}
34+
35+
public function description(): string {
36+
return 'Restore custom primary color after separating primary color from background color';
37+
}
38+
39+
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
40+
// nop
41+
}
42+
43+
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
44+
$this->restoreSystemColors($output);
45+
46+
$userThemingEnabled = $this->appConfig->getValueBool('theming', 'disable-user-theming') === false;
47+
if ($userThemingEnabled) {
48+
$this->restoreUserColors($output);
49+
}
50+
51+
return null;
52+
}
53+
54+
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
55+
$output->info('Initialize restoring of background colors for custom background images');
56+
// This is done in a background job as this can take a lot of time for large instances
57+
$this->jobList->add(RestoreBackgroundImageColor::class, ['stage' => RestoreBackgroundImageColor::STAGE_PREPARE]);
58+
}
59+
60+
private function restoreSystemColors(IOutput $output): void {
61+
$defaultColor = $this->appConfig->getValueString(Application::APP_ID, 'color', '');
62+
if ($defaultColor === '') {
63+
$output->info('No custom system color configured - skipping');
64+
} else {
65+
// Restore legacy value into new field
66+
$this->appConfig->setValueString(Application::APP_ID, 'background_color', $defaultColor);
67+
$this->appConfig->setValueString(Application::APP_ID, 'primary_color', $defaultColor);
68+
// Delete legacy field
69+
$this->appConfig->deleteKey(Application::APP_ID, 'color');
70+
// give some feedback
71+
$output->info('Global primary color restored');
72+
}
73+
}
74+
75+
private function restoreUserColors(IOutput $output): void {
76+
$output->info('Restoring user primary color');
77+
// For performance let the DB handle this
78+
$qb = $this->connection->getQueryBuilder();
79+
// Rename the `background_color` config to `primary_color` as this was the behavior on Nextcloud 29 and older
80+
// with Nextcloud 30 `background_color` is a new option to define the background color independent of the primary color.
81+
$qb->update('preferences')
82+
->set('configkey', $qb->createNamedParameter('primary_color'))
83+
->where($qb->expr()->eq('appid', $qb->createNamedParameter(Application::APP_ID)))
84+
->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('background_color')));
85+
$qb->executeStatement();
86+
$output->info('Primary color of users restored');
87+
}
88+
}

0 commit comments

Comments
 (0)