diff options
Diffstat (limited to 'apps')
27 files changed, 785 insertions, 216 deletions
diff --git a/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php b/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php index 21991007ee7..333a6393920 100644 --- a/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php +++ b/apps/contactsinteraction/lib/Listeners/ContactInteractionListener.php @@ -84,6 +84,11 @@ class ContactInteractionListener implements IEventListener { return; } + if ($event->getUid() !== null && $event->getUid() === $event->getActor()->getUID()) { + $this->logger->info("Ignoring contact interaction with self"); + return; + } + $existing = $this->mapper->findMatch( $event->getActor(), $event->getUid(), diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php index 93a3a188399..661a7e66e10 100644 --- a/apps/files/lib/Service/OwnershipTransferService.php +++ b/apps/files/lib/Service/OwnershipTransferService.php @@ -144,13 +144,12 @@ class OwnershipTransferService { throw new TransferOwnershipException("Unknown path provided: $path", 1); } - if ($move && ( - !$view->is_dir($finalTarget) || ( - !$firstLogin && - count($view->getDirectoryContent($finalTarget)) > 0 - ) - ) - ) { + if ($move && !$view->is_dir($finalTarget)) { + // Initialize storage + \OC_Util::setupFS($destinationUser->getUID()); + } + + if ($move && !$firstLogin && count($view->getDirectoryContent($finalTarget)) > 0) { throw new TransferOwnershipException("Destination path does not exists or is not empty", 1); } @@ -444,13 +443,17 @@ class OwnershipTransferService { $output->writeln("Restoring incoming shares ..."); $progress = new ProgressBar($output, count($sourceShares)); $prefix = "$destinationUid/files"; + $finalShareTarget = ''; if (substr($finalTarget, 0, strlen($prefix)) === $prefix) { $finalShareTarget = substr($finalTarget, strlen($prefix)); } foreach ($sourceShares as $share) { try { // Only restore if share is in given path. - $pathToCheck = '/' . trim($path) . '/'; + $pathToCheck = '/'; + if (trim($path, '/') !== '') { + $pathToCheck = '/' . trim($path) . '/'; + } if (substr($share->getTarget(), 0, strlen($pathToCheck)) !== $pathToCheck) { continue; } diff --git a/apps/files_sharing/lib/Controller/ShareController.php b/apps/files_sharing/lib/Controller/ShareController.php index b4a332d7419..614dae7ffba 100644 --- a/apps/files_sharing/lib/Controller/ShareController.php +++ b/apps/files_sharing/lib/Controller/ShareController.php @@ -385,7 +385,7 @@ class ShareController extends AuthPublicShareController { $shareTmpl['protected'] = $share->getPassword() !== null ? 'true' : 'false'; $shareTmpl['dir'] = ''; $shareTmpl['nonHumanFileSize'] = $shareNode->getSize(); - $shareTmpl['fileSize'] = str_replace(' ', ' ', \OCP\Util::humanFileSize($shareNode->getSize())); + $shareTmpl['fileSize'] = \OCP\Util::humanFileSize($shareNode->getSize()); $shareTmpl['hideDownload'] = $share->getHideDownload(); $hideFileList = false; diff --git a/apps/files_sharing/templates/public.php b/apps/files_sharing/templates/public.php index 677f015ce81..33dd6ecd189 100644 --- a/apps/files_sharing/templates/public.php +++ b/apps/files_sharing/templates/public.php @@ -75,7 +75,7 @@ $maxUploadFilesize = min($upload_max_filesize, $post_max_size); <?php if (isset($_['mimetype']) && strpos($_['mimetype'], 'image') === 0) { ?> <div class="directDownload"> <div> - <?php p($_['filename'])?> (<?php echo($_['fileSize']) ?>) + <?php p($_['filename'])?> (<?php p($_['fileSize']) ?>) </div> <a href="<?php p($_['downloadURL']); ?>" id="downloadFile" class="button"> <span class="icon icon-download"></span> @@ -87,7 +87,7 @@ $maxUploadFilesize = min($upload_max_filesize, $post_max_size); <?php if ($_['previewURL'] === $_['downloadURL'] && !$_['hideDownload']): ?> <div class="directDownload"> <div> - <?php p($_['filename'])?> (<?php echo($_['fileSize']) ?>) + <?php p($_['filename'])?> (<?php p($_['fileSize']) ?>) </div> <a href="<?php p($_['downloadURL']); ?>" id="downloadFile" class="button"> <span class="icon icon-download"></span> diff --git a/apps/files_sharing/tests/Controller/ShareControllerTest.php b/apps/files_sharing/tests/Controller/ShareControllerTest.php index 512a61d811e..be2616f70fc 100644 --- a/apps/files_sharing/tests/Controller/ShareControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareControllerTest.php @@ -329,7 +329,7 @@ class ShareControllerTest extends \Test\TestCase { 'protected' => 'true', 'dir' => '', 'downloadURL' => 'downloadURL', - 'fileSize' => '33 B', + 'fileSize' => '33 B', 'nonHumanFileSize' => 33, 'maxSizeAnimateGif' => 10, 'previewSupported' => true, @@ -480,7 +480,7 @@ class ShareControllerTest extends \Test\TestCase { 'protected' => 'true', 'dir' => '', 'downloadURL' => 'downloadURL', - 'fileSize' => '33 B', + 'fileSize' => '33 B', 'nonHumanFileSize' => 33, 'maxSizeAnimateGif' => 10, 'previewSupported' => true, @@ -631,7 +631,7 @@ class ShareControllerTest extends \Test\TestCase { 'protected' => 'true', 'dir' => '', 'downloadURL' => 'downloadURL', - 'fileSize' => '33 B', + 'fileSize' => '33 B', 'nonHumanFileSize' => 33, 'maxSizeAnimateGif' => 10, 'previewSupported' => true, @@ -756,7 +756,7 @@ class ShareControllerTest extends \Test\TestCase { 'protected' => 'false', 'dir' => null, 'downloadURL' => '', - 'fileSize' => '1 KB', + 'fileSize' => '1 KB', 'nonHumanFileSize' => 1337, 'maxSizeAnimateGif' => null, 'previewSupported' => null, diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index 0b5f37b44a4..3d3729a66e5 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -69,6 +69,7 @@ return array( 'OCA\\Settings\\Settings\\Personal\\Security\\WebAuthn' => $baseDir . '/../lib/Settings/Personal/Security/WebAuthn.php', 'OCA\\Settings\\Settings\\Personal\\ServerDevNotice' => $baseDir . '/../lib/Settings/Personal/ServerDevNotice.php', 'OCA\\Settings\\SetupChecks\\CheckUserCertificates' => $baseDir . '/../lib/SetupChecks/CheckUserCertificates.php', + 'OCA\\Settings\\SetupChecks\\LdapInvalidUuids' => $baseDir . '/../lib/SetupChecks/LdapInvalidUuids.php', 'OCA\\Settings\\SetupChecks\\LegacySSEKeyFormat' => $baseDir . '/../lib/SetupChecks/LegacySSEKeyFormat.php', 'OCA\\Settings\\SetupChecks\\PhpDefaultCharset' => $baseDir . '/../lib/SetupChecks/PhpDefaultCharset.php', 'OCA\\Settings\\SetupChecks\\PhpOutputBuffering' => $baseDir . '/../lib/SetupChecks/PhpOutputBuffering.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index efd36d32f47..7d00184dc7f 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -84,6 +84,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\Settings\\Personal\\Security\\WebAuthn' => __DIR__ . '/..' . '/../lib/Settings/Personal/Security/WebAuthn.php', 'OCA\\Settings\\Settings\\Personal\\ServerDevNotice' => __DIR__ . '/..' . '/../lib/Settings/Personal/ServerDevNotice.php', 'OCA\\Settings\\SetupChecks\\CheckUserCertificates' => __DIR__ . '/..' . '/../lib/SetupChecks/CheckUserCertificates.php', + 'OCA\\Settings\\SetupChecks\\LdapInvalidUuids' => __DIR__ . '/..' . '/../lib/SetupChecks/LdapInvalidUuids.php', 'OCA\\Settings\\SetupChecks\\LegacySSEKeyFormat' => __DIR__ . '/..' . '/../lib/SetupChecks/LegacySSEKeyFormat.php', 'OCA\\Settings\\SetupChecks\\PhpDefaultCharset' => __DIR__ . '/..' . '/../lib/SetupChecks/PhpDefaultCharset.php', 'OCA\\Settings\\SetupChecks\\PhpOutputBuffering' => __DIR__ . '/..' . '/../lib/SetupChecks/PhpOutputBuffering.php', diff --git a/apps/settings/composer/composer/installed.php b/apps/settings/composer/composer/installed.php index 5440719fa40..6e11f678155 100644 --- a/apps/settings/composer/composer/installed.php +++ b/apps/settings/composer/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), - 'reference' => 'c6429e6cd19c57582364338362e543580821cf99', + 'reference' => '3c77e489a6bb2541cd5d0c92b5498e71ec1a873f', 'name' => '__root__', 'dev' => false, ), @@ -16,7 +16,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), - 'reference' => 'c6429e6cd19c57582364338362e543580821cf99', + 'reference' => '3c77e489a6bb2541cd5d0c92b5498e71ec1a873f', 'dev_requirement' => false, ), ), diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index 3a8b9bfd4a5..11900fad45b 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -49,7 +49,6 @@ use DirectoryIterator; use Doctrine\DBAL\Exception; use Doctrine\DBAL\Platforms\SqlitePlatform; use Doctrine\DBAL\TransactionIsolationLevel; -use OCP\DB\Types; use GuzzleHttp\Exception\ClientException; use OC; use OC\AppFramework\Http; @@ -62,20 +61,24 @@ use OC\IntegrityCheck\Checker; use OC\Lock\NoopLockingProvider; use OC\MemoryInfo; use OCA\Settings\SetupChecks\CheckUserCertificates; +use OCA\Settings\SetupChecks\LdapInvalidUuids; use OCA\Settings\SetupChecks\LegacySSEKeyFormat; use OCA\Settings\SetupChecks\PhpDefaultCharset; use OCA\Settings\SetupChecks\PhpOutputBuffering; use OCA\Settings\SetupChecks\SupportedDatabase; +use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\RedirectResponse; +use OCP\DB\Types; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IDateTimeFormatter; use OCP\IDBConnection; use OCP\IL10N; use OCP\IRequest; +use OCP\IServerContainer; use OCP\ITempManager; use OCP\IURLGenerator; use OCP\Lock\ILockingProvider; @@ -118,6 +121,10 @@ class CheckSetupController extends Controller { private $tempManager; /** @var IManager */ private $manager; + /** @var IAppManager */ + private $appManager; + /** @var IServerContainer */ + private $serverContainer; public function __construct($AppName, IRequest $request, @@ -136,7 +143,10 @@ class CheckSetupController extends Controller { IniGetWrapper $iniGetWrapper, IDBConnection $connection, ITempManager $tempManager, - IManager $manager) { + IManager $manager, + IAppManager $appManager, + IServerContainer $serverContainer + ) { parent::__construct($AppName, $request); $this->config = $config; $this->clientService = $clientService; @@ -154,6 +164,8 @@ class CheckSetupController extends Controller { $this->connection = $connection; $this->tempManager = $tempManager; $this->manager = $manager; + $this->appManager = $appManager; + $this->serverContainer = $serverContainer; } /** @@ -817,6 +829,7 @@ Raw output $legacySSEKeyFormat = new LegacySSEKeyFormat($this->l10n, $this->config, $this->urlGenerator); $checkUserCertificates = new CheckUserCertificates($this->l10n, $this->config, $this->urlGenerator); $supportedDatabases = new SupportedDatabase($this->l10n, $this->connection); + $ldapInvalidUuids = new LdapInvalidUuids($this->appManager, $this->l10n, $this->serverContainer); return new DataResponse( [ @@ -865,6 +878,7 @@ Raw output 'isDefaultPhoneRegionSet' => $this->config->getSystemValueString('default_phone_region', '') !== '', SupportedDatabase::class => ['pass' => $supportedDatabases->run(), 'description' => $supportedDatabases->description(), 'severity' => $supportedDatabases->severity()], 'temporaryDirectoryWritable' => $this->isTemporaryDirectoryWritable(), + LdapInvalidUuids::class => ['pass' => $ldapInvalidUuids->run(), 'description' => $ldapInvalidUuids->description(), 'severity' => $ldapInvalidUuids->severity()], ] ); } diff --git a/apps/settings/lib/Hooks.php b/apps/settings/lib/Hooks.php index 4f005272b93..b7b78c49b12 100644 --- a/apps/settings/lib/Hooks.php +++ b/apps/settings/lib/Hooks.php @@ -28,6 +28,7 @@ namespace OCA\Settings; use OCA\Settings\Activity\Provider; use OCP\Activity\IManager as IActivityManager; +use OCP\Defaults; use OCP\IConfig; use OCP\IGroupManager; use OCP\IURLGenerator; @@ -55,6 +56,8 @@ class Hooks { protected $config; /** @var IFactory */ protected $languageFactory; + /** @var Defaults */ + protected $defaults; public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, @@ -63,7 +66,8 @@ class Hooks { IURLGenerator $urlGenerator, IMailer $mailer, IConfig $config, - IFactory $languageFactory) { + IFactory $languageFactory, + Defaults $defaults) { $this->activityManager = $activityManager; $this->groupManager = $groupManager; $this->userManager = $userManager; @@ -72,6 +76,7 @@ class Hooks { $this->mailer = $mailer; $this->config = $config; $this->languageFactory = $languageFactory; + $this->defaults = $defaults; } /** @@ -93,6 +98,7 @@ class Hooks { ->setType('personal_settings') ->setAffectedUser($user->getUID()); + $instanceName = $this->defaults->getName(); $instanceUrl = $this->urlGenerator->getAbsoluteURL('/'); $language = $this->languageFactory->getUserLanguage($user); $l = $this->languageFactory->get('settings', $language); @@ -131,7 +137,7 @@ class Hooks { 'instanceUrl' => $instanceUrl, ]); - $template->setSubject($l->t('Password for %1$s changed on %2$s', [$user->getDisplayName(), $instanceUrl])); + $template->setSubject($l->t('Password for %1$s changed on %2$s', [$user->getDisplayName(), $instanceName])); $template->addHeader(); $template->addHeading($l->t('Password changed for %s', [$user->getDisplayName()]), false); $template->addBodyText($text . ' ' . $l->t('If you did not request this, please contact an administrator.')); diff --git a/apps/settings/lib/SetupChecks/LdapInvalidUuids.php b/apps/settings/lib/SetupChecks/LdapInvalidUuids.php new file mode 100644 index 00000000000..11b0105cada --- /dev/null +++ b/apps/settings/lib/SetupChecks/LdapInvalidUuids.php @@ -0,0 +1,69 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2022 Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Settings\SetupChecks; + +use OCA\User_LDAP\Mapping\GroupMapping; +use OCA\User_LDAP\Mapping\UserMapping; +use OCP\App\IAppManager; +use OCP\IL10N; +use OCP\IServerContainer; + +class LdapInvalidUuids { + + /** @var IAppManager */ + private $appManager; + /** @var IL10N */ + private $l10n; + /** @var IServerContainer */ + private $server; + + public function __construct(IAppManager $appManager, IL10N $l10n, IServerContainer $server) { + $this->appManager = $appManager; + $this->l10n = $l10n; + $this->server = $server; + } + + public function description(): string { + return $this->l10n->t('Invalid UUIDs of LDAP users or groups have been found. Please review your "Override UUID detection" settings in the Expert part of the LDAP configuration and use "occ ldap:update-uuid" to update them.'); + } + + public function severity(): string { + return 'warning'; + } + + public function run(): bool { + if (!$this->appManager->isEnabledForUser('user_ldap')) { + return true; + } + /** @var UserMapping $userMapping */ + $userMapping = $this->server->get(UserMapping::class); + /** @var GroupMapping $groupMapping */ + $groupMapping = $this->server->get(GroupMapping::class); + return count($userMapping->getList(0, 1, true)) === 0 + && count($groupMapping->getList(0, 1, true)) === 0; + } +} diff --git a/apps/settings/tests/Controller/CheckSetupControllerTest.php b/apps/settings/tests/Controller/CheckSetupControllerTest.php index d7466991063..20cf2b01069 100644 --- a/apps/settings/tests/Controller/CheckSetupControllerTest.php +++ b/apps/settings/tests/Controller/CheckSetupControllerTest.php @@ -42,6 +42,7 @@ use OC\IntegrityCheck\Checker; use OC\MemoryInfo; use OC\Security\SecureRandom; use OCA\Settings\Controller\CheckSetupController; +use OCP\App\IAppManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataResponse; @@ -52,6 +53,7 @@ use OCP\IDateTimeFormatter; use OCP\IDBConnection; use OCP\IL10N; use OCP\IRequest; +use OCP\IServerContainer; use OCP\ITempManager; use OCP\IURLGenerator; use OCP\Lock\ILockingProvider; @@ -105,6 +107,10 @@ class CheckSetupControllerTest extends TestCase { private $tempManager; /** @var IManager|\PHPUnit\Framework\MockObject\MockObject */ private $notificationManager; + /** @var IAppManager|MockObject */ + private $appManager; + /** @var IServerContainer|MockObject */ + private $serverContainer; /** * Holds a list of directories created during tests. @@ -149,6 +155,8 @@ class CheckSetupControllerTest extends TestCase { ->disableOriginalConstructor()->getMock(); $this->tempManager = $this->getMockBuilder(ITempManager::class)->getMock(); $this->notificationManager = $this->getMockBuilder(IManager::class)->getMock(); + $this->appManager = $this->createMock(IAppManager::class); + $this->serverContainer = $this->createMock(IServerContainer::class); $this->checkSetupController = $this->getMockBuilder(CheckSetupController::class) ->setConstructorArgs([ 'settings', @@ -169,6 +177,8 @@ class CheckSetupControllerTest extends TestCase { $this->connection, $this->tempManager, $this->notificationManager, + $this->appManager, + $this->serverContainer, ]) ->setMethods([ 'isReadOnlyConfig', @@ -649,6 +659,7 @@ class CheckSetupControllerTest extends TestCase { 'OCA\Settings\SetupChecks\SupportedDatabase' => ['pass' => true, 'description' => '', 'severity' => 'info'], 'isFairUseOfFreePushService' => false, 'temporaryDirectoryWritable' => false, + \OCA\Settings\SetupChecks\LdapInvalidUuids::class => ['pass' => true, 'description' => 'Invalid UUIDs of LDAP users or groups have been found. Please review your "Override UUID detection" settings in the Expert part of the LDAP configuration and use "occ ldap:update-uuid" to update them.', 'severity' => 'warning'], ] ); $this->assertEquals($expected, $this->checkSetupController->check()); @@ -675,6 +686,8 @@ class CheckSetupControllerTest extends TestCase { $this->connection, $this->tempManager, $this->notificationManager, + $this->appManager, + $this->serverContainer ]) ->setMethods(null)->getMock(); @@ -1446,7 +1459,9 @@ Array $this->iniGetWrapper, $this->connection, $this->tempManager, - $this->notificationManager + $this->notificationManager, + $this->appManager, + $this->serverContainer ); $this->assertSame($expected, $this->invokePrivate($checkSetupController, 'isMysqlUsedWithoutUTF8MB4')); @@ -1498,7 +1513,9 @@ Array $this->iniGetWrapper, $this->connection, $this->tempManager, - $this->notificationManager + $this->notificationManager, + $this->appManager, + $this->serverContainer ); $this->assertSame($expected, $this->invokePrivate($checkSetupController, 'isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed')); diff --git a/apps/user_ldap/appinfo/info.xml b/apps/user_ldap/appinfo/info.xml index 57b5fe478b6..2dae4845241 100644 --- a/apps/user_ldap/appinfo/info.xml +++ b/apps/user_ldap/appinfo/info.xml @@ -56,6 +56,7 @@ A user logs into Nextcloud with their LDAP or AD credentials, and is granted acc <command>OCA\User_LDAP\Command\ShowConfig</command> <command>OCA\User_LDAP\Command\ShowRemnants</command> <command>OCA\User_LDAP\Command\TestConfig</command> + <command>OCA\User_LDAP\Command\UpdateUUID</command> </commands> <settings> diff --git a/apps/user_ldap/composer/composer/ClassLoader.php b/apps/user_ldap/composer/composer/ClassLoader.php index 0cd6055d1b7..afef3fa2ad8 100644 --- a/apps/user_ldap/composer/composer/ClassLoader.php +++ b/apps/user_ldap/composer/composer/ClassLoader.php @@ -149,7 +149,7 @@ class ClassLoader /** * @return string[] Array of classname => path - * @psalm-var array<string, string> + * @psalm-return array<string, string> */ public function getClassMap() { diff --git a/apps/user_ldap/composer/composer/autoload_classmap.php b/apps/user_ldap/composer/composer/autoload_classmap.php index ed8d535a6c5..cffb2aaa9fe 100644 --- a/apps/user_ldap/composer/composer/autoload_classmap.php +++ b/apps/user_ldap/composer/composer/autoload_classmap.php @@ -20,6 +20,7 @@ return array( 'OCA\\User_LDAP\\Command\\ShowConfig' => $baseDir . '/../lib/Command/ShowConfig.php', 'OCA\\User_LDAP\\Command\\ShowRemnants' => $baseDir . '/../lib/Command/ShowRemnants.php', 'OCA\\User_LDAP\\Command\\TestConfig' => $baseDir . '/../lib/Command/TestConfig.php', + 'OCA\\User_LDAP\\Command\\UpdateUUID' => $baseDir . '/../lib/Command/UpdateUUID.php', 'OCA\\User_LDAP\\Configuration' => $baseDir . '/../lib/Configuration.php', 'OCA\\User_LDAP\\Connection' => $baseDir . '/../lib/Connection.php', 'OCA\\User_LDAP\\ConnectionFactory' => $baseDir . '/../lib/ConnectionFactory.php', diff --git a/apps/user_ldap/composer/composer/autoload_static.php b/apps/user_ldap/composer/composer/autoload_static.php index 9ce20914307..5928ff78ef0 100644 --- a/apps/user_ldap/composer/composer/autoload_static.php +++ b/apps/user_ldap/composer/composer/autoload_static.php @@ -35,6 +35,7 @@ class ComposerStaticInitUser_LDAP 'OCA\\User_LDAP\\Command\\ShowConfig' => __DIR__ . '/..' . '/../lib/Command/ShowConfig.php', 'OCA\\User_LDAP\\Command\\ShowRemnants' => __DIR__ . '/..' . '/../lib/Command/ShowRemnants.php', 'OCA\\User_LDAP\\Command\\TestConfig' => __DIR__ . '/..' . '/../lib/Command/TestConfig.php', + 'OCA\\User_LDAP\\Command\\UpdateUUID' => __DIR__ . '/..' . '/../lib/Command/UpdateUUID.php', 'OCA\\User_LDAP\\Configuration' => __DIR__ . '/..' . '/../lib/Configuration.php', 'OCA\\User_LDAP\\Connection' => __DIR__ . '/..' . '/../lib/Connection.php', 'OCA\\User_LDAP\\ConnectionFactory' => __DIR__ . '/..' . '/../lib/ConnectionFactory.php', diff --git a/apps/user_ldap/composer/composer/installed.php b/apps/user_ldap/composer/composer/installed.php index f84b9c452e8..5e942064485 100644 --- a/apps/user_ldap/composer/composer/installed.php +++ b/apps/user_ldap/composer/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), - 'reference' => '6b960de47cabaa7a231e72479012ba4dcbc2e882', + 'reference' => '9915dc6785d1660068a51604f9379e8b1dc1418c', 'name' => '__root__', 'dev' => false, ), @@ -16,7 +16,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../', 'aliases' => array(), - 'reference' => '6b960de47cabaa7a231e72479012ba4dcbc2e882', + 'reference' => '9915dc6785d1660068a51604f9379e8b1dc1418c', 'dev_requirement' => false, ), ), diff --git a/apps/user_ldap/lib/Access.php b/apps/user_ldap/lib/Access.php index 093449ee0ea..ed5e5bff9ce 100644 --- a/apps/user_ldap/lib/Access.php +++ b/apps/user_ldap/lib/Access.php @@ -52,7 +52,6 @@ use OC\ServerNotAvailableException; use OCA\User_LDAP\Exceptions\ConstraintViolationException; use OCA\User_LDAP\Exceptions\NoMoreResults; use OCA\User_LDAP\Mapping\AbstractMapping; -use OCA\User_LDAP\Mapping\UserMapping; use OCA\User_LDAP\User\Manager; use OCA\User_LDAP\User\OfflineUser; use OCP\HintException; @@ -74,17 +73,16 @@ class Access extends LDAPUtility { public $connection; /** @var Manager */ public $userManager; - //never ever check this var directly, always use getPagedSearchResultState - protected $pagedSearchedSuccessful; - /** - * @var UserMapping $userMapper + * never ever check this var directly, always use getPagedSearchResultState + * @var ?bool */ + protected $pagedSearchedSuccessful; + + /** @var ?AbstractMapping */ protected $userMapper; - /** - * @var AbstractMapping $userMapper - */ + /** @var ?AbstractMapping */ protected $groupMapper; /** @@ -121,17 +119,15 @@ class Access extends LDAPUtility { /** * sets the User Mapper - * - * @param AbstractMapping $mapper */ - public function setUserMapper(AbstractMapping $mapper) { + public function setUserMapper(AbstractMapping $mapper): void { $this->userMapper = $mapper; } /** * @throws \Exception */ - public function getUserMapper(): UserMapping { + public function getUserMapper(): AbstractMapping { if (is_null($this->userMapper)) { throw new \Exception('UserMapper was not assigned to this Access instance.'); } @@ -140,20 +136,17 @@ class Access extends LDAPUtility { /** * sets the Group Mapper - * - * @param AbstractMapping $mapper */ - public function setGroupMapper(AbstractMapping $mapper) { + public function setGroupMapper(AbstractMapping $mapper): void { $this->groupMapper = $mapper; } /** * returns the Group Mapper * - * @return AbstractMapping * @throws \Exception */ - public function getGroupMapper() { + public function getGroupMapper(): AbstractMapping { if (is_null($this->groupMapper)) { throw new \Exception('GroupMapper was not assigned to this Access instance.'); } @@ -343,8 +336,8 @@ class Access extends LDAPUtility { public function extractRangeData($result, $attribute) { $keys = array_keys($result); foreach ($keys as $key) { - if ($key !== $attribute && strpos($key, $attribute) === 0) { - $queryData = explode(';', $key); + if ($key !== $attribute && strpos((string)$key, $attribute) === 0) { + $queryData = explode(';', (string)$key); if (strpos($queryData[1], 'range=') === 0) { $high = substr($queryData[1], 1 + strpos($queryData[1], '-')); $data = [ @@ -669,12 +662,10 @@ class Access extends LDAPUtility { } /** - * @param array $ldapObjects as returned by fetchList() - * @param bool $isUsers - * @return array + * @param array[] $ldapObjects as returned by fetchList() * @throws \Exception */ - private function ldap2NextcloudNames($ldapObjects, $isUsers) { + private function ldap2NextcloudNames(array $ldapObjects, bool $isUsers): array { if ($isUsers) { $nameAttribute = $this->connection->ldapUserDisplayName; $sndAttribute = $this->connection->ldapUserDisplayName2; @@ -786,7 +777,7 @@ class Access extends LDAPUtility { * Instead of using this method directly, call * createAltInternalOwnCloudName($name, true) */ - private function _createAltInternalOwnCloudNameForUsers($name) { + private function _createAltInternalOwnCloudNameForUsers(string $name) { $attempts = 0; //while loop is just a precaution. If a name is not generated within //20 attempts, something else is very wrong. Avoids infinite loop. @@ -813,8 +804,8 @@ class Access extends LDAPUtility { * numbering, e.g. Developers_42 when there are 41 other groups called * "Developers" */ - private function _createAltInternalOwnCloudNameForGroups($name) { - $usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%'); + private function _createAltInternalOwnCloudNameForGroups(string $name) { + $usedNames = $this->getGroupMapper()->getNamesBySearch($name, "", '_%'); if (!$usedNames || count($usedNames) === 0) { $lastNo = 1; //will become name_2 } else { @@ -843,10 +834,10 @@ class Access extends LDAPUtility { * creates a unique name for internal Nextcloud use. * * @param string $name the display name of the object - * @param boolean $isUser whether name should be created for a user (true) or a group (false) + * @param bool $isUser whether name should be created for a user (true) or a group (false) * @return string|false with with the name to use in Nextcloud or false if unsuccessful */ - private function createAltInternalOwnCloudName($name, $isUser) { + private function createAltInternalOwnCloudName(string $name, bool $isUser) { // ensure there is space for the "_1234" suffix if (strlen($name) > 59) { $name = substr($name, 0, 59); @@ -879,7 +870,7 @@ class Access extends LDAPUtility { * utilizing the login filter. * * @param string $loginName - * @return int + * @return false|int */ public function countUsersByLoginName($loginName) { $loginName = $this->escapeFilterPart($loginName); @@ -954,7 +945,7 @@ class Access extends LDAPUtility { * @param string|string[] $attr * @param int $limit * @param int $offset - * @return array + * @return array[] */ public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) { $groupRecords = $this->searchGroups($filter, $attr, $limit, $offset); @@ -965,7 +956,7 @@ class Access extends LDAPUtility { }, []); $idsByDn = $this->groupMapper->getListOfIdsByDn($listOfDNs); - array_walk($groupRecords, function ($record) use ($idsByDn) { + array_walk($groupRecords, function (array $record) use ($idsByDn) { $newlyMapped = false; $gid = $idsByDn[$record['dn'][0]] ?? null; if ($gid === null) { @@ -978,27 +969,17 @@ class Access extends LDAPUtility { return $this->fetchList($groupRecords, $this->manyAttributes($attr)); } - /** - * @param array $list - * @param bool $manyAttributes - * @return array - */ - private function fetchList($list, $manyAttributes) { - if (is_array($list)) { - if ($manyAttributes) { - return $list; - } else { - $list = array_reduce($list, function ($carry, $item) { - $attribute = array_keys($item)[0]; - $carry[] = $item[$attribute][0]; - return $carry; - }, []); - return array_unique($list, SORT_LOCALE_STRING); - } + private function fetchList(array $list, bool $manyAttributes): array { + if ($manyAttributes) { + return $list; + } else { + $list = array_reduce($list, function ($carry, $item) { + $attribute = array_keys($item)[0]; + $carry[] = $item[$attribute][0]; + return $carry; + }, []); + return array_unique($list, SORT_LOCALE_STRING); } - - //error cause actually, maybe throw an exception in future. - return []; } /** @@ -1518,7 +1499,7 @@ class Access extends LDAPUtility { * @param string $operator either & or | * @return string the combined filter */ - private function combineFilter($filters, $operator) { + private function combineFilter(array $filters, string $operator): string { $combinedFilter = '(' . $operator; foreach ($filters as $filter) { if ($filter !== '' && $filter[0] !== '(') { @@ -1559,12 +1540,12 @@ class Access extends LDAPUtility { * string into single words * * @param string $search the search term - * @param string[] $searchAttributes needs to have at least two attributes, + * @param string[]|null|'' $searchAttributes needs to have at least two attributes, * otherwise it does not make sense :) * @return string the final filter part to use in LDAP searches * @throws DomainException */ - private function getAdvancedFilterPartForSearch($search, $searchAttributes) { + private function getAdvancedFilterPartForSearch(string $search, $searchAttributes): string { if (!is_array($searchAttributes) || count($searchAttributes) < 2) { throw new DomainException('searchAttributes must be an array with at least two string'); } @@ -1586,12 +1567,12 @@ class Access extends LDAPUtility { * creates a filter part for searches * * @param string $search the search term - * @param string[]|null $searchAttributes + * @param string[]|null|'' $searchAttributes * @param string $fallbackAttribute a fallback attribute in case the user * did not define search attributes. Typically the display name attribute. * @return string the final filter part to use in LDAP searches */ - private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) { + private function getFilterPartForSearch(string $search, $searchAttributes, string $fallbackAttribute): string { $filter = []; $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0); if ($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) { @@ -1623,10 +1604,8 @@ class Access extends LDAPUtility { * returns the search term depending on whether we are allowed * list users found by ldap with the current input appended by * a * - * - * @return string */ - private function prepareSearchTerm($term) { + private function prepareSearchTerm(string $term): string { $config = \OC::$server->getConfig(); $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes'); @@ -1735,7 +1714,7 @@ class Access extends LDAPUtility { * @return bool true on success, false otherwise * @throws ServerNotAvailableException */ - private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) { + private function detectUuidAttribute(string $dn, bool $isUser = true, bool $force = false, ?array $ldapRecord = null): bool { if ($isUser) { $uuidAttr = 'ldapUuidUserAttribute'; $uuidOverride = $this->connection->ldapExpertUUIDUserAttr; @@ -1792,7 +1771,7 @@ class Access extends LDAPUtility { * @param string $dn * @param bool $isUser * @param null $ldapRecord - * @return bool|string + * @return false|string * @throws ServerNotAvailableException */ public function getUUID($dn, $isUser = true, $ldapRecord = null) { @@ -1827,10 +1806,9 @@ class Access extends LDAPUtility { * converts a binary ObjectGUID into a string representation * * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD - * @return string * @link https://www.php.net/manual/en/function.ldap-get-values-len.php#73198 */ - private function convertObjectGUID2Str($oguid) { + private function convertObjectGUID2Str(string $oguid): string { $hex_guid = bin2hex($oguid); $hex_guid_to_guid_str = ''; for ($k = 1; $k <= 4; ++$k) { @@ -1990,7 +1968,7 @@ class Access extends LDAPUtility { * * @throws ServerNotAvailableException */ - private function abandonPagedSearch() { + private function abandonPagedSearch(): void { if ($this->lastCookie === '') { return; } diff --git a/apps/user_ldap/lib/Command/UpdateUUID.php b/apps/user_ldap/lib/Command/UpdateUUID.php new file mode 100644 index 00000000000..716bc2d0563 --- /dev/null +++ b/apps/user_ldap/lib/Command/UpdateUUID.php @@ -0,0 +1,374 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * @author Côme Chilliet <come.chilliet@nextcloud.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + */ + +namespace OCA\User_LDAP\Command; + +use OCA\User_LDAP\Access; +use OCA\User_LDAP\Group_Proxy; +use OCA\User_LDAP\Mapping\AbstractMapping; +use OCA\User_LDAP\Mapping\GroupMapping; +use OCA\User_LDAP\Mapping\UserMapping; +use OCA\User_LDAP\User_Proxy; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use function sprintf; + +class UuidUpdateReport { + const UNCHANGED = 0; + const UNKNOWN = 1; + const UNREADABLE = 2; + const UPDATED = 3; + const UNWRITABLE = 4; + const UNMAPPED = 5; + + public $id = ''; + public $dn = ''; + public $isUser = true; + public $state = self::UNCHANGED; + public $oldUuid = ''; + public $newUuid = ''; + + public function __construct(string $id, string $dn, bool $isUser, int $state, string $oldUuid = '', string $newUuid = '') { + $this->id = $id; + $this->dn = $dn; + $this->isUser = $isUser; + $this->state = $state; + $this->oldUuid = $oldUuid; + $this->newUuid = $newUuid; + } +} + +class UpdateUUID extends Command { + /** @var UserMapping */ + private $userMapping; + /** @var GroupMapping */ + private $groupMapping; + /** @var User_Proxy */ + private $userProxy; + /** @var Group_Proxy */ + private $groupProxy; + /** @var array<UuidUpdateReport[]> */ + protected $reports = []; + /** @var LoggerInterface */ + private $logger; + /** @var bool */ + private $dryRun = false; + + public function __construct(UserMapping $userMapping, GroupMapping $groupMapping, User_Proxy $userProxy, Group_Proxy $groupProxy, LoggerInterface $logger) { + $this->userMapping = $userMapping; + $this->groupMapping = $groupMapping; + $this->userProxy = $userProxy; + $this->groupProxy = $groupProxy; + $this->logger = $logger; + $this->reports = [ + UuidUpdateReport::UPDATED => [], + UuidUpdateReport::UNKNOWN => [], + UuidUpdateReport::UNREADABLE => [], + UuidUpdateReport::UNWRITABLE => [], + UuidUpdateReport::UNMAPPED => [], + ]; + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('ldap:update-uuid') + ->setDescription('Attempts to update UUIDs of user and group entries. By default, the command attempts to update UUIDs that have been invalidated by a migration step.') + ->addOption( + 'all', + null, + InputOption::VALUE_NONE, + 'updates every user and group. All other options are ignored.' + ) + ->addOption( + 'userId', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'a user ID to update' + ) + ->addOption( + 'groupId', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'a group ID to update' + ) + ->addOption( + 'dn', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'a DN to update' + ) + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'UUIDs will not be updated in the database' + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $this->dryRun = $input->getOption('dry-run'); + $entriesToUpdate = $this->estimateNumberOfUpdates($input); + $progress = new ProgressBar($output); + $progress->start($entriesToUpdate); + foreach($this->handleUpdates($input) as $_) { + $progress->advance(); + } + $progress->finish(); + $output->writeln(''); + $this->printReport($output); + return count($this->reports[UuidUpdateReport::UNMAPPED]) === 0 + && count($this->reports[UuidUpdateReport::UNREADABLE]) === 0 + && count($this->reports[UuidUpdateReport::UNWRITABLE]) === 0 + ? 0 + : 1; + } + + protected function printReport(OutputInterface $output): void { + if ($output->isQuiet()) { + return; + } + + if (count($this->reports[UuidUpdateReport::UPDATED]) === 0) { + $output->writeln('<info>No record was updated.</info>'); + } else { + $output->writeln(sprintf('<info>%d record(s) were updated.</info>', count($this->reports[UuidUpdateReport::UPDATED]))); + if ($output->isVerbose()) { + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UPDATED] as $report) { + $output->writeln(sprintf(' %s had their old UUID %s updated to %s', $report->id, $report->oldUuid, $report->newUuid)); + } + $output->writeln(''); + } + } + + if (count($this->reports[UuidUpdateReport::UNMAPPED]) > 0) { + $output->writeln(sprintf('<error>%d provided IDs were not mapped. These were:</error>', count($this->reports[UuidUpdateReport::UNMAPPED]))); + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UNMAPPED] as $report) { + if (!empty($report->id)) { + $output->writeln(sprintf(' %s: %s', + $report->isUser ? 'User' : 'Group', $report->id)); + } else if (!empty($report->dn)) { + $output->writeln(sprintf(' DN: %s', $report->dn)); + } + } + $output->writeln(''); + } + + if (count($this->reports[UuidUpdateReport::UNKNOWN]) > 0) { + $output->writeln(sprintf('<info>%d provided IDs were unknown on LDAP.</info>', count($this->reports[UuidUpdateReport::UNKNOWN]))); + if ($output->isVerbose()) { + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UNKNOWN] as $report) { + $output->writeln(sprintf(' %s: %s',$report->isUser ? 'User' : 'Group', $report->id)); + } + $output->writeln(PHP_EOL . 'Old users can be removed along with their data per occ user:delete.' . PHP_EOL); + } + } + + if (count($this->reports[UuidUpdateReport::UNREADABLE]) > 0) { + $output->writeln(sprintf('<error>For %d records, the UUID could not be read. Double-check your configuration.</error>', count($this->reports[UuidUpdateReport::UNREADABLE]))); + if ($output->isVerbose()) { + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UNREADABLE] as $report) { + $output->writeln(sprintf(' %s: %s',$report->isUser ? 'User' : 'Group', $report->id)); + } + } + } + + if (count($this->reports[UuidUpdateReport::UNWRITABLE]) > 0) { + $output->writeln(sprintf('<error>For %d records, the UUID could not be saved to database. Double-check your configuration.</error>', count($this->reports[UuidUpdateReport::UNWRITABLE]))); + if ($output->isVerbose()) { + /** @var UuidUpdateReport $report */ + foreach ($this->reports[UuidUpdateReport::UNWRITABLE] as $report) { + $output->writeln(sprintf(' %s: %s',$report->isUser ? 'User' : 'Group', $report->id)); + } + } + } + } + + protected function handleUpdates(InputInterface $input): \Generator { + if ($input->getOption('all')) { + foreach($this->handleMappingBasedUpdates(false) as $_) { + yield; + } + } else if ($input->getOption('userId') + || $input->getOption('groupId') + || $input->getOption('dn') + ) { + foreach($this->handleUpdatesByUserId($input->getOption('userId')) as $_) { + yield; + } + foreach($this->handleUpdatesByGroupId($input->getOption('groupId')) as $_) { + yield; + } + foreach($this->handleUpdatesByDN($input->getOption('dn')) as $_) { + yield; + } + } else { + foreach($this->handleMappingBasedUpdates(true) as $_) { + yield; + } + } + } + + protected function handleUpdatesByUserId(array $userIds): \Generator { + foreach($this->handleUpdatesByEntryId($userIds, $this->userMapping) as $_) { + yield; + } + } + + protected function handleUpdatesByGroupId(array $groupIds): \Generator { + foreach($this->handleUpdatesByEntryId($groupIds, $this->groupMapping) as $_) { + yield; + } + } + + protected function handleUpdatesByDN(array $dns): \Generator { + $userList = $groupList = []; + while ($dn = array_pop($dns)) { + $uuid = $this->userMapping->getUUIDByDN($dn); + if ($uuid) { + $id = $this->userMapping->getNameByDN($dn); + $userList[] = ['name' => $id, 'uuid' => $uuid]; + continue; + } + $uuid = $this->groupMapping->getUUIDByDN($dn); + if ($uuid) { + $id = $this->groupMapping->getNameByDN($dn); + $groupList[] = ['name' => $id, 'uuid' => $uuid]; + continue; + } + $this->reports[UuidUpdateReport::UNMAPPED][] = new UuidUpdateReport('', $dn, true, UuidUpdateReport::UNMAPPED); + yield; + } + foreach($this->handleUpdatesByList($this->userMapping, $userList) as $_) { + yield; + } + foreach($this->handleUpdatesByList($this->groupMapping, $groupList) as $_) { + yield; + } + } + + protected function handleUpdatesByEntryId(array $ids, AbstractMapping $mapping): \Generator { + $isUser = $mapping instanceof UserMapping; + $list = []; + while ($id = array_pop($ids)) { + if(!$dn = $mapping->getDNByName($id)) { + $this->reports[UuidUpdateReport::UNMAPPED][] = new UuidUpdateReport($id, '', $isUser, UuidUpdateReport::UNMAPPED); + yield; + continue; + } + // Since we know it was mapped the UUID is populated + $uuid = $mapping->getUUIDByDN($dn); + $list[] = ['name' => $id, 'uuid' => $uuid]; + } + foreach($this->handleUpdatesByList($mapping, $list) as $_) { + yield; + } + } + + protected function handleMappingBasedUpdates(bool $invalidatedOnly): \Generator { + $limit = 1000; + /** @var AbstractMapping $mapping*/ + foreach([$this->userMapping, $this->groupMapping] as $mapping) { + $offset = 0; + do { + $list = $mapping->getList($offset, $limit, $invalidatedOnly); + $offset += $limit; + + foreach($this->handleUpdatesByList($mapping, $list) as $tick) { + yield; // null, for it only advances progress counter + } + } while (count($list) === $limit); + } + } + + protected function handleUpdatesByList(AbstractMapping $mapping, array $list): \Generator { + if ($mapping instanceof UserMapping) { + $isUser = true; + $backendProxy = $this->userProxy; + } else { + $isUser = false; + $backendProxy = $this->groupProxy; + } + + foreach ($list as $row) { + $access = $backendProxy->getLDAPAccess($row['name']); + if ($access instanceof Access + && $dn = $mapping->getDNByName($row['name'])) + { + if ($uuid = $access->getUUID($dn, $isUser)) { + if ($uuid !== $row['uuid']) { + if ($this->dryRun || $mapping->setUUIDbyDN($uuid, $dn)) { + $this->reports[UuidUpdateReport::UPDATED][] + = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UPDATED, $row['uuid'], $uuid); + } else { + $this->reports[UuidUpdateReport::UNWRITABLE][] + = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UNWRITABLE, $row['uuid'], $uuid); + } + $this->logger->info('UUID of {id} was updated from {from} to {to}', + [ + 'appid' => 'user_ldap', + 'id' => $row['name'], + 'from' => $row['uuid'], + 'to' => $uuid, + ] + ); + } + } else { + $this->reports[UuidUpdateReport::UNREADABLE][] = new UuidUpdateReport($row['name'], $dn, $isUser, UuidUpdateReport::UNREADABLE); + } + } else { + $this->reports[UuidUpdateReport::UNKNOWN][] = new UuidUpdateReport($row['name'], '', $isUser, UuidUpdateReport::UNKNOWN); + } + yield; // null, for it only advances progress counter + } + } + + protected function estimateNumberOfUpdates(InputInterface $input): int { + if ($input->getOption('all')) { + return $this->userMapping->count() + $this->groupMapping->count(); + } else if ($input->getOption('userId') + || $input->getOption('groupId') + || $input->getOption('dn') + ) { + return count($input->getOption('userId')) + + count($input->getOption('groupId')) + + count($input->getOption('dn')); + } else { + return $this->userMapping->countInvalidated() + $this->groupMapping->countInvalidated(); + } + } + +} diff --git a/apps/user_ldap/lib/Connection.php b/apps/user_ldap/lib/Connection.php index 6666da1e933..3cd6a340a56 100644 --- a/apps/user_ldap/lib/Connection.php +++ b/apps/user_ldap/lib/Connection.php @@ -260,7 +260,7 @@ class Connection extends LDAPUtility { } $key = $this->getCacheKey($key); - return json_decode(base64_decode($this->cache->get($key)), true); + return json_decode(base64_decode($this->cache->get($key) ?? ''), true); } /** diff --git a/apps/user_ldap/lib/Helper.php b/apps/user_ldap/lib/Helper.php index 650755842b6..437fab6b6a8 100644 --- a/apps/user_ldap/lib/Helper.php +++ b/apps/user_ldap/lib/Helper.php @@ -129,10 +129,10 @@ class Helper { sort($serverConnections); $lastKey = array_pop($serverConnections); $lastNumber = (int)str_replace('s', '', $lastKey); - return 's' . str_pad($lastNumber + 1, 2, '0', STR_PAD_LEFT); + return 's' . str_pad((string)($lastNumber + 1), 2, '0', STR_PAD_LEFT); } - private function getServersConfig($value) { + private function getServersConfig(string $value): array { $regex = '/' . $value . '$/S'; $keys = $this->config->getAppKeys('user_ldap'); @@ -211,7 +211,7 @@ class Helper { /** * sanitizes a DN received from the LDAP server * - * @param array $dn the DN in question + * @param array|string $dn the DN in question * @return array|string the sanitized DN */ public function sanitizeDN($dn) { @@ -275,10 +275,10 @@ class Helper { * listens to a hook thrown by server2server sharing and replaces the given * login name by a username, if it matches an LDAP user. * - * @param array $param + * @param array $param contains a reference to a $uid var under 'uid' key * @throws \Exception */ - public static function loginName2UserName($param) { + public static function loginName2UserName($param): void { if (!isset($param['uid'])) { throw new \Exception('key uid is expected to be set in $param'); } diff --git a/apps/user_ldap/lib/Jobs/CleanUp.php b/apps/user_ldap/lib/Jobs/CleanUp.php index ee6879d452f..1fb423b5faf 100644 --- a/apps/user_ldap/lib/Jobs/CleanUp.php +++ b/apps/user_ldap/lib/Jobs/CleanUp.php @@ -40,7 +40,7 @@ use OCA\User_LDAP\User_Proxy; * @package OCA\User_LDAP\Jobs; */ class CleanUp extends TimedJob { - /** @var int $limit amount of users that should be checked per run */ + /** @var ?int $limit amount of users that should be checked per run */ protected $limit; /** @var int $defaultIntervalMin default interval in minutes */ @@ -76,7 +76,7 @@ class CleanUp extends TimedJob { * assigns the instances passed to run() to the class properties * @param array $arguments */ - public function setArguments($arguments) { + public function setArguments($arguments): void { //Dependency Injection is not possible, because the constructor will //only get values that are serialized to JSON. I.e. whatever we would //pass in app.php we do add here, except something else is passed e.g. @@ -119,19 +119,13 @@ class CleanUp extends TimedJob { * makes the background job do its work * @param array $argument */ - public function run($argument) { + public function run($argument): void { $this->setArguments($argument); if (!$this->isCleanUpAllowed()) { return; } $users = $this->mapping->getList($this->getOffset(), $this->getChunkSize()); - if (!is_array($users)) { - //something wrong? Let's start from the beginning next time and - //abort - $this->setOffset(true); - return; - } $resetOffset = $this->isOffsetResetNecessary(count($users)); $this->checkUsers($users); $this->setOffset($resetOffset); @@ -139,18 +133,15 @@ class CleanUp extends TimedJob { /** * checks whether next run should start at 0 again - * @param int $resultCount - * @return bool */ - public function isOffsetResetNecessary($resultCount) { + public function isOffsetResetNecessary(int $resultCount): bool { return $resultCount < $this->getChunkSize(); } /** * checks whether cleaning up LDAP users is allowed - * @return bool */ - public function isCleanUpAllowed() { + public function isCleanUpAllowed(): bool { try { if ($this->ldapHelper->haveDisabledConfigurations()) { return false; @@ -164,9 +155,8 @@ class CleanUp extends TimedJob { /** * checks whether clean up is enabled by configuration - * @return bool */ - private function isCleanUpEnabled() { + private function isCleanUpEnabled(): bool { return (bool)$this->ocConfig->getSystemValue( 'ldapUserCleanupInterval', (string)$this->defaultIntervalMin); } @@ -175,7 +165,7 @@ class CleanUp extends TimedJob { * checks users whether they are still existing * @param array $users result from getMappedUsers() */ - private function checkUsers(array $users) { + private function checkUsers(array $users): void { foreach ($users as $user) { $this->checkUser($user); } @@ -185,7 +175,7 @@ class CleanUp extends TimedJob { * checks whether a user is still existing in LDAP * @param string[] $user */ - private function checkUser(array $user) { + private function checkUser(array $user): void { if ($this->userBackend->userExistsOnLDAP($user['name'])) { //still available, all good @@ -197,29 +187,27 @@ class CleanUp extends TimedJob { /** * gets the offset to fetch users from the mappings table - * @return int */ - private function getOffset() { - return (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobOffset', 0); + private function getOffset(): int { + return (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobOffset', '0'); } /** * sets the new offset for the next run * @param bool $reset whether the offset should be set to 0 */ - public function setOffset($reset = false) { + public function setOffset(bool $reset = false): void { $newOffset = $reset ? 0 : $this->getOffset() + $this->getChunkSize(); - $this->ocConfig->setAppValue('user_ldap', 'cleanUpJobOffset', $newOffset); + $this->ocConfig->setAppValue('user_ldap', 'cleanUpJobOffset', (string)$newOffset); } /** * returns the chunk size (limit in DB speak) - * @return int */ - public function getChunkSize() { + public function getChunkSize(): int { if ($this->limit === null) { - $this->limit = (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobChunkSize', 50); + $this->limit = (int)$this->ocConfig->getAppValue('user_ldap', 'cleanUpJobChunkSize', '50'); } return $this->limit; } diff --git a/apps/user_ldap/lib/Mapping/AbstractMapping.php b/apps/user_ldap/lib/Mapping/AbstractMapping.php index 1d3e1a221f7..16973f76ff4 100644 --- a/apps/user_ldap/lib/Mapping/AbstractMapping.php +++ b/apps/user_ldap/lib/Mapping/AbstractMapping.php @@ -175,7 +175,7 @@ abstract class AbstractMapping { * @param $fdn * @return bool */ - public function setUUIDbyDN($uuid, $fdn) { + public function setUUIDbyDN($uuid, $fdn): bool { $statement = $this->dbc->prepare(' UPDATE `' . $this->getTableName() . '` SET `directory_uuid` = ? @@ -329,26 +329,24 @@ abstract class AbstractMapping { return $this->getXbyY('directory_uuid', 'ldap_dn_hash', $this->getDNHash($dn)); } - /** - * gets a piece of the mapping list - * - * @param int $offset - * @param int $limit - * @return array - */ - public function getList($offset = null, $limit = null) { - $query = $this->dbc->prepare(' - SELECT - `ldap_dn` AS `dn`, - `owncloud_name` AS `name`, - `directory_uuid` AS `uuid` - FROM `' . $this->getTableName() . '`', - $limit, - $offset - ); - - $query->execute(); - return $query->fetchAll(); + public function getList(int $offset = 0, int $limit = null, bool $invalidatedOnly = false): array { + $select = $this->dbc->getQueryBuilder(); + $select->selectAlias('ldap_dn', 'dn') + ->selectAlias('owncloud_name', 'name') + ->selectAlias('directory_uuid', 'uuid') + ->from($this->getTableName()) + ->setMaxResults($limit) + ->setFirstResult($offset); + + if ($invalidatedOnly) { + $select->where($select->expr()->like('directory_uuid', $select->createNamedParameter('invalidated_%'))); + } + + $result = $select->executeQuery(); + $entries = $result->fetchAll(); + $result->closeCursor(); + + return $entries; } /** @@ -458,13 +456,24 @@ abstract class AbstractMapping { * * @return int */ - public function count() { - $qb = $this->dbc->getQueryBuilder(); - $query = $qb->select($qb->func()->count('ldap_dn_hash')) + public function count(): int { + $query = $this->dbc->getQueryBuilder(); + $query->select($query->func()->count('ldap_dn_hash')) ->from($this->getTableName()); $res = $query->execute(); $count = $res->fetchOne(); $res->closeCursor(); return (int)$count; } + + public function countInvalidated(): int { + $query = $this->dbc->getQueryBuilder(); + $query->select($query->func()->count('ldap_dn_hash')) + ->from($this->getTableName()) + ->where($query->expr()->like('directory_uuid', $query->createNamedParameter('invalidated_%'))); + $res = $query->execute(); + $count = $res->fetchOne(); + $res->closeCursor(); + return (int)$count; + } } diff --git a/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php b/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php index 8695f90ca65..27f5f5ce504 100644 --- a/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php +++ b/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php @@ -27,6 +27,7 @@ declare(strict_types=1); namespace OCA\User_LDAP\Migration; use Closure; +use Generator; use OCP\DB\Exception; use OCP\DB\ISchemaWrapper; use OCP\DB\QueryBuilder\IQueryBuilder; @@ -52,6 +53,23 @@ class Version1130Date20211102154716 extends SimpleMigrationStep { return 'Adjust LDAP user and group ldap_dn column lengths and add ldap_dn_hash columns'; } + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) { + foreach (['ldap_user_mapping', 'ldap_group_mapping'] as $tableName) { + $this->processDuplicateUUIDs($tableName); + } + + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + if ($schema->hasTable('ldap_group_mapping_backup')) { + // Previous upgrades of a broken release might have left an incomplete + // ldap_group_mapping_backup table. No need to recreate, but it + // should be empty. + // TRUNCATE is not available from Query Builder, but faster than DELETE FROM. + $sql = $this->dbc->getDatabasePlatform()->getTruncateTableSQL('ldap_group_mapping_backup', false); + $this->dbc->executeStatement($sql); + } + } + /** * @param IOutput $output * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` @@ -91,7 +109,7 @@ class Version1130Date20211102154716 extends SimpleMigrationStep { $table->addUniqueIndex(['directory_uuid'], 'ldap_user_directory_uuid'); $changeSchema = true; } - } else { + } else if (!$schema->hasTable('ldap_group_mapping_backup')) { // We need to copy the table twice to be able to change primary key, prepare the backup table $table2 = $schema->createTable('ldap_group_mapping_backup'); $table2->addColumn('ldap_dn', Types::STRING, [ @@ -172,4 +190,87 @@ class Version1130Date20211102154716 extends SimpleMigrationStep { ->where($qb->expr()->eq('owncloud_name', $qb->createParameter('name'))); return $qb; } + + /** + * @throws Exception + */ + protected function processDuplicateUUIDs(string $table): void { + $uuids = $this->getDuplicatedUuids($table); + $idsWithUuidToInvalidate = []; + foreach ($uuids as $uuid) { + array_push($idsWithUuidToInvalidate, ...$this->getNextcloudIdsByUuid($table, $uuid)); + } + $this->invalidateUuids($table, $idsWithUuidToInvalidate); + } + + /** + * @throws Exception + */ + protected function invalidateUuids(string $table, array $idList): void { + $update = $this->dbc->getQueryBuilder(); + $update->update($table) + ->set('directory_uuid', $update->createParameter('invalidatedUuid')) + ->where($update->expr()->eq('owncloud_name', $update->createParameter('nextcloudId'))); + + while ($nextcloudId = array_shift($idList)) { + $update->setParameter('nextcloudId', $nextcloudId); + $update->setParameter('invalidatedUuid', 'invalidated_' . \bin2hex(\random_bytes(6))); + try { + $update->executeStatement(); + $this->logger->warning( + 'LDAP user or group with ID {nid} has a duplicated UUID value which therefore was invalidated. You may double-check your LDAP configuration and trigger an update of the UUID.', + [ + 'app' => 'user_ldap', + 'nid' => $nextcloudId, + ] + ); + } catch (Exception $e) { + // Catch possible, but unlikely duplications if new invalidated errors. + // There is the theoretical chance of an infinity loop is, when + // the constraint violation has a different background. I cannot + // think of one at the moment. + if ($e->getReason() !== Exception::REASON_CONSTRAINT_VIOLATION) { + throw $e; + } + $idList[] = $nextcloudId; + } + } + } + + /** + * @throws \OCP\DB\Exception + * @return array<string> + */ + protected function getNextcloudIdsByUuid(string $table, string $uuid): array { + $select = $this->dbc->getQueryBuilder(); + $select->select('owncloud_name') + ->from($table) + ->where($select->expr()->eq('directory_uuid', $select->createNamedParameter($uuid))); + + $result = $select->executeQuery(); + $idList = []; + while ($id = $result->fetchOne()) { + $idList[] = $id; + } + $result->closeCursor(); + return $idList; + } + + /** + * @return Generator<string> + * @throws \OCP\DB\Exception + */ + protected function getDuplicatedUuids(string $table): Generator{ + $select = $this->dbc->getQueryBuilder(); + $select->select('directory_uuid') + ->from($table) + ->groupBy('directory_uuid') + ->having($select->expr()->gt($select->func()->count('owncloud_name'), $select->createNamedParameter(1))); + + $result = $select->executeQuery(); + while ($uuid = $result->fetchOne()) { + yield $uuid; + } + $result->closeCursor(); + } } diff --git a/apps/user_ldap/tests/Mapping/AbstractMappingTest.php b/apps/user_ldap/tests/Mapping/AbstractMappingTest.php index 9c25b1d9af6..0d21172445f 100644 --- a/apps/user_ldap/tests/Mapping/AbstractMappingTest.php +++ b/apps/user_ldap/tests/Mapping/AbstractMappingTest.php @@ -276,7 +276,7 @@ abstract class AbstractMappingTest extends \Test\TestCase { $this->assertSame(count($data) - 1, count($results)); // get first 2 entries by limit, but not offset - $results = $mapper->getList(null, 2); + $results = $mapper->getList(0, 2); $this->assertSame(2, count($results)); // get 2nd entry by specifying both offset and limit diff --git a/apps/weather_status/l10n/sv.js b/apps/weather_status/l10n/sv.js index 3ebc604b74a..9c9d5905844 100644 --- a/apps/weather_status/l10n/sv.js +++ b/apps/weather_status/l10n/sv.js @@ -11,28 +11,28 @@ OC.L10N.register( "Detect location" : "Hitta min position", "Set custom address" : "Uppge egen adress", "Favorites" : "Favoriter", - "{temperature} {unit} clear sky later today" : "{temperature} {unit} klar himmel senare idag", - "{temperature} {unit} clear sky" : "{temperature} {unit} klar himmel", - "{temperature} {unit} cloudy later today" : "{temperature} {unit} växlande molnighet senare idag", - "{temperature} {unit} cloudy" : "{temperature} {unit} mulet", - "{temperature} {unit} fair weather later today" : "{temperature} {unit} klart väder senare idag", - "{temperature} {unit} fair weather" : "{temperature} {unit} klart väder", - "{temperature} {unit} partly cloudy later today" : "{temperature} {unit} växlande molnighet senare idag", - "{temperature} {unit} partly cloudy" : "{temperature} {unit} växlande molnighet", - "{temperature} {unit} foggy later today" : "{temperature} {unit} dimmigt senare idag", - "{temperature} {unit} foggy" : "{temperature} {unit} dimmigt", - "{temperature} {unit} light rain later today" : "{temperature} {unit} lätt regn senare idag", - "{temperature} {unit} light rain" : "{temperature} {unit} lätt regn", - "{temperature} {unit} rain later today" : "{temperature} {unit} regn senare idag", - "{temperature} {unit} rain" : "{temperature} {unit} regn", - "{temperature} {unit} heavy rain later today" : "{temperature} {unit} kraftigt regn senare idag", - "{temperature} {unit} heavy rain" : "{temperature} {unit} kraftigt regn", - "{temperature} {unit} rain showers later today" : "{temperature} {unit} regnbyar senare idag", - "{temperature} {unit} rain showers" : "{temperature} {unit} regnbyar", - "{temperature} {unit} light rain showers later today" : "{temperature} {unit} lätta regnbyar senare idag", - "{temperature} {unit} light rain showers" : "{temperature} {unit} lätta regnbyar", - "{temperature} {unit} heavy rain showers later today" : "{temperature} {unit} kraftiga regnbyar senare idag", - "{temperature} {unit} heavy rain showers" : "{temperature} {unit} kraftiga regnbyar", + "{temperature} {unit} clear sky later today" : "{temperature} {unit} och klar himmel senare idag", + "{temperature} {unit} clear sky" : "{temperature} {unit} och klar himmel", + "{temperature} {unit} cloudy later today" : "{temperature} {unit} och mulet senare idag", + "{temperature} {unit} cloudy" : "{temperature} {unit} och mulet", + "{temperature} {unit} fair weather later today" : "{temperature} {unit} och klart väder senare idag", + "{temperature} {unit} fair weather" : "{temperature} {unit} och klart väder", + "{temperature} {unit} partly cloudy later today" : "{temperature} {unit} och växlande molnighet senare idag", + "{temperature} {unit} partly cloudy" : "{temperature} {unit} och växlande molnighet", + "{temperature} {unit} foggy later today" : "{temperature} {unit} och dimma senare idag", + "{temperature} {unit} foggy" : "{temperature} {unit} och dimma", + "{temperature} {unit} light rain later today" : "{temperature} {unit} och lätt regn senare idag", + "{temperature} {unit} light rain" : "{temperature} {unit} och lätt regn", + "{temperature} {unit} rain later today" : "{temperature} {unit} och regn senare idag", + "{temperature} {unit} rain" : "{temperature} {unit} och regn", + "{temperature} {unit} heavy rain later today" : "{temperature} {unit} och kraftigt regn senare idag", + "{temperature} {unit} heavy rain" : "{temperature} {unit} och kraftigt regn", + "{temperature} {unit} rain showers later today" : "{temperature} {unit} och regnbyar senare idag", + "{temperature} {unit} rain showers" : "{temperature} {unit} och regnbyar", + "{temperature} {unit} light rain showers later today" : "{temperature} {unit} och lätta regnbyar senare idag", + "{temperature} {unit} light rain showers" : "{temperature} {unit} och lätta regnbyar", + "{temperature} {unit} heavy rain showers later today" : "{temperature} {unit} och kraftiga regnbyar senare idag", + "{temperature} {unit} heavy rain showers" : "{temperature} {unit} och kraftiga regnbyar", "More weather for {adr}" : "Mer väder omkring {adr}", "Loading weather" : "Hämtar väder", "Remove from favorites" : "Ta bort från favoriter", @@ -47,17 +47,17 @@ OC.L10N.register( "There was an error using personal address." : "Det uppstod ett fel vid användning av personlig adress.", "Set location for weather" : "Ange position för väder", "Weather status integrated in the dashboard app.\n User's position can be automatically determined or manually defined. A 6 hours forecast is then displayed.\n This status can also be integrated in other places like the Calendar app." : "Väderuppdatering integrerad i Instrumentpanelappen.\n Användarens position kan identifieras automatiskt eller anges manuellt. En 6-timmarsprognos visas sedan.\n Den här väderuppdateringen kan också integreras på andra platser så som i Kalender-appen.", - "{temperature} {unit} Clear sky at {time}" : "{temperature} {unit} klar himmel klockan {time}", - "{temperature} {unit} Cloudy at {time}" : "{temperature} {unit} mulet klockan {time}", - "{temperature} {unit} Fair day at {time}" : "{temperature} {unit} klar dag klockan {time}", - "{temperature} {unit} Fair night at {time}" : "{temperature} {unit} klar natt klockan {time}", - "{temperature} {unit} Partly cloudy at {time}" : "{temperature} {unit} växlande molnighet klockan {time}", - "{temperature} {unit} Foggy at {time}" : "{temperature} {unit} dimma klockan {time}", - "{temperature} {unit} Light rain at {time}" : "{temperature} {unit} lätt regn klockan {time}", - "{temperature} {unit} Rain at {time}" : "{temperature} {unit} regn klockan {time}", - "{temperature} {unit} Heavy rain at {time}" : "{temperature} {unit} kraftigt regn klockan {time}", - "{temperature} {unit} Rain showers at {time}" : "{temperature} {unit} regnbyar klockan {time}", - "{temperature} {unit} Light rain showers at {time}" : "{temperature} {unit} lätta regnbyar klockan {time}", - "{temperature} {unit} Heavy rain showers at {time}" : "{temperature} {unit} kraftiga regnbyar klockan {time}" + "{temperature} {unit} Clear sky at {time}" : "{temperature} {unit} och klar himmel klockan {time}", + "{temperature} {unit} Cloudy at {time}" : "{temperature} {unit} och mulet klockan {time}", + "{temperature} {unit} Fair day at {time}" : "{temperature} {unit} och klar dag klockan {time}", + "{temperature} {unit} Fair night at {time}" : "{temperature} {unit} och stjärnklart klockan {time}", + "{temperature} {unit} Partly cloudy at {time}" : "{temperature} {unit} och växlande molnighet klockan {time}", + "{temperature} {unit} Foggy at {time}" : "{temperature} {unit} och dimma klockan {time}", + "{temperature} {unit} Light rain at {time}" : "{temperature} {unit} och lätt regn klockan {time}", + "{temperature} {unit} Rain at {time}" : "{temperature} {unit} och regn klockan {time}", + "{temperature} {unit} Heavy rain at {time}" : "{temperature} {unit} och kraftigt regn klockan {time}", + "{temperature} {unit} Rain showers at {time}" : "{temperature} {unit} och regnbyar klockan {time}", + "{temperature} {unit} Light rain showers at {time}" : "{temperature} {unit} och lätta regnbyar klockan {time}", + "{temperature} {unit} Heavy rain showers at {time}" : "{temperature} {unit} och kraftiga regnbyar klockan {time}" }, "nplurals=2; plural=(n != 1);"); diff --git a/apps/weather_status/l10n/sv.json b/apps/weather_status/l10n/sv.json index 342b4845232..814fc6f17cb 100644 --- a/apps/weather_status/l10n/sv.json +++ b/apps/weather_status/l10n/sv.json @@ -9,28 +9,28 @@ "Detect location" : "Hitta min position", "Set custom address" : "Uppge egen adress", "Favorites" : "Favoriter", - "{temperature} {unit} clear sky later today" : "{temperature} {unit} klar himmel senare idag", - "{temperature} {unit} clear sky" : "{temperature} {unit} klar himmel", - "{temperature} {unit} cloudy later today" : "{temperature} {unit} växlande molnighet senare idag", - "{temperature} {unit} cloudy" : "{temperature} {unit} mulet", - "{temperature} {unit} fair weather later today" : "{temperature} {unit} klart väder senare idag", - "{temperature} {unit} fair weather" : "{temperature} {unit} klart väder", - "{temperature} {unit} partly cloudy later today" : "{temperature} {unit} växlande molnighet senare idag", - "{temperature} {unit} partly cloudy" : "{temperature} {unit} växlande molnighet", - "{temperature} {unit} foggy later today" : "{temperature} {unit} dimmigt senare idag", - "{temperature} {unit} foggy" : "{temperature} {unit} dimmigt", - "{temperature} {unit} light rain later today" : "{temperature} {unit} lätt regn senare idag", - "{temperature} {unit} light rain" : "{temperature} {unit} lätt regn", - "{temperature} {unit} rain later today" : "{temperature} {unit} regn senare idag", - "{temperature} {unit} rain" : "{temperature} {unit} regn", - "{temperature} {unit} heavy rain later today" : "{temperature} {unit} kraftigt regn senare idag", - "{temperature} {unit} heavy rain" : "{temperature} {unit} kraftigt regn", - "{temperature} {unit} rain showers later today" : "{temperature} {unit} regnbyar senare idag", - "{temperature} {unit} rain showers" : "{temperature} {unit} regnbyar", - "{temperature} {unit} light rain showers later today" : "{temperature} {unit} lätta regnbyar senare idag", - "{temperature} {unit} light rain showers" : "{temperature} {unit} lätta regnbyar", - "{temperature} {unit} heavy rain showers later today" : "{temperature} {unit} kraftiga regnbyar senare idag", - "{temperature} {unit} heavy rain showers" : "{temperature} {unit} kraftiga regnbyar", + "{temperature} {unit} clear sky later today" : "{temperature} {unit} och klar himmel senare idag", + "{temperature} {unit} clear sky" : "{temperature} {unit} och klar himmel", + "{temperature} {unit} cloudy later today" : "{temperature} {unit} och mulet senare idag", + "{temperature} {unit} cloudy" : "{temperature} {unit} och mulet", + "{temperature} {unit} fair weather later today" : "{temperature} {unit} och klart väder senare idag", + "{temperature} {unit} fair weather" : "{temperature} {unit} och klart väder", + "{temperature} {unit} partly cloudy later today" : "{temperature} {unit} och växlande molnighet senare idag", + "{temperature} {unit} partly cloudy" : "{temperature} {unit} och växlande molnighet", + "{temperature} {unit} foggy later today" : "{temperature} {unit} och dimma senare idag", + "{temperature} {unit} foggy" : "{temperature} {unit} och dimma", + "{temperature} {unit} light rain later today" : "{temperature} {unit} och lätt regn senare idag", + "{temperature} {unit} light rain" : "{temperature} {unit} och lätt regn", + "{temperature} {unit} rain later today" : "{temperature} {unit} och regn senare idag", + "{temperature} {unit} rain" : "{temperature} {unit} och regn", + "{temperature} {unit} heavy rain later today" : "{temperature} {unit} och kraftigt regn senare idag", + "{temperature} {unit} heavy rain" : "{temperature} {unit} och kraftigt regn", + "{temperature} {unit} rain showers later today" : "{temperature} {unit} och regnbyar senare idag", + "{temperature} {unit} rain showers" : "{temperature} {unit} och regnbyar", + "{temperature} {unit} light rain showers later today" : "{temperature} {unit} och lätta regnbyar senare idag", + "{temperature} {unit} light rain showers" : "{temperature} {unit} och lätta regnbyar", + "{temperature} {unit} heavy rain showers later today" : "{temperature} {unit} och kraftiga regnbyar senare idag", + "{temperature} {unit} heavy rain showers" : "{temperature} {unit} och kraftiga regnbyar", "More weather for {adr}" : "Mer väder omkring {adr}", "Loading weather" : "Hämtar väder", "Remove from favorites" : "Ta bort från favoriter", @@ -45,17 +45,17 @@ "There was an error using personal address." : "Det uppstod ett fel vid användning av personlig adress.", "Set location for weather" : "Ange position för väder", "Weather status integrated in the dashboard app.\n User's position can be automatically determined or manually defined. A 6 hours forecast is then displayed.\n This status can also be integrated in other places like the Calendar app." : "Väderuppdatering integrerad i Instrumentpanelappen.\n Användarens position kan identifieras automatiskt eller anges manuellt. En 6-timmarsprognos visas sedan.\n Den här väderuppdateringen kan också integreras på andra platser så som i Kalender-appen.", - "{temperature} {unit} Clear sky at {time}" : "{temperature} {unit} klar himmel klockan {time}", - "{temperature} {unit} Cloudy at {time}" : "{temperature} {unit} mulet klockan {time}", - "{temperature} {unit} Fair day at {time}" : "{temperature} {unit} klar dag klockan {time}", - "{temperature} {unit} Fair night at {time}" : "{temperature} {unit} klar natt klockan {time}", - "{temperature} {unit} Partly cloudy at {time}" : "{temperature} {unit} växlande molnighet klockan {time}", - "{temperature} {unit} Foggy at {time}" : "{temperature} {unit} dimma klockan {time}", - "{temperature} {unit} Light rain at {time}" : "{temperature} {unit} lätt regn klockan {time}", - "{temperature} {unit} Rain at {time}" : "{temperature} {unit} regn klockan {time}", - "{temperature} {unit} Heavy rain at {time}" : "{temperature} {unit} kraftigt regn klockan {time}", - "{temperature} {unit} Rain showers at {time}" : "{temperature} {unit} regnbyar klockan {time}", - "{temperature} {unit} Light rain showers at {time}" : "{temperature} {unit} lätta regnbyar klockan {time}", - "{temperature} {unit} Heavy rain showers at {time}" : "{temperature} {unit} kraftiga regnbyar klockan {time}" + "{temperature} {unit} Clear sky at {time}" : "{temperature} {unit} och klar himmel klockan {time}", + "{temperature} {unit} Cloudy at {time}" : "{temperature} {unit} och mulet klockan {time}", + "{temperature} {unit} Fair day at {time}" : "{temperature} {unit} och klar dag klockan {time}", + "{temperature} {unit} Fair night at {time}" : "{temperature} {unit} och stjärnklart klockan {time}", + "{temperature} {unit} Partly cloudy at {time}" : "{temperature} {unit} och växlande molnighet klockan {time}", + "{temperature} {unit} Foggy at {time}" : "{temperature} {unit} och dimma klockan {time}", + "{temperature} {unit} Light rain at {time}" : "{temperature} {unit} och lätt regn klockan {time}", + "{temperature} {unit} Rain at {time}" : "{temperature} {unit} och regn klockan {time}", + "{temperature} {unit} Heavy rain at {time}" : "{temperature} {unit} och kraftigt regn klockan {time}", + "{temperature} {unit} Rain showers at {time}" : "{temperature} {unit} och regnbyar klockan {time}", + "{temperature} {unit} Light rain showers at {time}" : "{temperature} {unit} och lätta regnbyar klockan {time}", + "{temperature} {unit} Heavy rain showers at {time}" : "{temperature} {unit} och kraftiga regnbyar klockan {time}" },"pluralForm" :"nplurals=2; plural=(n != 1);" }
\ No newline at end of file |