diff --git a/apps/files/src/components/FilesAppSettings/FilesAppSettingsGeneral.vue b/apps/files/src/components/FilesAppSettings/FilesAppSettingsGeneral.vue index 01212793d02a0..8f51ff352a107 100644 --- a/apps/files/src/components/FilesAppSettings/FilesAppSettingsGeneral.vue +++ b/apps/files/src/components/FilesAppSettings/FilesAppSettingsGeneral.vue @@ -37,6 +37,7 @@ const store = useUserConfigStore() diff --git a/core/Migrations/Version33000Date20250819110529.php b/core/Migrations/Version33000Date20250819110529.php index 570a2ee72d429..41f3e37a8128d 100644 --- a/core/Migrations/Version33000Date20250819110529.php +++ b/core/Migrations/Version33000Date20250819110529.php @@ -31,7 +31,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt if (!$schema->hasTable('preview_locations')) { $table = $schema->createTable('preview_locations'); $table->addColumn('id', Types::BIGINT, ['autoincrement' => true, 'notnull' => true, 'length' => 20, 'unsigned' => true]); - $table->addColumn('bucket_name', Types::STRING, ['notnull' => true, 'length' => 40]); + $table->addColumn('bucket_name', Types::STRING, ['notnull' => true, 'length' => 63]); $table->addColumn('object_store_name', Types::STRING, ['notnull' => true, 'length' => 40]); $table->setPrimaryKey(['id']); } diff --git a/core/Migrations/Version33000Date20260306120000.php b/core/Migrations/Version33000Date20260306120000.php new file mode 100644 index 0000000000000..d86f7d5304d26 --- /dev/null +++ b/core/Migrations/Version33000Date20260306120000.php @@ -0,0 +1,40 @@ +hasTable('preview_locations')) { + $table = $schema->getTable('preview_locations'); + $column = $table->getColumn('bucket_name'); + + if ($column->getLength() < 63) { + $column->setLength(63); + } + } + + return $schema; + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 5b6de5ff356d1..66d4221f734c7 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -2072,6 +2072,7 @@ 'OC\\Repair\\NC29\\SanitizeAccountProperties' => $baseDir . '/lib/private/Repair/NC29/SanitizeAccountProperties.php', 'OC\\Repair\\NC29\\SanitizeAccountPropertiesJob' => $baseDir . '/lib/private/Repair/NC29/SanitizeAccountPropertiesJob.php', 'OC\\Repair\\NC30\\RemoveLegacyDatadirFile' => $baseDir . '/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php', + 'OC\\Repair\\NC33\\FixDefaultAccountScopesToLocal' => $baseDir . '/lib/private/Repair/NC33/FixDefaultAccountScopesToLocal.php', 'OC\\Repair\\OldGroupMembershipShares' => $baseDir . '/lib/private/Repair/OldGroupMembershipShares.php', 'OC\\Repair\\Owncloud\\CleanPreviews' => $baseDir . '/lib/private/Repair/Owncloud/CleanPreviews.php', 'OC\\Repair\\Owncloud\\CleanPreviewsBackgroundJob' => $baseDir . '/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index fbee07dafc6b4..c8e75d7f60daa 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -2113,6 +2113,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Repair\\NC29\\SanitizeAccountProperties' => __DIR__ . '/../../..' . '/lib/private/Repair/NC29/SanitizeAccountProperties.php', 'OC\\Repair\\NC29\\SanitizeAccountPropertiesJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC29/SanitizeAccountPropertiesJob.php', 'OC\\Repair\\NC30\\RemoveLegacyDatadirFile' => __DIR__ . '/../../..' . '/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php', + 'OC\\Repair\\NC33\\FixDefaultAccountScopesToLocal' => __DIR__ . '/../../..' . '/lib/private/Repair/NC33/FixDefaultAccountScopesToLocal.php', 'OC\\Repair\\OldGroupMembershipShares' => __DIR__ . '/../../..' . '/lib/private/Repair/OldGroupMembershipShares.php', 'OC\\Repair\\Owncloud\\CleanPreviews' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/CleanPreviews.php', 'OC\\Repair\\Owncloud\\CleanPreviewsBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php', diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 373a697a327a5..9099126615bdf 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -66,16 +66,16 @@ class AccountManager implements IAccountManager { */ public const DEFAULT_SCOPES = [ self::PROPERTY_ADDRESS => self::SCOPE_LOCAL, - self::PROPERTY_AVATAR => self::SCOPE_FEDERATED, + self::PROPERTY_AVATAR => self::SCOPE_LOCAL, self::PROPERTY_BIOGRAPHY => self::SCOPE_LOCAL, self::PROPERTY_BIRTHDATE => self::SCOPE_LOCAL, - self::PROPERTY_DISPLAYNAME => self::SCOPE_FEDERATED, - self::PROPERTY_EMAIL => self::SCOPE_FEDERATED, + self::PROPERTY_DISPLAYNAME => self::SCOPE_LOCAL, + self::PROPERTY_EMAIL => self::SCOPE_LOCAL, self::PROPERTY_FEDIVERSE => self::SCOPE_LOCAL, self::PROPERTY_HEADLINE => self::SCOPE_LOCAL, self::PROPERTY_ORGANISATION => self::SCOPE_LOCAL, self::PROPERTY_PHONE => self::SCOPE_LOCAL, - self::PROPERTY_PRONOUNS => self::SCOPE_FEDERATED, + self::PROPERTY_PRONOUNS => self::SCOPE_LOCAL, self::PROPERTY_ROLE => self::SCOPE_LOCAL, self::PROPERTY_TWITTER => self::SCOPE_LOCAL, self::PROPERTY_BLUESKY => self::SCOPE_LOCAL, diff --git a/lib/private/Repair.php b/lib/private/Repair.php index e35feda335773..f7546d04abd5e 100644 --- a/lib/private/Repair.php +++ b/lib/private/Repair.php @@ -42,6 +42,7 @@ use OC\Repair\NC25\AddMissingSecretJob; use OC\Repair\NC29\SanitizeAccountProperties; use OC\Repair\NC30\RemoveLegacyDatadirFile; +use OC\Repair\NC33\FixDefaultAccountScopesToLocal; use OC\Repair\OldGroupMembershipShares; use OC\Repair\Owncloud\CleanPreviews; use OC\Repair\Owncloud\DropAccountTermsTable; @@ -189,6 +190,7 @@ public static function getRepairSteps(): array { Server::get(SanitizeAccountProperties::class), Server::get(AddMovePreviewJob::class), Server::get(ConfigKeyMigration::class), + Server::get(FixDefaultAccountScopesToLocal::class), ]; } diff --git a/lib/private/Repair/NC33/FixDefaultAccountScopesToLocal.php b/lib/private/Repair/NC33/FixDefaultAccountScopesToLocal.php new file mode 100644 index 0000000000000..517b4d73ca525 --- /dev/null +++ b/lib/private/Repair/NC33/FixDefaultAccountScopesToLocal.php @@ -0,0 +1,92 @@ +connection->getQueryBuilder(); + $select->select('uid', 'data') + ->from('accounts'); + + $update = $this->connection->getQueryBuilder(); + $update->update('accounts') + ->set('data', $update->createParameter('data')) + ->where($update->expr()->eq('uid', $update->createParameter('uid'))); + + $result = $select->executeQuery(); + while ($row = $result->fetch()) { + $processed++; + $data = json_decode($row['data'], true); + if (!is_array($data)) { + continue; + } + + $changed = false; + foreach (self::AFFECTED_PROPERTIES as $property) { + if (isset($data[$property]['scope']) + && $data[$property]['scope'] === IAccountManager::SCOPE_FEDERATED + ) { + $data[$property]['scope'] = IAccountManager::SCOPE_LOCAL; + $changed = true; + } + } + + if ($changed) { + $update->setParameter('data', json_encode($data)); + $update->setParameter('uid', $row['uid']); + $update->executeStatement(); + $updated++; + } + } + $result->closeCursor(); + + $output->info("Processed $processed accounts, updated $updated accounts with local scope defaults."); + } +} diff --git a/tests/lib/Accounts/AccountManagerTest.php b/tests/lib/Accounts/AccountManagerTest.php index 8d66ba15ce15a..4e799458961ae 100644 --- a/tests/lib/Accounts/AccountManagerTest.php +++ b/tests/lib/Accounts/AccountManagerTest.php @@ -532,14 +532,14 @@ public function testAddMissingDefaults(): void { [ 'name' => IAccountManager::PROPERTY_DISPLAYNAME, 'value' => 'bob', - 'scope' => IAccountManager::SCOPE_FEDERATED, + 'scope' => IAccountManager::SCOPE_LOCAL, 'verified' => IAccountManager::NOT_VERIFIED, ], [ 'name' => IAccountManager::PROPERTY_EMAIL, 'value' => 'bob@bob.bob', - 'scope' => IAccountManager::SCOPE_FEDERATED, + 'scope' => IAccountManager::SCOPE_LOCAL, 'verified' => IAccountManager::NOT_VERIFIED, ], @@ -559,7 +559,7 @@ public function testAddMissingDefaults(): void { [ 'name' => IAccountManager::PROPERTY_AVATAR, - 'scope' => IAccountManager::SCOPE_FEDERATED + 'scope' => IAccountManager::SCOPE_LOCAL, ], [ @@ -628,7 +628,7 @@ public function testAddMissingDefaults(): void { [ 'name' => IAccountManager::PROPERTY_PRONOUNS, 'value' => '', - 'scope' => IAccountManager::SCOPE_FEDERATED, + 'scope' => IAccountManager::SCOPE_LOCAL, ], ]; $this->config->expects($this->once())->method('getSystemValue')->with('account_manager.default_property_scope', [])->willReturn([]); @@ -1014,10 +1014,12 @@ public static function dataSetDefaultPropertyScopes(): array { [ [], [ - IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_FEDERATED, + IAccountManager::PROPERTY_DISPLAYNAME => IAccountManager::SCOPE_LOCAL, IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL, - IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED, + IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL, IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_LOCAL, + IAccountManager::PROPERTY_AVATAR => IAccountManager::SCOPE_LOCAL, + IAccountManager::PROPERTY_PRONOUNS => IAccountManager::SCOPE_LOCAL, ] ], [ @@ -1039,7 +1041,7 @@ public static function dataSetDefaultPropertyScopes(): array { ], [ IAccountManager::PROPERTY_ADDRESS => IAccountManager::SCOPE_LOCAL, - IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_FEDERATED, + IAccountManager::PROPERTY_EMAIL => IAccountManager::SCOPE_LOCAL, IAccountManager::PROPERTY_ROLE => IAccountManager::SCOPE_PRIVATE, ] ], diff --git a/tests/lib/Repair/NC33/FixDefaultAccountScopesToLocalTest.php b/tests/lib/Repair/NC33/FixDefaultAccountScopesToLocalTest.php new file mode 100644 index 0000000000000..d1cfbad35f83a --- /dev/null +++ b/tests/lib/Repair/NC33/FixDefaultAccountScopesToLocalTest.php @@ -0,0 +1,196 @@ +connection = Server::get(IDBConnection::class); + $this->output = $this->createMock(IOutput::class); + $this->repair = new FixDefaultAccountScopesToLocal($this->connection); + } + + protected function tearDown(): void { + parent::tearDown(); + $query = $this->connection->getQueryBuilder(); + $query->delete('accounts') + ->where($query->expr()->like('uid', $query->createNamedParameter('test-fix-scope-%'))) + ->executeStatement(); + } + + private function insertAccount(string $uid, array $data): void { + $query = $this->connection->getQueryBuilder(); + $query->insert('accounts') + ->values([ + 'uid' => $query->createNamedParameter($uid), + 'data' => $query->createNamedParameter(json_encode($data)), + ]) + ->executeStatement(); + } + + private function getAccountData(string $uid): ?array { + $query = $this->connection->getQueryBuilder(); + $query->select('data') + ->from('accounts') + ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))); + $result = $query->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + if ($row === false) { + return null; + } + return json_decode($row['data'], true); + } + + public function testMigratesFederatedToLocal(): void { + $uid = 'test-fix-scope-federated'; + $data = [ + IAccountManager::PROPERTY_DISPLAYNAME => [ + 'value' => 'Test User', + 'scope' => IAccountManager::SCOPE_FEDERATED, + 'verified' => '0', + ], + IAccountManager::PROPERTY_EMAIL => [ + 'value' => 'test@example.com', + 'scope' => IAccountManager::SCOPE_FEDERATED, + 'verified' => '0', + ], + IAccountManager::PROPERTY_AVATAR => [ + 'value' => '', + 'scope' => IAccountManager::SCOPE_FEDERATED, + ], + IAccountManager::PROPERTY_PRONOUNS => [ + 'value' => '', + 'scope' => IAccountManager::SCOPE_FEDERATED, + ], + IAccountManager::PROPERTY_PHONE => [ + 'value' => '', + 'scope' => IAccountManager::SCOPE_LOCAL, + 'verified' => '0', + ], + IAccountManager::PROPERTY_ADDRESS => [ + 'value' => '', + 'scope' => IAccountManager::SCOPE_LOCAL, + 'verified' => '0', + ], + ]; + + $this->insertAccount($uid, $data); + $this->repair->run($this->output); + + $updatedData = $this->getAccountData($uid); + $this->assertNotNull($updatedData); + + // These should be changed from federated to local + $this->assertEquals(IAccountManager::SCOPE_LOCAL, $updatedData[IAccountManager::PROPERTY_DISPLAYNAME]['scope']); + $this->assertEquals(IAccountManager::SCOPE_LOCAL, $updatedData[IAccountManager::PROPERTY_EMAIL]['scope']); + $this->assertEquals(IAccountManager::SCOPE_LOCAL, $updatedData[IAccountManager::PROPERTY_AVATAR]['scope']); + $this->assertEquals(IAccountManager::SCOPE_LOCAL, $updatedData[IAccountManager::PROPERTY_PRONOUNS]['scope']); + + // These should remain unchanged + $this->assertEquals(IAccountManager::SCOPE_LOCAL, $updatedData[IAccountManager::PROPERTY_PHONE]['scope']); + $this->assertEquals(IAccountManager::SCOPE_LOCAL, $updatedData[IAccountManager::PROPERTY_ADDRESS]['scope']); + } + + public function testDoesNotChangePublishedScope(): void { + $uid = 'test-fix-scope-published'; + $data = [ + IAccountManager::PROPERTY_DISPLAYNAME => [ + 'value' => 'Public User', + 'scope' => IAccountManager::SCOPE_PUBLISHED, + 'verified' => '0', + ], + IAccountManager::PROPERTY_EMAIL => [ + 'value' => 'public@example.com', + 'scope' => IAccountManager::SCOPE_PUBLISHED, + 'verified' => '0', + ], + ]; + + $this->insertAccount($uid, $data); + $this->repair->run($this->output); + + $updatedData = $this->getAccountData($uid); + $this->assertNotNull($updatedData); + + // Published scope should NOT be changed + $this->assertEquals(IAccountManager::SCOPE_PUBLISHED, $updatedData[IAccountManager::PROPERTY_DISPLAYNAME]['scope']); + $this->assertEquals(IAccountManager::SCOPE_PUBLISHED, $updatedData[IAccountManager::PROPERTY_EMAIL]['scope']); + } + + public function testDoesNotChangeAlreadyLocalScope(): void { + $uid = 'test-fix-scope-local'; + $data = [ + IAccountManager::PROPERTY_DISPLAYNAME => [ + 'value' => 'Local User', + 'scope' => IAccountManager::SCOPE_LOCAL, + 'verified' => '0', + ], + IAccountManager::PROPERTY_EMAIL => [ + 'value' => 'local@example.com', + 'scope' => IAccountManager::SCOPE_LOCAL, + 'verified' => '0', + ], + ]; + + $this->insertAccount($uid, $data); + $this->repair->run($this->output); + + $updatedData = $this->getAccountData($uid); + $this->assertNotNull($updatedData); + + // Should remain local + $this->assertEquals(IAccountManager::SCOPE_LOCAL, $updatedData[IAccountManager::PROPERTY_DISPLAYNAME]['scope']); + $this->assertEquals(IAccountManager::SCOPE_LOCAL, $updatedData[IAccountManager::PROPERTY_EMAIL]['scope']); + } + + public function testDoesNotChangeNonAffectedProperties(): void { + $uid = 'test-fix-scope-phone-federated'; + $data = [ + IAccountManager::PROPERTY_PHONE => [ + 'value' => '+1234567890', + 'scope' => IAccountManager::SCOPE_FEDERATED, + 'verified' => '0', + ], + IAccountManager::PROPERTY_WEBSITE => [ + 'value' => 'https://example.com', + 'scope' => IAccountManager::SCOPE_FEDERATED, + 'verified' => '0', + ], + ]; + + $this->insertAccount($uid, $data); + $this->repair->run($this->output); + + $updatedData = $this->getAccountData($uid); + $this->assertNotNull($updatedData); + + // Phone and website were not in the old defaults that were federated, + // so they should remain unchanged (user chose federated deliberately) + $this->assertEquals(IAccountManager::SCOPE_FEDERATED, $updatedData[IAccountManager::PROPERTY_PHONE]['scope']); + $this->assertEquals(IAccountManager::SCOPE_FEDERATED, $updatedData[IAccountManager::PROPERTY_WEBSITE]['scope']); + } +}