diff options
Diffstat (limited to 'apps')
48 files changed, 1530 insertions, 78 deletions
diff --git a/apps/accessibility/l10n/zh_CN.js b/apps/accessibility/l10n/zh_CN.js index e0397935e14..c2fcf620956 100644 --- a/apps/accessibility/l10n/zh_CN.js +++ b/apps/accessibility/l10n/zh_CN.js @@ -3,7 +3,7 @@ OC.L10N.register( { "Dark theme" : "深色主题", "Enable dark theme" : "启用深色主题", - "A dark theme to ease your eyes by reducing the overall luminosity and brightness. It is still under development, so please report any issues you may find." : "一款通过降低整体亮度来使您的眼睛放松的深色主题。该主题目前还在开发中,请您及时向我们反馈您可能发现的任何问题。", + "A dark theme to ease your eyes by reducing the overall luminosity and brightness. It is still under development, so please report any issues you may find." : "深色主题通过降低色彩的明度和亮度来使您的眼睛保持舒适。这个功能还在完善中,所以请您随时向我们反馈所发现的问题。", "High contrast mode" : "高对比度模式", "Enable high contrast mode" : "启用高对比度模式", "A high contrast mode to ease your navigation. Visual quality will be reduced but clarity will be increased." : "使用高对比度模式。图像质量会下降,但清晰度会提升。", diff --git a/apps/accessibility/l10n/zh_CN.json b/apps/accessibility/l10n/zh_CN.json index f06f85dc3a8..932435e6ba1 100644 --- a/apps/accessibility/l10n/zh_CN.json +++ b/apps/accessibility/l10n/zh_CN.json @@ -1,7 +1,7 @@ { "translations": { "Dark theme" : "深色主题", "Enable dark theme" : "启用深色主题", - "A dark theme to ease your eyes by reducing the overall luminosity and brightness. It is still under development, so please report any issues you may find." : "一款通过降低整体亮度来使您的眼睛放松的深色主题。该主题目前还在开发中,请您及时向我们反馈您可能发现的任何问题。", + "A dark theme to ease your eyes by reducing the overall luminosity and brightness. It is still under development, so please report any issues you may find." : "深色主题通过降低色彩的明度和亮度来使您的眼睛保持舒适。这个功能还在完善中,所以请您随时向我们反馈所发现的问题。", "High contrast mode" : "高对比度模式", "Enable high contrast mode" : "启用高对比度模式", "A high contrast mode to ease your navigation. Visual quality will be reduced but clarity will be increased." : "使用高对比度模式。图像质量会下降,但清晰度会提升。", diff --git a/apps/admin_audit/composer/composer/autoload_classmap.php b/apps/admin_audit/composer/composer/autoload_classmap.php index e52032ca3ea..5dcaa32bb8d 100644 --- a/apps/admin_audit/composer/composer/autoload_classmap.php +++ b/apps/admin_audit/composer/composer/autoload_classmap.php @@ -19,6 +19,8 @@ return array( 'OCA\\AdminAudit\\Actions\\UserManagement' => $baseDir . '/../lib/Actions/UserManagement.php', 'OCA\\AdminAudit\\Actions\\Versions' => $baseDir . '/../lib/Actions/Versions.php', 'OCA\\AdminAudit\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', + 'OCA\\AdminAudit\\AuditLogger' => $baseDir . '/../lib/AuditLogger.php', 'OCA\\AdminAudit\\BackgroundJobs\\Rotate' => $baseDir . '/../lib/BackgroundJobs/Rotate.php', + 'OCA\\AdminAudit\\IAuditLogger' => $baseDir . '/../lib/IAuditLogger.php', 'OCA\\AdminAudit\\Listener\\CriticalActionPerformedEventListener' => $baseDir . '/../lib/Listener/CriticalActionPerformedEventListener.php', ); diff --git a/apps/admin_audit/composer/composer/autoload_static.php b/apps/admin_audit/composer/composer/autoload_static.php index 829bc2ab049..38518c8a9ba 100644 --- a/apps/admin_audit/composer/composer/autoload_static.php +++ b/apps/admin_audit/composer/composer/autoload_static.php @@ -34,7 +34,9 @@ class ComposerStaticInitAdminAudit 'OCA\\AdminAudit\\Actions\\UserManagement' => __DIR__ . '/..' . '/../lib/Actions/UserManagement.php', 'OCA\\AdminAudit\\Actions\\Versions' => __DIR__ . '/..' . '/../lib/Actions/Versions.php', 'OCA\\AdminAudit\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', + 'OCA\\AdminAudit\\AuditLogger' => __DIR__ . '/..' . '/../lib/AuditLogger.php', 'OCA\\AdminAudit\\BackgroundJobs\\Rotate' => __DIR__ . '/..' . '/../lib/BackgroundJobs/Rotate.php', + 'OCA\\AdminAudit\\IAuditLogger' => __DIR__ . '/..' . '/../lib/IAuditLogger.php', 'OCA\\AdminAudit\\Listener\\CriticalActionPerformedEventListener' => __DIR__ . '/..' . '/../lib/Listener/CriticalActionPerformedEventListener.php', ); diff --git a/apps/admin_audit/lib/Actions/Action.php b/apps/admin_audit/lib/Actions/Action.php index 17be0fb8197..0eaf06b8c0f 100644 --- a/apps/admin_audit/lib/Actions/Action.php +++ b/apps/admin_audit/lib/Actions/Action.php @@ -28,13 +28,13 @@ declare(strict_types=1); */ namespace OCA\AdminAudit\Actions; -use Psr\Log\LoggerInterface; +use OCA\AdminAudit\IAuditLogger; class Action { - /** @var LoggerInterface */ + /** @var IAuditLogger */ private $logger; - public function __construct(LoggerInterface $logger) { + public function __construct(IAuditLogger $logger) { $this->logger = $logger; } diff --git a/apps/admin_audit/lib/AppInfo/Application.php b/apps/admin_audit/lib/AppInfo/Application.php index 594e1c7f2c4..1160d151710 100644 --- a/apps/admin_audit/lib/AppInfo/Application.php +++ b/apps/admin_audit/lib/AppInfo/Application.php @@ -48,6 +48,8 @@ use OCA\AdminAudit\Actions\Sharing; use OCA\AdminAudit\Actions\Trashbin; use OCA\AdminAudit\Actions\UserManagement; use OCA\AdminAudit\Actions\Versions; +use OCA\AdminAudit\AuditLogger; +use OCA\AdminAudit\IAuditLogger; use OCA\AdminAudit\Listener\CriticalActionPerformedEventListener; use OCP\App\ManagerEvent; use OCP\AppFramework\App; @@ -65,6 +67,7 @@ use OCP\Log\Audit\CriticalActionPerformedEvent; use OCP\Log\ILogFactory; use OCP\Share; use OCP\Util; +use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; @@ -79,14 +82,16 @@ class Application extends App implements IBootstrap { } public function register(IRegistrationContext $context): void { + $context->registerService(IAuditLogger::class, function (ContainerInterface $c) { + return new AuditLogger($c->get(ILogFactory::class), $c->get(Iconfig::class)); + }); + $context->registerEventListener(CriticalActionPerformedEvent::class, CriticalActionPerformedEventListener::class); } public function boot(IBootContext $context): void { - /** @var LoggerInterface $logger */ - $logger = $context->injectFn( - Closure::fromCallable([$this, 'getLogger']) - ); + /** @var IAuditLogger $logger */ + $logger = $context->getAppContainer()->get(IAuditLogger::class); /* * TODO: once the hooks are migrated to lazy events, this should be done @@ -95,26 +100,10 @@ class Application extends App implements IBootstrap { $this->registerHooks($logger, $context->getServerContainer()); } - private function getLogger(IConfig $config, - ILogFactory $logFactory): LoggerInterface { - $auditType = $config->getSystemValueString('log_type_audit', 'file'); - $defaultTag = $config->getSystemValueString('syslog_tag', 'Nextcloud'); - $auditTag = $config->getSystemValueString('syslog_tag_audit', $defaultTag); - $logFile = $config->getSystemValueString('logfile_audit', ''); - - if ($auditType === 'file' && !$logFile) { - $default = $config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/audit.log'; - // Legacy way was appconfig, now it's paralleled with the normal log config - $logFile = $config->getAppValue('admin_audit', 'logfile', $default); - } - - return $logFactory->getCustomPsrLogger($logFile, $auditType, $auditTag); - } - /** * Register hooks in order to log them */ - private function registerHooks(LoggerInterface $logger, + private function registerHooks(IAuditLogger $logger, IServerContainer $serverContainer): void { $this->userManagementHooks($logger, $serverContainer->get(IUserSession::class)); $this->groupHooks($logger, $serverContainer->get(IGroupManager::class)); @@ -134,7 +123,7 @@ class Application extends App implements IBootstrap { $this->securityHooks($logger, $eventDispatcher); } - private function userManagementHooks(LoggerInterface $logger, + private function userManagementHooks(IAuditLogger $logger, IUserSession $userSession): void { $userActions = new UserManagement($logger); @@ -148,7 +137,7 @@ class Application extends App implements IBootstrap { $userSession->listen('\OC\User', 'postUnassignedUserId', [$userActions, 'unassign']); } - private function groupHooks(LoggerInterface $logger, + private function groupHooks(IAuditLogger $logger, IGroupManager $groupManager): void { $groupActions = new GroupManagement($logger); @@ -159,7 +148,7 @@ class Application extends App implements IBootstrap { $groupManager->listen('\OC\Group', 'postCreate', [$groupActions, 'createGroup']); } - private function sharingHooks(LoggerInterface $logger): void { + private function sharingHooks(IAuditLogger $logger): void { $shareActions = new Sharing($logger); Util::connectHook(Share::class, 'post_shared', $shareActions, 'shared'); @@ -171,7 +160,7 @@ class Application extends App implements IBootstrap { Util::connectHook(Share::class, 'share_link_access', $shareActions, 'shareAccessed'); } - private function authHooks(LoggerInterface $logger): void { + private function authHooks(IAuditLogger $logger): void { $authActions = new Auth($logger); Util::connectHook('OC_User', 'pre_login', $authActions, 'loginAttempt'); @@ -179,7 +168,7 @@ class Application extends App implements IBootstrap { Util::connectHook('OC_User', 'logout', $authActions, 'logout'); } - private function appHooks(LoggerInterface $logger, + private function appHooks(IAuditLogger $logger, EventDispatcherInterface $eventDispatcher): void { $eventDispatcher->addListener(ManagerEvent::EVENT_APP_ENABLE, function (ManagerEvent $event) use ($logger) { $appActions = new AppManagement($logger); @@ -195,7 +184,7 @@ class Application extends App implements IBootstrap { }); } - private function consoleHooks(LoggerInterface $logger, + private function consoleHooks(IAuditLogger $logger, EventDispatcherInterface $eventDispatcher): void { $eventDispatcher->addListener(ConsoleEvent::EVENT_RUN, function (ConsoleEvent $event) use ($logger) { $appActions = new Console($logger); @@ -203,7 +192,7 @@ class Application extends App implements IBootstrap { }); } - private function fileHooks(LoggerInterface $logger, + private function fileHooks(IAuditLogger $logger, EventDispatcherInterface $eventDispatcher): void { $fileActions = new Files($logger); $eventDispatcher->addListener( @@ -265,19 +254,19 @@ class Application extends App implements IBootstrap { ); } - private function versionsHooks(LoggerInterface $logger): void { + private function versionsHooks(IAuditLogger $logger): void { $versionsActions = new Versions($logger); Util::connectHook('\OCP\Versions', 'rollback', $versionsActions, 'rollback'); Util::connectHook('\OCP\Versions', 'delete', $versionsActions, 'delete'); } - private function trashbinHooks(LoggerInterface $logger): void { + private function trashbinHooks(IAuditLogger $logger): void { $trashActions = new Trashbin($logger); Util::connectHook('\OCP\Trashbin', 'preDelete', $trashActions, 'delete'); Util::connectHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', $trashActions, 'restore'); } - private function securityHooks(LoggerInterface $logger, + private function securityHooks(IAuditLogger $logger, EventDispatcherInterface $eventDispatcher): void { $eventDispatcher->addListener(IProvider::EVENT_SUCCESS, function (GenericEvent $event) use ($logger) { $security = new Security($logger); diff --git a/apps/admin_audit/lib/AuditLogger.php b/apps/admin_audit/lib/AuditLogger.php new file mode 100644 index 00000000000..0a7a330a743 --- /dev/null +++ b/apps/admin_audit/lib/AuditLogger.php @@ -0,0 +1,88 @@ +<?php +/** + * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\AdminAudit; + +use OCP\IConfig; +use OCP\Log\ILogFactory; +use Psr\Log\LoggerInterface; + +/** + * Logger that logs in the audit log file instead of the normal log file + */ +class AuditLogger implements IAuditLogger { + + /** @var LoggerInterface */ + private $parentLogger; + + public function __construct(ILogFactory $logFactory, IConfig $config) { + $auditType = $config->getSystemValueString('log_type_audit', 'file'); + $defaultTag = $config->getSystemValueString('syslog_tag', 'Nextcloud'); + $auditTag = $config->getSystemValueString('syslog_tag_audit', $defaultTag); + $logFile = $config->getSystemValueString('logfile_audit', ''); + + if ($auditType === 'file' && !$logFile) { + $default = $config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/audit.log'; + // Legacy way was appconfig, now it's paralleled with the normal log config + $logFile = $config->getAppValue('admin_audit', 'logfile', $default); + } + + $this->parentLogger = $logFactory->getCustomPsrLogger($logFile, $auditType, $auditTag); + } + + public function emergency($message, array $context = array()) { + $this->parentLogger->emergency($message, $context); + } + + public function alert($message, array $context = array()) { + $this->parentLogger->alert($message, $context); + } + + public function critical($message, array $context = array()) { + $this->parentLogger->critical($message, $context); + } + + public function error($message, array $context = array()) { + $this->parentLogger->error($message, $context); + } + + public function warning($message, array $context = array()) { + $this->parentLogger->warning($message, $context); + } + + public function notice($message, array $context = array()) { + $this->parentLogger->notice($message, $context); + } + + public function info($message, array $context = array()) { + $this->parentLogger->info($message, $context); + } + + public function debug($message, array $context = array()) { + $this->parentLogger->debug($message, $context); + } + + public function log($level, $message, array $context = array()) { + $this->parentLogger->log($level, $message, $context); + } +} diff --git a/apps/admin_audit/lib/IAuditLogger.php b/apps/admin_audit/lib/IAuditLogger.php new file mode 100644 index 00000000000..b55d36b942d --- /dev/null +++ b/apps/admin_audit/lib/IAuditLogger.php @@ -0,0 +1,32 @@ +<?php +/** + * @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\AdminAudit; + +use Psr\Log\LoggerInterface; + +/** + * Interface for a logger that logs in the audit log file instead of the normal log file + */ +interface IAuditLogger extends LoggerInterface { +} diff --git a/apps/admin_audit/tests/Actions/SecurityTest.php b/apps/admin_audit/tests/Actions/SecurityTest.php index aa9d9713768..604d2276fb2 100644 --- a/apps/admin_audit/tests/Actions/SecurityTest.php +++ b/apps/admin_audit/tests/Actions/SecurityTest.php @@ -44,7 +44,7 @@ class SecurityTest extends TestCase { protected function setUp(): void { parent::setUp(); - $this->logger = $this->createMock(LoggerInterface::class); + $this->logger = $this->createMock(AuditLogger::class); $this->security = new Security($this->logger); $this->user = $this->createMock(IUser::class); diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 9cfefaed1c8..f4b17814817 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -299,4 +299,7 @@ return array( 'OCA\\DAV\\Upload\\UploadFile' => $baseDir . '/../lib/Upload/UploadFile.php', 'OCA\\DAV\\Upload\\UploadFolder' => $baseDir . '/../lib/Upload/UploadFolder.php', 'OCA\\DAV\\Upload\\UploadHome' => $baseDir . '/../lib/Upload/UploadHome.php', + 'OCA\\DAV\\UserMigration\\CalendarMigrator' => $baseDir . '/../lib/UserMigration/CalendarMigrator.php', + 'OCA\\DAV\\UserMigration\\CalendarMigratorException' => $baseDir . '/../lib/UserMigration/CalendarMigratorException.php', + 'OCA\\DAV\\UserMigration\\InvalidCalendarException' => $baseDir . '/../lib/UserMigration/InvalidCalendarException.php', ); diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 01c24fe38c8..d164ab2b1ce 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -314,6 +314,9 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Upload\\UploadFile' => __DIR__ . '/..' . '/../lib/Upload/UploadFile.php', 'OCA\\DAV\\Upload\\UploadFolder' => __DIR__ . '/..' . '/../lib/Upload/UploadFolder.php', 'OCA\\DAV\\Upload\\UploadHome' => __DIR__ . '/..' . '/../lib/Upload/UploadHome.php', + 'OCA\\DAV\\UserMigration\\CalendarMigrator' => __DIR__ . '/..' . '/../lib/UserMigration/CalendarMigrator.php', + 'OCA\\DAV\\UserMigration\\CalendarMigratorException' => __DIR__ . '/..' . '/../lib/UserMigration/CalendarMigratorException.php', + 'OCA\\DAV\\UserMigration\\InvalidCalendarException' => __DIR__ . '/..' . '/../lib/UserMigration/InvalidCalendarException.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/apps/dav/l10n/fr.js b/apps/dav/l10n/fr.js index e82d25bdd4c..b626481677f 100644 --- a/apps/dav/l10n/fr.js +++ b/apps/dav/l10n/fr.js @@ -108,6 +108,19 @@ OC.L10N.register( "{actor} updated contact {card} in address book {addressbook}" : "{actor} a mis à jour le contact {card} dans le carnet d'adresses {addressbook}", "You updated contact {card} in address book {addressbook}" : "Vous avez mis à jour le contact {card} dans le carnet d'adresses {addressbook}", "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Un <strong>contact</strong> ou <strong>carnet d'adresses</strong> a été modifié", + "File is not updatable: %1$s" : "Ce fichier ne peut pas être mis à jour : %1$s", + "Could not write file contents" : "Impossible d'écrire le contenu du fichier", + "_%n byte_::_%n bytes_" : ["%n octet","%n octets"], + "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erreur en copiant le fichier à destination (copié : %1$s, taille du fichier attendue : %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Taille du fichier attendue : %1$s mais taille du fichier lue (depuis le client Nextcloud) et écrit (dans le stockage Nextcloud) : %2$s. Cela peut être un problème de réseau au niveau du client ou un problème de stockage au niveau du serveur.", + "Could not rename part file to final file" : "Impossible de renommer le fichier partiel en fichier définitif.", + "Failed to check file size: %1$s" : "Échec à la vérification de la taille du fichier : %1$s", + "Could not open file" : "Impossible d'ouvrir le fichier", + "Encryption not ready: %1$s" : "Encryption pas prête : %1$s", + "Failed to open file: %1$s" : "Échec à l'ouverture du fichier : %1$s", + "Failed to unlink: %1$s" : "Échec à la suppression :%1$s", + "Failed to write file contents: %1$s" : "Échec à l'écriture du contenu du fichier : %1$s", + "File not found: %1$s" : "Fichier non trouvé : %1$s", "System is in maintenance mode." : "Le système est en mode maintenance.", "Upgrade needed" : "Mise à jour requise", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Votre %s a besoin d'être configuré pour utiliser le HTTPS dans le but d'utiliser CalDAV et CardDAV avec iOS/macOS.", diff --git a/apps/dav/l10n/fr.json b/apps/dav/l10n/fr.json index d82ec7c70ee..4d4cc4d950a 100644 --- a/apps/dav/l10n/fr.json +++ b/apps/dav/l10n/fr.json @@ -106,6 +106,19 @@ "{actor} updated contact {card} in address book {addressbook}" : "{actor} a mis à jour le contact {card} dans le carnet d'adresses {addressbook}", "You updated contact {card} in address book {addressbook}" : "Vous avez mis à jour le contact {card} dans le carnet d'adresses {addressbook}", "A <strong>contact</strong> or <strong>address book</strong> was modified" : "Un <strong>contact</strong> ou <strong>carnet d'adresses</strong> a été modifié", + "File is not updatable: %1$s" : "Ce fichier ne peut pas être mis à jour : %1$s", + "Could not write file contents" : "Impossible d'écrire le contenu du fichier", + "_%n byte_::_%n bytes_" : ["%n octet","%n octets"], + "Error while copying file to target location (copied: %1$s, expected filesize: %2$s)" : "Erreur en copiant le fichier à destination (copié : %1$s, taille du fichier attendue : %2$s)", + "Expected filesize of %1$s but read (from Nextcloud client) and wrote (to Nextcloud storage) %2$s. Could either be a network problem on the sending side or a problem writing to the storage on the server side." : "Taille du fichier attendue : %1$s mais taille du fichier lue (depuis le client Nextcloud) et écrit (dans le stockage Nextcloud) : %2$s. Cela peut être un problème de réseau au niveau du client ou un problème de stockage au niveau du serveur.", + "Could not rename part file to final file" : "Impossible de renommer le fichier partiel en fichier définitif.", + "Failed to check file size: %1$s" : "Échec à la vérification de la taille du fichier : %1$s", + "Could not open file" : "Impossible d'ouvrir le fichier", + "Encryption not ready: %1$s" : "Encryption pas prête : %1$s", + "Failed to open file: %1$s" : "Échec à l'ouverture du fichier : %1$s", + "Failed to unlink: %1$s" : "Échec à la suppression :%1$s", + "Failed to write file contents: %1$s" : "Échec à l'écriture du contenu du fichier : %1$s", + "File not found: %1$s" : "Fichier non trouvé : %1$s", "System is in maintenance mode." : "Le système est en mode maintenance.", "Upgrade needed" : "Mise à jour requise", "Your %s needs to be configured to use HTTPS in order to use CalDAV and CardDAV with iOS/macOS." : "Votre %s a besoin d'être configuré pour utiliser le HTTPS dans le but d'utiliser CalDAV et CardDAV avec iOS/macOS.", diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 99521b61e8b..f29161d6976 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -80,6 +80,7 @@ use OCA\DAV\Listener\CardListener; use OCA\DAV\Search\ContactsSearchProvider; use OCA\DAV\Search\EventsSearchProvider; use OCA\DAV\Search\TasksSearchProvider; +use OCA\DAV\UserMigration\CalendarMigrator; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -165,6 +166,8 @@ class Application extends App implements IBootstrap { $context->registerNotifierService(Notifier::class); $context->registerCalendarProvider(CalendarProvider::class); + + $context->registerUserMigrator(CalendarMigrator::class); } public function boot(IBootContext $context): void { diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php index 1c36db68cca..406389e3a3d 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -70,6 +70,13 @@ class CalendarImpl implements ICreateFromString { } /** + * {@inheritDoc} + */ + public function getUri(): string { + return $this->calendarInfo['uri']; + } + + /** * In comparison to getKey() this function returns a human readable (maybe translated) name * @return null|string * @since 13.0.0 diff --git a/apps/dav/lib/UserMigration/CalendarMigrator.php b/apps/dav/lib/UserMigration/CalendarMigrator.php new file mode 100644 index 00000000000..d0d9e94351b --- /dev/null +++ b/apps/dav/lib/UserMigration/CalendarMigrator.php @@ -0,0 +1,460 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Christopher Ng <chrng8@gmail.com> + * + * @author Christopher Ng <chrng8@gmail.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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\UserMigration; + +use function Safe\substr; +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin; +use OCA\DAV\CalDAV\Plugin as CalDAVPlugin; +use OCA\DAV\Connector\Sabre\CachingTree; +use OCA\DAV\Connector\Sabre\Server as SabreDavServer; +use OCA\DAV\RootCollection; +use OCP\Calendar\ICalendar; +use OCP\Calendar\IManager as ICalendarManager; +use OCP\Defaults; +use OCP\IL10N; +use OCP\IUser; +use OCP\UserMigration\IExportDestination; +use OCP\UserMigration\IImportSource; +use OCP\UserMigration\IMigrator; +use OCP\UserMigration\TMigratorBasicVersionHandling; +use Sabre\VObject\Component as VObjectComponent; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VTimeZone; +use Sabre\VObject\Property\ICalendar\DateTime; +use Sabre\VObject\Reader as VObjectReader; +use Sabre\VObject\UUIDUtil; +use Safe\Exceptions\StringsException; +use Symfony\Component\Console\Output\OutputInterface; +use Throwable; + +class CalendarMigrator implements IMigrator { + + use TMigratorBasicVersionHandling; + + private CalDavBackend $calDavBackend; + + private ICalendarManager $calendarManager; + + // ICSExportPlugin is injected as the mergeObjects() method is required and is not to be used as a SabreDAV server plugin + private ICSExportPlugin $icsExportPlugin; + + private Defaults $defaults; + + private IL10N $l10n; + + private SabreDavServer $sabreDavServer; + + private const USERS_URI_ROOT = 'principals/users/'; + + private const FILENAME_EXT = '.ics'; + + private const MIGRATED_URI_PREFIX = 'migrated-'; + + private const EXPORT_ROOT = Application::APP_ID . '/calendars/'; + + public function __construct( + CalDavBackend $calDavBackend, + ICalendarManager $calendarManager, + ICSExportPlugin $icsExportPlugin, + Defaults $defaults, + IL10N $l10n + ) { + $this->calDavBackend = $calDavBackend; + $this->calendarManager = $calendarManager; + $this->icsExportPlugin = $icsExportPlugin; + $this->defaults = $defaults; + $this->l10n = $l10n; + + $root = new RootCollection(); + $this->sabreDavServer = new SabreDavServer(new CachingTree($root)); + $this->sabreDavServer->addPlugin(new CalDAVPlugin()); + } + + private function getPrincipalUri(IUser $user): string { + return CalendarMigrator::USERS_URI_ROOT . $user->getUID(); + } + + /** + * @return array{name: string, vCalendar: VCalendar} + * + * @throws CalendarMigratorException + * @throws InvalidCalendarException + */ + private function getCalendarExportData(IUser $user, ICalendar $calendar, OutputInterface $output): array { + $userId = $user->getUID(); + $calendarId = $calendar->getKey(); + $calendarInfo = $this->calDavBackend->getCalendarById($calendarId); + + if (empty($calendarInfo)) { + throw new CalendarMigratorException("Invalid info for calendar ID $calendarId"); + } + + $uri = $calendarInfo['uri']; + $path = CalDAVPlugin::CALENDAR_ROOT . "/$userId/$uri"; + + /** + * @see \Sabre\CalDAV\ICSExportPlugin::httpGet() implementation reference + */ + + $properties = $this->sabreDavServer->getProperties($path, [ + '{DAV:}resourcetype', + '{DAV:}displayname', + '{http://sabredav.org/ns}sync-token', + '{DAV:}sync-token', + '{http://apple.com/ns/ical/}calendar-color', + ]); + + // Filter out invalid (e.g. deleted) calendars + if (!isset($properties['{DAV:}resourcetype']) || !$properties['{DAV:}resourcetype']->is('{' . CalDAVPlugin::NS_CALDAV . '}calendar')) { + throw new InvalidCalendarException(); + } + + /** + * @see \Sabre\CalDAV\ICSExportPlugin::generateResponse() implementation reference + */ + + $calDataProp = '{' . CalDAVPlugin::NS_CALDAV . '}calendar-data'; + $calendarNode = $this->sabreDavServer->tree->getNodeForPath($path); + $nodes = $this->sabreDavServer->getPropertiesIteratorForPath($path, [$calDataProp], 1); + + $blobs = []; + foreach ($nodes as $node) { + if (isset($node[200][$calDataProp])) { + $blobs[$node['href']] = $node[200][$calDataProp]; + } + } + + $mergedCalendar = $this->icsExportPlugin->mergeObjects( + $properties, + $blobs, + ); + + $problems = $mergedCalendar->validate(); + if (!empty($problems)) { + $output->writeln('Skipping calendar "' . $properties['{DAV:}displayname'] . '" containing invalid calendar data'); + throw new InvalidCalendarException(); + } + + return [ + 'name' => $calendarNode->getName(), + 'vCalendar' => $mergedCalendar, + ]; + } + + /** + * @return array<int, array{name: string, vCalendar: VCalendar}> + * + * @throws CalendarMigratorException + */ + private function getCalendarExports(IUser $user, OutputInterface $output): array { + $principalUri = $this->getPrincipalUri($user); + + return array_values(array_filter(array_map( + function (ICalendar $calendar) use ($user, $output) { + try { + return $this->getCalendarExportData($user, $calendar, $output); + } catch (InvalidCalendarException $e) { + // Allow this exception as invalid (e.g. deleted) calendars are not to be exported + return null; + } + }, + $this->calendarManager->getCalendarsForPrincipal($principalUri), + ))); + } + + private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): string { + $principalUri = $this->getPrincipalUri($user); + try { + $initialCalendarUri = substr($initialCalendarUri, 0, strlen(CalendarMigrator::MIGRATED_URI_PREFIX)) === CalendarMigrator::MIGRATED_URI_PREFIX + ? $initialCalendarUri + : CalendarMigrator::MIGRATED_URI_PREFIX . $initialCalendarUri; + } catch (StringsException $e) { + throw new CalendarMigratorException('Failed to get unique calendar URI', 0, $e); + } + + $existingCalendarUris = array_map( + fn (ICalendar $calendar) => $calendar->getUri(), + $this->calendarManager->getCalendarsForPrincipal($principalUri), + ); + + $calendarUri = $initialCalendarUri; + $acc = 1; + while (in_array($calendarUri, $existingCalendarUris, true)) { + $calendarUri = $initialCalendarUri . "-$acc"; + ++$acc; + } + + return $calendarUri; + } + + /** + * {@inheritDoc} + */ + public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void { + $output->writeln('Exporting calendars into ' . CalendarMigrator::EXPORT_ROOT . '…'); + + $calendarExports = $this->getCalendarExports($user, $output); + + if (empty($calendarExports)) { + $output->writeln('No calendars to export…'); + } + + /** + * @var string $name + * @var VCalendar $vCalendar + */ + foreach ($calendarExports as ['name' => $name, 'vCalendar' => $vCalendar]) { + // Set filename to sanitized calendar name appended with the date + $filename = preg_replace('/[^a-zA-Z0-9-_ ]/um', '', $name) . '_' . date('Y-m-d') . CalendarMigrator::FILENAME_EXT; + $exportPath = CalendarMigrator::EXPORT_ROOT . $filename; + + if ($exportDestination->addFileContents($exportPath, $vCalendar->serialize()) === false) { + throw new CalendarMigratorException('Could not export calendars'); + } + } + } + + /** + * @return array<string, VTimeZone> + */ + private function getCalendarTimezones(VCalendar $vCalendar): array { + /** @var VTimeZone[] $calendarTimezones */ + $calendarTimezones = array_filter( + $vCalendar->getComponents(), + fn ($component) => $component->name === 'VTIMEZONE', + ); + + /** @var array<string, VTimeZone> $calendarTimezoneMap */ + $calendarTimezoneMap = []; + foreach ($calendarTimezones as $vTimeZone) { + $calendarTimezoneMap[$vTimeZone->getTimeZone()->getName()] = $vTimeZone; + } + + return $calendarTimezoneMap; + } + + /** + * @return VTimeZone[] + */ + private function getTimezonesForComponent(VCalendar $vCalendar, VObjectComponent $component): array { + $componentTimezoneIds = []; + + foreach ($component->children() as $child) { + if ($child instanceof DateTime && isset($child->parameters['TZID'])) { + $timezoneId = $child->parameters['TZID']->getValue(); + if (!in_array($timezoneId, $componentTimezoneIds, true)) { + $componentTimezoneIds[] = $timezoneId; + } + } + } + + $calendarTimezoneMap = $this->getCalendarTimezones($vCalendar); + + return array_values(array_filter(array_map( + fn (string $timezoneId) => $calendarTimezoneMap[$timezoneId], + $componentTimezoneIds, + ))); + } + + private function sanitizeComponent(VObjectComponent $component): VObjectComponent { + // Operate on the component clone to prevent mutation of the original + $component = clone $component; + + // Remove RSVP parameters to prevent automatically sending invitation emails to attendees on import + foreach ($component->children() as $child) { + if ( + $child->name === 'ATTENDEE' + && isset($child->parameters['RSVP']) + ) { + unset($child->parameters['RSVP']); + } + } + + return $component; + } + + /** + * @return VObjectComponent[] + */ + private function getRequiredImportComponents(VCalendar $vCalendar, VObjectComponent $component): array { + $component = $this->sanitizeComponent($component); + /** @var array<int, VTimeZone> $timezoneComponents */ + $timezoneComponents = $this->getTimezonesForComponent($vCalendar, $component); + return [ + ...$timezoneComponents, + $component, + ]; + } + + private function initCalendarObject(): VCalendar { + $vCalendarObject = new VCalendar(); + $vCalendarObject->PRODID = '-//IDN nextcloud.com//Migrated calendar//EN'; + return $vCalendarObject; + } + + private function importCalendarObject(int $calendarId, VCalendar $vCalendarObject, OutputInterface $output): void { + try { + $this->calDavBackend->createCalendarObject( + $calendarId, + UUIDUtil::getUUID() . CalendarMigrator::FILENAME_EXT, + $vCalendarObject->serialize(), + CalDavBackend::CALENDAR_TYPE_CALENDAR, + ); + } catch (Throwable $e) { + // Rollback creation of calendar on error + $output->writeln('Error creating calendar object, rolling back creation of calendar…'); + $this->calDavBackend->deleteCalendar($calendarId, true); + } + } + + /** + * @throws CalendarMigratorException + */ + private function importCalendar(IUser $user, string $filename, string $initialCalendarUri, VCalendar $vCalendar, OutputInterface $output): void { + $principalUri = $this->getPrincipalUri($user); + $calendarUri = $this->getUniqueCalendarUri($user, $initialCalendarUri); + + $calendarId = $this->calDavBackend->createCalendar($principalUri, $calendarUri, [ + '{DAV:}displayname' => isset($vCalendar->{'X-WR-CALNAME'}) ? $vCalendar->{'X-WR-CALNAME'}->getValue() : $this->l10n->t('Migrated calendar (%1$s)', [$filename]), + '{http://apple.com/ns/ical/}calendar-color' => isset($vCalendar->{'X-APPLE-CALENDAR-COLOR'}) ? $vCalendar->{'X-APPLE-CALENDAR-COLOR'}->getValue() : $this->defaults->getColorPrimary(), + 'components' => implode( + ',', + array_reduce( + $vCalendar->getComponents(), + function (array $componentNames, VObjectComponent $component) { + /** @var array<int, string> $componentNames */ + return !in_array($component->name, $componentNames, true) + ? [...$componentNames, $component->name] + : $componentNames; + }, + [], + ) + ), + ]); + + /** @var VObjectComponent[] $calendarComponents */ + $calendarComponents = array_values(array_filter( + $vCalendar->getComponents(), + // VTIMEZONE components are handled separately and added to the calendar object only if depended on by the component + fn (VObjectComponent $component) => $component->name !== 'VTIMEZONE', + )); + + /** @var array<string, VObjectComponent[]> $groupedCalendarComponents */ + $groupedCalendarComponents = []; + /** @var VObjectComponent[] $ungroupedCalendarComponents */ + $ungroupedCalendarComponents = []; + + foreach ($calendarComponents as $component) { + if (isset($component->UID)) { + $uid = $component->UID->getValue(); + // Components with the same UID (e.g. recurring events) are grouped together into a single calendar object + if (isset($groupedCalendarComponents[$uid])) { + $groupedCalendarComponents[$uid][] = $component; + } else { + $groupedCalendarComponents[$uid] = [$component]; + } + } else { + $ungroupedCalendarComponents[] = $component; + } + } + + foreach ($groupedCalendarComponents as $uid => $components) { + // Construct and import a calendar object containing all components of a group + $vCalendarObject = $this->initCalendarObject(); + foreach ($components as $component) { + foreach ($this->getRequiredImportComponents($vCalendar, $component) as $component) { + $vCalendarObject->add($component); + } + } + $this->importCalendarObject($calendarId, $vCalendarObject, $output); + } + + foreach ($ungroupedCalendarComponents as $component) { + // Construct and import a calendar object for a single component + $vCalendarObject = $this->initCalendarObject(); + foreach ($this->getRequiredImportComponents($vCalendar, $component) as $component) { + $vCalendarObject->add($component); + } + $this->importCalendarObject($calendarId, $vCalendarObject, $output); + } + } + + /** + * {@inheritDoc} + * + * @throws CalendarMigratorException + */ + public function import(IUser $user, IImportSource $importSource, OutputInterface $output): void { + if ($importSource->getMigratorVersion(static::class) === null) { + $output->writeln('No version for ' . static::class . ', skipping import…'); + return; + } + + $output->writeln('Importing calendars from ' . CalendarMigrator::EXPORT_ROOT . '…'); + + $calendarImports = $importSource->getFolderListing(CalendarMigrator::EXPORT_ROOT); + if (empty($calendarImports)) { + $output->writeln('No calendars to import…'); + } + + foreach ($calendarImports as $filename) { + $importPath = CalendarMigrator::EXPORT_ROOT . $filename; + try { + /** @var VCalendar $vCalendar */ + $vCalendar = VObjectReader::read( + $importSource->getFileAsStream($importPath), + VObjectReader::OPTION_FORGIVING, + ); + } catch (Throwable $e) { + throw new CalendarMigratorException("Failed to read file \"$importPath\"", 0, $e); + } + + $problems = $vCalendar->validate(); + if (!empty($problems)) { + throw new CalendarMigratorException("Invalid calendar data contained in \"$importPath\""); + } + + $splitFilename = explode('_', $filename, 2); + if (count($splitFilename) !== 2) { + throw new CalendarMigratorException("Invalid filename \"$filename\", expected filename of the format \"<calendar_name>_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . '"'); + } + [$initialCalendarUri, $suffix] = $splitFilename; + + $this->importCalendar( + $user, + $filename, + $initialCalendarUri, + $vCalendar, + $output, + ); + + $vCalendar->destroy(); + } + } +} diff --git a/apps/dav/lib/UserMigration/CalendarMigratorException.php b/apps/dav/lib/UserMigration/CalendarMigratorException.php new file mode 100644 index 00000000000..91bac58ffac --- /dev/null +++ b/apps/dav/lib/UserMigration/CalendarMigratorException.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Christopher Ng <chrng8@gmail.com> + * + * @author Christopher Ng <chrng8@gmail.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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\UserMigration; + +use Exception; + +class CalendarMigratorException extends Exception { +} diff --git a/apps/dav/lib/UserMigration/InvalidCalendarException.php b/apps/dav/lib/UserMigration/InvalidCalendarException.php new file mode 100644 index 00000000000..0e42ef1bc20 --- /dev/null +++ b/apps/dav/lib/UserMigration/InvalidCalendarException.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Christopher Ng <chrng8@gmail.com> + * + * @author Christopher Ng <chrng8@gmail.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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\UserMigration; + +use Exception; + +class InvalidCalendarException extends Exception { +} diff --git a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php new file mode 100644 index 00000000000..f1ad6dda22e --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php @@ -0,0 +1,137 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Christopher Ng <chrng8@gmail.com> + * + * @author Christopher Ng <chrng8@gmail.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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\DAV\Tests\integration\UserMigration; + +use function Safe\scandir; +use OCA\DAV\AppInfo\Application; +use OCA\DAV\UserMigration\CalendarMigrator; +use OCP\AppFramework\App; +use OCP\IUserManager; +use Sabre\VObject\Component as VObjectComponent; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Property as VObjectProperty; +use Sabre\VObject\Reader as VObjectReader; +use Sabre\VObject\UUIDUtil; +use Symfony\Component\Console\Output\OutputInterface; +use Test\TestCase; + +/** + * @group DB + */ +class CalendarMigratorTest extends TestCase { + + private IUserManager $userManager; + + private CalendarMigrator $migrator; + + private OutputInterface $output; + + private const ASSETS_DIR = __DIR__ . '/assets/'; + + protected function setUp(): void { + $app = new App(Application::APP_ID); + $container = $app->getContainer(); + + $this->userManager = $container->get(IUserManager::class); + $this->migrator = $container->get(CalendarMigrator::class); + $this->output = $this->createMock(OutputInterface::class); + } + + public function dataAssets(): array { + return array_map( + function (string $filename) { + /** @var VCalendar $vCalendar */ + $vCalendar = VObjectReader::read( + fopen(self::ASSETS_DIR . $filename, 'r'), + VObjectReader::OPTION_FORGIVING, + ); + [$initialCalendarUri, $ext] = explode('.', $filename, 2); + return [UUIDUtil::getUUID(), $filename, $initialCalendarUri, $vCalendar]; + }, + array_diff( + scandir(self::ASSETS_DIR), + // Exclude current and parent directories + ['.', '..'], + ), + ); + } + + private function getProperties(VCalendar $vCalendar): array { + return array_map( + fn (VObjectProperty $property) => $property->serialize(), + array_values(array_filter( + $vCalendar->children(), + fn (mixed $child) => $child instanceof VObjectProperty, + )), + ); + } + + private function getComponents(VCalendar $vCalendar): array { + return array_map( + // Elements of the serialized blob are sorted + fn (VObjectComponent $component) => $component->serialize(), + $vCalendar->getComponents(), + ); + } + + private function getSanitizedComponents(VCalendar $vCalendar): array { + return array_map( + // Elements of the serialized blob are sorted + fn (VObjectComponent $component) => $this->invokePrivate($this->migrator, 'sanitizeComponent', [$component])->serialize(), + $vCalendar->getComponents(), + ); + } + + /** + * @dataProvider dataAssets + */ + public function testImportExportAsset(string $userId, string $filename, string $initialCalendarUri, VCalendar $importCalendar): void { + $user = $this->userManager->createUser($userId, 'topsecretpassword'); + + $problems = $importCalendar->validate(); + $this->assertEmpty($problems); + + $this->invokePrivate($this->migrator, 'importCalendar', [$user, $filename, $initialCalendarUri, $importCalendar, $this->output]); + + $calendarExports = $this->invokePrivate($this->migrator, 'getCalendarExports', [$user, $this->output]); + $this->assertCount(1, $calendarExports); + + /** @var VCalendar $exportCalendar */ + ['vCalendar' => $exportCalendar] = reset($calendarExports); + + $this->assertEqualsCanonicalizing( + $this->getProperties($importCalendar), + $this->getProperties($exportCalendar), + ); + + $this->assertEqualsCanonicalizing( + // Components are expected to be sanitized on import + $this->getSanitizedComponents($importCalendar), + $this->getComponents($exportCalendar), + ); + } +} diff --git a/apps/dav/tests/integration/UserMigration/assets/event-alarms.ics b/apps/dav/tests/integration/UserMigration/assets/event-alarms.ics new file mode 100644 index 00000000000..5dc6acbb605 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-alarms.ics @@ -0,0 +1,42 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:Alarms +X-APPLE-CALENDAR-COLOR:#0082c9 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:P1DT9H +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;VALUE=DATE-TIME:20200306T083000Z +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/event-attendees.ics b/apps/dav/tests/integration/UserMigration/assets/event-attendees.ics new file mode 100644 index 00000000000..4100365926c --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-attendees.ics @@ -0,0 +1,39 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:Attendees +X-APPLE-CALENDAR-COLOR:#0082c9 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +ORGANIZER;CN=John Smith:mailto:jsmith@example.com +ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:jsmith@example.com +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Henry Cabot:mailto:hcabot@example.com +ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="mailto:bob@example.com";PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:jdoe@example.com +ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss@example.com +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/event-categories.ics b/apps/dav/tests/integration/UserMigration/assets/event-categories.ics new file mode 100644 index 00000000000..7e7054d0251 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-categories.ics @@ -0,0 +1,35 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:Categories +X-APPLE-CALENDAR-COLOR:#0082c9 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +CATEGORIES:BUSINESS,HUMAN RESOURCES +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/event-complex-alarm-recurring.ics b/apps/dav/tests/integration/UserMigration/assets/event-complex-alarm-recurring.ics new file mode 100644 index 00000000000..8aa8e085c63 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-complex-alarm-recurring.ics @@ -0,0 +1,33 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//SabreDAV//SabreDAV//EN +X-WR-CALNAME:Complex alarm recurring +X-APPLE-CALENDAR-COLOR:#0082c9 +BEGIN:VEVENT +CREATED:20220218T031205Z +DTSTAMP:20220218T031409Z +LAST-MODIFIED:20220218T031409Z +SEQUENCE:2 +UID:b78f3a65-413d-4fa7-b125-1232bc6a2c72 +DTSTART;VALUE=DATE:20220217 +DTEND;VALUE=DATE:20220218 +STATUS:TENTATIVE +SUMMARY:Complex recurring event +LOCATION:Antarctica +DESCRIPTION:Event description +CLASS:CONFIDENTIAL +TRANSP:TRANSPARENT +CATEGORIES:Personal,Travel,Special occasion +COLOR:khaki +RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=WE;BYSETPOS=2 +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:-P6DT15H +END:VALARM +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER;RELATED=START:-PT15H +END:VALARM +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/event-complex-recurrence.ics b/apps/dav/tests/integration/UserMigration/assets/event-complex-recurrence.ics new file mode 100644 index 00000000000..45fb5540af6 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-complex-recurrence.ics @@ -0,0 +1,87 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:Complex recurrence +X-APPLE-CALENDAR-COLOR:#0082c9 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST +RRULE:FREQ=WEEKLY +DTSTART;TZID=Europe/Berlin:20200301T150000 +DTEND;TZID=Europe/Berlin:20200301T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 1 +RECURRENCE-ID;TZID=Europe/Berlin:20200308T150000 +DTSTART;TZID=Europe/Berlin:20200401T150000 +DTEND;TZID=Europe/Berlin:20200401T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 2 +RECURRENCE-ID;TZID=Europe/Berlin:20200315T150000 +DTSTART;TZID=Europe/Berlin:20201101T150000 +DTEND;TZID=Europe/Berlin:20201101T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 3 +RECURRENCE-ID;TZID=Europe/Berlin:20200405T150000 +DTSTART;TZID=Europe/Berlin:20200406T150000 +DTEND;TZID=Europe/Berlin:20200406T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 4 +RECURRENCE-ID;TZID=Europe/Berlin:20200412T150000 +DTSTART;TZID=Europe/Berlin:20201201T150000 +DTEND;TZID=Europe/Berlin:20201201T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 5 +RECURRENCE-ID;TZID=Europe/Berlin:20200426T150000 +DTSTART;TZID=Europe/Berlin:20200410T150000 +DTEND;TZID=Europe/Berlin:20200410T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:INVALID RECURRENCE-ID +RECURRENCE-ID;TZID=Europe/Berlin:20200427T150000 +DTSTART;TZID=Europe/Berlin:20200420T150000 +DTEND;TZID=Europe/Berlin:20200420T160000 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/event-custom-color.ics b/apps/dav/tests/integration/UserMigration/assets/event-custom-color.ics new file mode 100644 index 00000000000..012ec9fbc17 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-custom-color.ics @@ -0,0 +1,34 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +X-WR-CALNAME:Personal +X-APPLE-CALENDAR-COLOR:#f264ab +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/event-multiple-recurring.ics b/apps/dav/tests/integration/UserMigration/assets/event-multiple-recurring.ics new file mode 100644 index 00000000000..a9e748b5252 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-multiple-recurring.ics @@ -0,0 +1,74 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//SabreDAV//SabreDAV//EN +X-WR-CALNAME:Multiple and recurring +X-APPLE-CALENDAR-COLOR:#795AAB +BEGIN:VEVENT +CREATED:20220218T044833Z +DTSTAMP:20220218T044837Z +LAST-MODIFIED:20220218T044837Z +SEQUENCE:2 +UID:dc343863-b57c-43a5-9ba4-19ae2740cd7e +DTSTART;VALUE=DATE:20220607 +DTEND;VALUE=DATE:20220608 +STATUS:CONFIRMED +SUMMARY:Event 4 +END:VEVENT +BEGIN:VEVENT +CREATED:20220218T044806Z +DTSTAMP:20220218T044809Z +LAST-MODIFIED:20220218T044809Z +SEQUENCE:2 +UID:ae28b642-7e11-4e16-818a-06c89ae74f44 +DTSTART;VALUE=DATE:20220218 +DTEND;VALUE=DATE:20220219 +STATUS:CONFIRMED +SUMMARY:Event 1 +END:VEVENT +BEGIN:VEVENT +CREATED:20220218T044820Z +DTSTAMP:20220218T044827Z +LAST-MODIFIED:20220218T044827Z +SEQUENCE:2 +UID:5edfb90e-44b3-47c6-863b-f632327f46f0 +DTSTART;VALUE=DATE:20220518 +DTEND;VALUE=DATE:20220519 +STATUS:CONFIRMED +SUMMARY:Event 3 +END:VEVENT +BEGIN:VEVENT +CREATED:20220218T044810Z +DTSTAMP:20220218T044814Z +LAST-MODIFIED:20220218T044814Z +SEQUENCE:2 +UID:9789f684-1cf9-4ee7-90cb-54cdec6a6f03 +DTSTART;VALUE=DATE:20220223 +DTEND;VALUE=DATE:20220224 +STATUS:CONFIRMED +SUMMARY:Event 2 +END:VEVENT +BEGIN:VEVENT +CREATED:20220218T044932Z +DTSTAMP:20220218T044945Z +LAST-MODIFIED:20220218T044945Z +SEQUENCE:2 +UID:d5bdaf0e-d6c7-4e30-a730-04928976a1d2 +DTSTART;VALUE=DATE:20221102 +DTEND;VALUE=DATE:20221103 +STATUS:CONFIRMED +SUMMARY:Recurring event +RRULE:FREQ=WEEKLY;BYDAY=WE +END:VEVENT +BEGIN:VEVENT +CREATED:20220218T044915Z +DTSTAMP:20220218T044918Z +LAST-MODIFIED:20220218T044918Z +SEQUENCE:2 +UID:11c3d9fd-fb54-4384-ab68-40b5d6acef6f +DTSTART;VALUE=DATE:20221010 +DTEND;VALUE=DATE:20221011 +STATUS:CONFIRMED +SUMMARY:Event 5 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/event-multiple.ics b/apps/dav/tests/integration/UserMigration/assets/event-multiple.ics new file mode 100644 index 00000000000..ba0f5457052 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-multiple.ics @@ -0,0 +1,62 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//SabreDAV//SabreDAV//EN +X-WR-CALNAME:Multiple +X-APPLE-CALENDAR-COLOR:#795AAB +BEGIN:VEVENT +CREATED:20220218T044833Z +DTSTAMP:20220218T044837Z +LAST-MODIFIED:20220218T044837Z +SEQUENCE:2 +UID:dc343863-b57c-43a5-9ba4-19ae2740cd7e +DTSTART;VALUE=DATE:20220607 +DTEND;VALUE=DATE:20220608 +STATUS:CONFIRMED +SUMMARY:Event 4 +END:VEVENT +BEGIN:VEVENT +CREATED:20220218T044806Z +DTSTAMP:20220218T044809Z +LAST-MODIFIED:20220218T044809Z +SEQUENCE:2 +UID:ae28b642-7e11-4e16-818a-06c89ae74f44 +DTSTART;VALUE=DATE:20220218 +DTEND;VALUE=DATE:20220219 +STATUS:CONFIRMED +SUMMARY:Event 1 +END:VEVENT +BEGIN:VEVENT +CREATED:20220218T044820Z +DTSTAMP:20220218T044827Z +LAST-MODIFIED:20220218T044827Z +SEQUENCE:2 +UID:5edfb90e-44b3-47c6-863b-f632327f46f0 +DTSTART;VALUE=DATE:20220518 +DTEND;VALUE=DATE:20220519 +STATUS:CONFIRMED +SUMMARY:Event 3 +END:VEVENT +BEGIN:VEVENT +CREATED:20220218T044810Z +DTSTAMP:20220218T044814Z +LAST-MODIFIED:20220218T044814Z +SEQUENCE:2 +UID:9789f684-1cf9-4ee7-90cb-54cdec6a6f03 +DTSTART;VALUE=DATE:20220223 +DTEND;VALUE=DATE:20220224 +STATUS:CONFIRMED +SUMMARY:Event 2 +END:VEVENT +BEGIN:VEVENT +CREATED:20220218T044915Z +DTSTAMP:20220218T044918Z +LAST-MODIFIED:20220218T044918Z +SEQUENCE:2 +UID:11c3d9fd-fb54-4384-ab68-40b5d6acef6f +DTSTART;VALUE=DATE:20221010 +DTEND;VALUE=DATE:20221011 +STATUS:CONFIRMED +SUMMARY:Event 5 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/event-recurring.ics b/apps/dav/tests/integration/UserMigration/assets/event-recurring.ics new file mode 100644 index 00000000000..d59627b4a6c --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-recurring.ics @@ -0,0 +1,87 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:Recurring +X-APPLE-CALENDAR-COLOR:#0082c9 +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST +RRULE:FREQ=WEEKLY +DTSTART;TZID=Europe/Berlin:20200301T150000 +DTEND;TZID=Europe/Berlin:20200301T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 1 +RECURRENCE-ID;TZID=Europe/Berlin:20200308T150000 +DTSTART;TZID=Europe/Berlin:20200401T150000 +DTEND;TZID=Europe/Berlin:20200401T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 2 +RECURRENCE-ID;TZID=Europe/Berlin:20200315T150000 +DTSTART;TZID=Europe/Berlin:20201101T150000 +DTEND;TZID=Europe/Berlin:20201101T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 3 +RECURRENCE-ID;TZID=Europe/Berlin:20200405T150000 +DTSTART;TZID=Europe/Berlin:20200406T150000 +DTEND;TZID=Europe/Berlin:20200406T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 4 +RECURRENCE-ID;TZID=Europe/Berlin:20200412T150000 +DTSTART;TZID=Europe/Berlin:20201201T150000 +DTEND;TZID=Europe/Berlin:20201201T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:TEST EX 5 +RECURRENCE-ID;TZID=Europe/Berlin:20200426T150000 +DTSTART;TZID=Europe/Berlin:20200410T150000 +DTEND;TZID=Europe/Berlin:20200410T160000 +END:VEVENT +BEGIN:VEVENT +CREATED:20160809T163629Z +DTSTAMP:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +SUMMARY:INVALID RECURRENCE-ID +RECURRENCE-ID;TZID=Europe/Berlin:20200427T150000 +DTSTART;TZID=Europe/Berlin:20200420T150000 +DTEND;TZID=Europe/Berlin:20200420T160000 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/event-timed.ics b/apps/dav/tests/integration/UserMigration/assets/event-timed.ics new file mode 100644 index 00000000000..980a290a4f7 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/event-timed.ics @@ -0,0 +1,34 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +X-WR-CALNAME:Timed +X-APPLE-CALENDAR-COLOR:#0082c9 +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +DTSTART:19810329T020000 +TZNAME:GMT+2 +TZOFFSETTO:+0200 +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +DTSTART:19961027T030000 +TZNAME:GMT+1 +TZOFFSETTO:+0100 +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND;TZID=Europe/Berlin:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Europe Berlin +DTSTART;TZID=Europe/Berlin:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/journal-todo-event.ics b/apps/dav/tests/integration/UserMigration/assets/journal-todo-event.ics new file mode 100644 index 00000000000..6a74aface84 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/journal-todo-event.ics @@ -0,0 +1,41 @@ +BEGIN:VCALENDAR +VERSION:2.0 +CALSCALE:GREGORIAN +PRODID:-//SabreDAV//SabreDAV//EN +X-WR-CALNAME:Journal Todo Event +X-APPLE-CALENDAR-COLOR:#0082c9 +BEGIN:VJOURNAL +UID:19970901T130000Z-123405@example.com +DTSTAMP:19970901T130000Z +DTSTART;VALUE=DATE:19970317 +SUMMARY:Staff meeting minutes +DESCRIPTION:1. Staff meeting: Participants include Joe\, + Lisa\, and Bob. Aurora project plans were reviewed. + There is currently no budget reserves for this project. + Lisa will escalate to management. Next meeting on Tuesday.\n + 2. Telephone Conference: ABC Corp. sales representative + called to discuss new printer. Promised to get us a demo by + Friday.\n3. Henry Miller (Handsoff Insurance): Car was + totaled by tree. Is looking into a loaner car. 555-2323 + (tel). +END:VJOURNAL +BEGIN:VTODO +UID:20070313T123432Z-456553@example.com +DTSTAMP:20070313T123432Z +DUE;VALUE=DATE:20070501 +SUMMARY:Submit Quebec Income Tax Return for 2006 +CLASS:CONFIDENTIAL +CATEGORIES:FAMILY,FINANCE +STATUS:NEEDS-ACTION +END:VTODO +BEGIN:VEVENT +CREATED:20160809T163629Z +UID:0AD16F58-01B3-463B-A215-FD09FC729A02 +DTEND:20160816T100000 +TRANSP:OPAQUE +SUMMARY:Test Event +DTSTART:20160816T090000 +DTSTAMP:20160809T163632Z +SEQUENCE:0 +END:VEVENT +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/journal.ics b/apps/dav/tests/integration/UserMigration/assets/journal.ics new file mode 100644 index 00000000000..4add335f075 --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/journal.ics @@ -0,0 +1,22 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:Journal +X-APPLE-CALENDAR-COLOR:#0082c9 +BEGIN:VJOURNAL +UID:19970901T130000Z-123405@example.com +DTSTAMP:19970901T130000Z +DTSTART;VALUE=DATE:19970317 +SUMMARY:Staff meeting minutes +DESCRIPTION:1. Staff meeting: Participants include Joe\, + Lisa\, and Bob. Aurora project plans were reviewed. + There is currently no budget reserves for this project. + Lisa will escalate to management. Next meeting on Tuesday.\n + 2. Telephone Conference: ABC Corp. sales representative + called to discuss new printer. Promised to get us a demo by + Friday.\n3. Henry Miller (Handsoff Insurance): Car was + totaled by tree. Is looking into a loaner car. 555-2323 + (tel). +END:VJOURNAL +END:VCALENDAR diff --git a/apps/dav/tests/integration/UserMigration/assets/todo.ics b/apps/dav/tests/integration/UserMigration/assets/todo.ics new file mode 100644 index 00000000000..48f00e6315a --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/assets/todo.ics @@ -0,0 +1,16 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//SabreDAV//SabreDAV//EN +CALSCALE:GREGORIAN +X-WR-CALNAME:Todo +X-APPLE-CALENDAR-COLOR:#0082c9 +BEGIN:VTODO +UID:20070313T123432Z-456553@example.com +DTSTAMP:20070313T123432Z +DUE;VALUE=DATE:20070501 +SUMMARY:Submit Quebec Income Tax Return for 2006 +CLASS:CONFIDENTIAL +CATEGORIES:FAMILY,FINANCE +STATUS:NEEDS-ACTION +END:VTODO +END:VCALENDAR diff --git a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php index d77f65b33f5..7416cf7a3f7 100644 --- a/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/ObjectTreeTest.php @@ -34,6 +34,7 @@ use OC\Files\Storage\Temporary; use OC\Files\View; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\ObjectTree; +use OCP\Files\Mount\IMountManager; /** * Class ObjectTreeTest @@ -266,7 +267,7 @@ class ObjectTreeTest extends \Test\TestCase { ]; } - + public function testGetNodeForPathInvalidPath() { $this->expectException(\OCA\DAV\Connector\Sabre\Exception\InvalidPath::class); @@ -287,8 +288,7 @@ class ObjectTreeTest extends \Test\TestCase { $rootNode = $this->getMockBuilder(Directory::class) ->disableOriginalConstructor() ->getMock(); - $mountManager = $this->getMockBuilder(Manager::class) - ->getMock(); + $mountManager = $this->createMock(IMountManager::class); $tree = new \OCA\DAV\Connector\Sabre\ObjectTree(); $tree->init($rootNode, $view, $mountManager); @@ -314,8 +314,7 @@ class ObjectTreeTest extends \Test\TestCase { $rootNode = $this->getMockBuilder(Directory::class) ->disableOriginalConstructor() ->getMock(); - $mountManager = $this->getMockBuilder(Manager::class) - ->getMock(); + $mountManager = $this->createMock(IMountManager::class); $tree = new \OCA\DAV\Connector\Sabre\ObjectTree(); $tree->init($rootNode, $view, $mountManager); diff --git a/apps/files_external/tests/PersonalMountTest.php b/apps/files_external/tests/PersonalMountTest.php index b8a57657f9d..024695b0188 100644 --- a/apps/files_external/tests/PersonalMountTest.php +++ b/apps/files_external/tests/PersonalMountTest.php @@ -25,8 +25,13 @@ namespace OCA\Files_External\Tests; use OC\Files\Mount\Manager; +use OC\Files\SetupManagerFactory; use OCA\Files_External\Lib\PersonalMount; use OCA\Files_External\Lib\StorageConfig; +use OCP\Diagnostics\IEventLogger; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Config\IMountProviderCollection; +use OCP\IUserManager; use Test\TestCase; class PersonalMountTest extends TestCase { @@ -47,7 +52,7 @@ class PersonalMountTest extends TestCase { $mount = new PersonalMount($storageService, $storageConfig, 10, $storage, '/foo'); - $mountManager = new Manager(); + $mountManager = new Manager($this->createMock(SetupManagerFactory::class)); $mountManager->addMount($mount); $this->assertEquals([$mount], $mountManager->findByStorageId('dummy')); diff --git a/apps/files_sharing/l10n/eu.js b/apps/files_sharing/l10n/eu.js index faf1ad66257..c7e2baa4be0 100644 --- a/apps/files_sharing/l10n/eu.js +++ b/apps/files_sharing/l10n/eu.js @@ -123,6 +123,8 @@ OC.L10N.register( "Could not lock node" : "Ezin izan da nodoa blokeatu", "Could not lock path" : "Ezin izan da bidea blokeatu", "Wrong or no update parameter given" : "Eguneraketa parametrorik ez da eman edo okerra da", + "Share must at least have READ or CREATE permissions" : "Partekatzeak gutxienez IRAKURRI edo SORTU egiteko baimenak behar ditu", + "Share must have READ permission if UPDATE or DELETE permission is set." : "Partekatzeak IRAKURRI egiteko baimenak behar ditu, EGUNERATU edo EZABATU baimenak baldin badauzka.", "\"Sending the password by Nextcloud Talk\" for sharing a file or folder failed because Nextcloud Talk is not enabled." : "\"Nextcloud Talk-ek pasahitza bidaltzeak\" huts egin du ez dagoelako Nextcloud Talk gaituta fitxategi edo karpeta bat partekatzeko.", "shared by %s" : "%s erabiltzaileak partekatua", "Download all files" : "Deskargatu fitxategi guztiak", @@ -151,6 +153,10 @@ OC.L10N.register( "Read only" : "Irakurtzeko soilik", "Allow upload and editing" : "Onartu kargatzea eta edizioa", "File drop (upload only)" : "Fitxategia jaregin (kargatzeko soilik)", + "Custom permissions" : "Baimen pertsonalizatuak", + "Read" : "Irakurri", + "Upload" : "Kargatu", + "Edit" : "Aldatu", "Allow creating" : "Baimendu sortzea", "Allow deleting" : "Baimendu ezabatzea", "Allow resharing" : "Baimendu birpartekatzea", diff --git a/apps/files_sharing/l10n/eu.json b/apps/files_sharing/l10n/eu.json index 77b2414f01d..813338441ad 100644 --- a/apps/files_sharing/l10n/eu.json +++ b/apps/files_sharing/l10n/eu.json @@ -121,6 +121,8 @@ "Could not lock node" : "Ezin izan da nodoa blokeatu", "Could not lock path" : "Ezin izan da bidea blokeatu", "Wrong or no update parameter given" : "Eguneraketa parametrorik ez da eman edo okerra da", + "Share must at least have READ or CREATE permissions" : "Partekatzeak gutxienez IRAKURRI edo SORTU egiteko baimenak behar ditu", + "Share must have READ permission if UPDATE or DELETE permission is set." : "Partekatzeak IRAKURRI egiteko baimenak behar ditu, EGUNERATU edo EZABATU baimenak baldin badauzka.", "\"Sending the password by Nextcloud Talk\" for sharing a file or folder failed because Nextcloud Talk is not enabled." : "\"Nextcloud Talk-ek pasahitza bidaltzeak\" huts egin du ez dagoelako Nextcloud Talk gaituta fitxategi edo karpeta bat partekatzeko.", "shared by %s" : "%s erabiltzaileak partekatua", "Download all files" : "Deskargatu fitxategi guztiak", @@ -149,6 +151,10 @@ "Read only" : "Irakurtzeko soilik", "Allow upload and editing" : "Onartu kargatzea eta edizioa", "File drop (upload only)" : "Fitxategia jaregin (kargatzeko soilik)", + "Custom permissions" : "Baimen pertsonalizatuak", + "Read" : "Irakurri", + "Upload" : "Kargatu", + "Edit" : "Aldatu", "Allow creating" : "Baimendu sortzea", "Allow deleting" : "Baimendu ezabatzea", "Allow resharing" : "Baimendu birpartekatzea", diff --git a/apps/files_sharing/lib/External/Manager.php b/apps/files_sharing/lib/External/Manager.php index a48e2a63ae4..a8510321a5a 100644 --- a/apps/files_sharing/lib/External/Manager.php +++ b/apps/files_sharing/lib/External/Manager.php @@ -43,6 +43,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\ICloudFederationFactory; use OCP\Federation\ICloudFederationProviderManager; use OCP\Files; +use OCP\Files\NotFoundException; use OCP\Files\Storage\IStorageFactory; use OCP\Http\Client\IClientService; use OCP\IDBConnection; @@ -599,8 +600,9 @@ class Manager { } public function removeShare($mountPoint): bool { - $mountPointObj = $this->mountManager->find($mountPoint); - if ($mountPointObj === null) { + try { + $mountPointObj = $this->mountManager->find($mountPoint); + } catch (NotFoundException $e) { $this->logger->error('Mount point to remove share not found', ['mountPoint' => $mountPoint]); return false; } diff --git a/apps/files_sharing/lib/Updater.php b/apps/files_sharing/lib/Updater.php index 9ce114f495d..ad194dde016 100644 --- a/apps/files_sharing/lib/Updater.php +++ b/apps/files_sharing/lib/Updater.php @@ -26,6 +26,7 @@ */ namespace OCA\Files_Sharing; +use OC\Files\Mount\MountPoint; use OCP\Constants; use OCP\Share\IShare; @@ -105,6 +106,7 @@ class Updater { $mountManager = \OC\Files\Filesystem::getMountManager(); $mountedShares = $mountManager->findIn('/' . \OC_User::getUser() . '/files/' . $oldPath); foreach ($mountedShares as $mount) { + /** @var MountPoint $mount */ if ($mount->getStorage()->instanceOfStorage(ISharedStorage::class)) { $mountPoint = $mount->getMountPoint(); $target = str_replace($absOldPath, $absNewPath, $mountPoint); diff --git a/apps/files_sharing/tests/External/ManagerTest.php b/apps/files_sharing/tests/External/ManagerTest.php index ab7c682c3a6..307b630970f 100644 --- a/apps/files_sharing/tests/External/ManagerTest.php +++ b/apps/files_sharing/tests/External/ManagerTest.php @@ -31,14 +31,19 @@ namespace OCA\Files_Sharing\Tests\External; use OC\Federation\CloudIdManager; +use OC\Files\SetupManager; +use OC\Files\SetupManagerFactory; use OC\Files\Storage\StorageFactory; use OCA\Files_Sharing\External\Manager; use OCA\Files_Sharing\External\MountProvider; use OCA\Files_Sharing\Tests\TestCase; use OCP\Contacts\IManager; +use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; use OCP\Federation\ICloudFederationFactory; use OCP\Federation\ICloudFederationProviderManager; +use OCP\Files\Config\IMountProviderCollection; +use OCP\Files\NotFoundException; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; use OCP\IGroup; @@ -102,9 +107,8 @@ class ManagerTest extends TestCase { parent::setUp(); $this->uid = $this->getUniqueID('user'); - $this->createUser($this->uid, ''); - $this->user = \OC::$server->getUserManager()->get($this->uid); - $this->mountManager = new \OC\Files\Mount\Manager(); + $this->user = $this->createUser($this->uid, ''); + $this->mountManager = new \OC\Files\Mount\Manager($this->createMock(SetupManagerFactory::class)); $this->clientService = $this->getMockBuilder(IClientService::class) ->disableOriginalConstructor()->getMock(); $this->cloudFederationProviderManager = $this->createMock(ICloudFederationProviderManager::class); @@ -740,12 +744,12 @@ class ManagerTest extends TestCase { private function assertNotMount($mountPoint) { $mountPoint = rtrim($mountPoint, '/'); - $mount = $this->mountManager->find($this->getFullPath($mountPoint)); - if ($mount) { + try { + $mount = $this->mountManager->find($this->getFullPath($mountPoint)); $this->assertInstanceOf('\OCP\Files\Mount\IMountPoint', $mount); $this->assertNotEquals($this->getFullPath($mountPoint), rtrim($mount->getMountPoint(), '/')); - } else { - $this->assertNull($mount); + } catch (NotFoundException $e) { + } } diff --git a/apps/settings/lib/Activity/Provider.php b/apps/settings/lib/Activity/Provider.php index 2d5c858f5e8..a6314fdfb11 100644 --- a/apps/settings/lib/Activity/Provider.php +++ b/apps/settings/lib/Activity/Provider.php @@ -115,7 +115,11 @@ class Provider implements IProvider { } elseif ($event->getSubject() === self::EMAIL_CHANGED) { $subject = $this->l->t('Your email address was changed by an administrator'); } elseif ($event->getSubject() === self::APP_TOKEN_CREATED) { - $subject = $this->l->t('You created app password "{token}"'); + if ($event->getAffectedUser() === $event->getAuthor()) { + $subject = $this->l->t('You created app password "{token}"'); + } else { + $subject = $this->l->t('An administrator created app password "{token}"'); + } } elseif ($event->getSubject() === self::APP_TOKEN_DELETED) { $subject = $this->l->t('You deleted app password "{token}"'); } elseif ($event->getSubject() === self::APP_TOKEN_RENAMED) { diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index 37305f3edf5..5225cd04f09 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -614,15 +614,14 @@ Raw output } protected function getSuggestedOverwriteCliURL(): string { - $suggestedOverwriteCliUrl = ''; - if ($this->config->getSystemValue('overwrite.cli.url', '') === '') { - $suggestedOverwriteCliUrl = $this->request->getServerProtocol() . '://' . $this->request->getInsecureServerHost() . \OC::$WEBROOT; - if (!$this->config->getSystemValue('config_is_read_only', false)) { - // Set the overwrite URL when it was not set yet. - $this->config->setSystemValue('overwrite.cli.url', $suggestedOverwriteCliUrl); - $suggestedOverwriteCliUrl = ''; - } + $currentOverwriteCliUrl = $this->config->getSystemValue('overwrite.cli.url', ''); + $suggestedOverwriteCliUrl = $this->request->getServerProtocol() . '://' . $this->request->getInsecureServerHost() . \OC::$WEBROOT; + + // Check correctness by checking if it is a valid URL + if (filter_var($currentOverwriteCliUrl, FILTER_VALIDATE_URL)) { + $suggestedOverwriteCliUrl = ''; } + return $suggestedOverwriteCliUrl; } diff --git a/apps/settings/lib/Listener/AppPasswordCreatedActivityListener.php b/apps/settings/lib/Listener/AppPasswordCreatedActivityListener.php index 3eec74f4604..587d626ef97 100644 --- a/apps/settings/lib/Listener/AppPasswordCreatedActivityListener.php +++ b/apps/settings/lib/Listener/AppPasswordCreatedActivityListener.php @@ -31,6 +31,7 @@ use OCA\Settings\Activity\Provider; use OCP\Activity\IManager as IActivityManager; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; +use OCP\IUserSession; use Psr\Log\LoggerInterface; /** @@ -40,12 +41,17 @@ class AppPasswordCreatedActivityListener implements IEventListener { /** @var IActivityManager */ private $activityManager; + /** @var IUserSession */ + private $userSession; + /** @var LoggerInterface */ private $logger; public function __construct(IActivityManager $activityManager, + IUserSession $userSession, LoggerInterface $logger) { $this->activityManager = $activityManager; + $this->userSession = $userSession; $this->logger = $logger; } @@ -58,7 +64,7 @@ class AppPasswordCreatedActivityListener implements IEventListener { $activity->setApp('settings') ->setType('security') ->setAffectedUser($event->getToken()->getUID()) - ->setAuthor($event->getToken()->getUID()) + ->setAuthor($this->userSession->getUser() ? $this->userSession->getUser()->getUID() : '') ->setSubject(Provider::APP_TOKEN_CREATED, ['name' => $event->getToken()->getName()]) ->setObject('app_token', $event->getToken()->getId()); diff --git a/apps/theming/css/theming.scss b/apps/theming/css/theming.scss index 81b1b1a87ec..52337d2105f 100644 --- a/apps/theming/css/theming.scss +++ b/apps/theming/css/theming.scss @@ -167,8 +167,7 @@ $invert: luma($color-primary) > 0.6; } } -input.primary, -.alternative-logins a, { +input.primary { background-color: $color-primary-element; border: 1px solid $color-primary-text; color: $color-primary-text; @@ -202,17 +201,15 @@ input.primary, } } - input, - .alternative-logins a { + input { border: 1px solid nc-lighten($color-primary-text, 50%); } input.primary, - button.primary, - .alternative-logins a { + button.primary { background-color: $color-primary; color: $color-primary-text; } - a, + :not(div.alternative-logins) > a, label, footer p, .alternative-logins legend, @@ -257,7 +254,7 @@ input.primary, } #body-login { - a, label, p { + :not(.alternative-logins) a, label, p { color: $color-primary-text; } diff --git a/apps/user_ldap/lib/Mapping/AbstractMapping.php b/apps/user_ldap/lib/Mapping/AbstractMapping.php index 16973f76ff4..1a747cc8bfd 100644 --- a/apps/user_ldap/lib/Mapping/AbstractMapping.php +++ b/apps/user_ldap/lib/Mapping/AbstractMapping.php @@ -438,14 +438,14 @@ abstract class AbstractMapping { $picker = $this->dbc->getQueryBuilder(); $picker->select('owncloud_name') ->from($this->getTableName()); - $cursor = $picker->execute(); + $cursor = $picker->executeQuery(); $result = true; - while ($id = $cursor->fetchOne()) { + while (($id = $cursor->fetchOne()) !== false) { $preCallback($id); if ($isUnmapped = $this->unmap($id)) { $postCallback($id); } - $result &= $isUnmapped; + $result = $result && $isUnmapped; } $cursor->closeCursor(); return $result; diff --git a/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php b/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php index 2cca72ac493..3f590df7fa5 100644 --- a/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php +++ b/apps/user_ldap/lib/Migration/Version1130Date20211102154716.php @@ -259,7 +259,7 @@ class Version1130Date20211102154716 extends SimpleMigrationStep { $result = $select->executeQuery(); $idList = []; - while ($id = $result->fetchOne()) { + while (($id = $result->fetchOne()) !== false) { $idList[] = $id; } $result->closeCursor(); @@ -278,7 +278,7 @@ class Version1130Date20211102154716 extends SimpleMigrationStep { ->having($select->expr()->gt($select->func()->count('owncloud_name'), $select->createNamedParameter(1))); $result = $select->executeQuery(); - while ($uuid = $result->fetchOne()) { + while (($uuid = $result->fetchOne()) !== false) { yield $uuid; } $result->closeCursor(); diff --git a/apps/user_status/lib/Service/StatusService.php b/apps/user_status/lib/Service/StatusService.php index c7ad7afe322..5dd70e4ea5e 100644 --- a/apps/user_status/lib/Service/StatusService.php +++ b/apps/user_status/lib/Service/StatusService.php @@ -295,7 +295,7 @@ class StatusService { $userStatus->setStatus($status); $userStatus->setStatusTimestamp($this->timeFactory->getTime()); - $userStatus->setIsUserDefined(false); + $userStatus->setIsUserDefined(true); $userStatus->setIsBackup(false); $userStatus->setMessageId($messageId); $userStatus->setCustomIcon(null); diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php index 178bc87365b..34dbf507b91 100644 --- a/apps/workflowengine/lib/Manager.php +++ b/apps/workflowengine/lib/Manager.php @@ -351,7 +351,7 @@ class Manager implements IManager { $result = $qb->execute(); $this->operationsByScope[$scopeContext->getHash()] = []; - while ($opId = $result->fetchOne()) { + while (($opId = $result->fetchOne()) !== false) { $this->operationsByScope[$scopeContext->getHash()][] = (int)$opId; } $result->closeCursor(); diff --git a/apps/workflowengine/lib/Migration/PopulateNewlyIntroducedDatabaseFields.php b/apps/workflowengine/lib/Migration/PopulateNewlyIntroducedDatabaseFields.php index 019d3ae6bcc..974793e99b2 100644 --- a/apps/workflowengine/lib/Migration/PopulateNewlyIntroducedDatabaseFields.php +++ b/apps/workflowengine/lib/Migration/PopulateNewlyIntroducedDatabaseFields.php @@ -57,7 +57,7 @@ class PopulateNewlyIntroducedDatabaseFields implements IRepairStep { $qb = $this->dbc->getQueryBuilder(); $insertQuery = $qb->insert('flow_operations_scope'); - while ($id = $ids->fetchOne()) { + while (($id = $ids->fetchOne()) !== false) { $insertQuery->values(['operation_id' => $qb->createNamedParameter($id), 'type' => IManager::SCOPE_ADMIN]); $insertQuery->execute(); } |