From 4eff3594416481dbbf34057f53072d92119ababd Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Tue, 8 Feb 2022 06:54:07 +0000 Subject: [PATCH 1/6] Calendar export and import Signed-off-by: Christopher Ng --- apps/dav/appinfo/info.xml | 2 + .../composer/composer/autoload_classmap.php | 5 + .../dav/composer/composer/autoload_static.php | 5 + apps/dav/lib/CalDAV/CalendarImpl.php | 7 + apps/dav/lib/Command/ExportCalendars.php | 83 ++++ apps/dav/lib/Command/ImportCalendar.php | 94 ++++ .../lib/UserMigration/CalendarMigrator.php | 457 ++++++++++++++++++ .../CalendarMigratorException.php | 32 ++ .../InvalidCalendarException.php | 32 ++ lib/public/Calendar/ICalendar.php | 5 + 10 files changed, 722 insertions(+) create mode 100644 apps/dav/lib/Command/ExportCalendars.php create mode 100644 apps/dav/lib/Command/ImportCalendar.php create mode 100644 apps/dav/lib/UserMigration/CalendarMigrator.php create mode 100644 apps/dav/lib/UserMigration/CalendarMigratorException.php create mode 100644 apps/dav/lib/UserMigration/InvalidCalendarException.php diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 8462ed1816d42..88c4ee03ac373 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -56,6 +56,8 @@ OCA\DAV\Command\SyncBirthdayCalendar OCA\DAV\Command\SyncSystemAddressBook OCA\DAV\Command\RemoveInvalidShares + OCA\DAV\Command\ExportCalendars + OCA\DAV\Command\ImportCalendar diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 9cfefaed1c85d..bf0fe9cbf7502 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -124,6 +124,8 @@ 'OCA\\DAV\\Command\\CreateAddressBook' => $baseDir . '/../lib/Command/CreateAddressBook.php', 'OCA\\DAV\\Command\\CreateCalendar' => $baseDir . '/../lib/Command/CreateCalendar.php', 'OCA\\DAV\\Command\\DeleteCalendar' => $baseDir . '/../lib/Command/DeleteCalendar.php', + 'OCA\\DAV\\Command\\ExportCalendars' => $baseDir . '/../lib/Command/ExportCalendars.php', + 'OCA\\DAV\\Command\\ImportCalendar' => $baseDir . '/../lib/Command/ImportCalendar.php', 'OCA\\DAV\\Command\\ListCalendars' => $baseDir . '/../lib/Command/ListCalendars.php', 'OCA\\DAV\\Command\\MoveCalendar' => $baseDir . '/../lib/Command/MoveCalendar.php', 'OCA\\DAV\\Command\\RemoveInvalidShares' => $baseDir . '/../lib/Command/RemoveInvalidShares.php', @@ -299,4 +301,7 @@ '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 01c24fe38c898..18bfd64296029 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -139,6 +139,8 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Command\\CreateAddressBook' => __DIR__ . '/..' . '/../lib/Command/CreateAddressBook.php', 'OCA\\DAV\\Command\\CreateCalendar' => __DIR__ . '/..' . '/../lib/Command/CreateCalendar.php', 'OCA\\DAV\\Command\\DeleteCalendar' => __DIR__ . '/..' . '/../lib/Command/DeleteCalendar.php', + 'OCA\\DAV\\Command\\ExportCalendars' => __DIR__ . '/..' . '/../lib/Command/ExportCalendars.php', + 'OCA\\DAV\\Command\\ImportCalendar' => __DIR__ . '/..' . '/../lib/Command/ImportCalendar.php', 'OCA\\DAV\\Command\\ListCalendars' => __DIR__ . '/..' . '/../lib/Command/ListCalendars.php', 'OCA\\DAV\\Command\\MoveCalendar' => __DIR__ . '/..' . '/../lib/Command/MoveCalendar.php', 'OCA\\DAV\\Command\\RemoveInvalidShares' => __DIR__ . '/..' . '/../lib/Command/RemoveInvalidShares.php', @@ -314,6 +316,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/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php index 1c36db68cca85..87bed32428f7d 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -69,6 +69,13 @@ public function getKey() { return $this->calendarInfo['id']; } + /** + * {@inheritDoc} + */ + public function getUri() { + return $this->calendarInfo['uri']; + } + /** * In comparison to getKey() this function returns a human readable (maybe translated) name * @return null|string diff --git a/apps/dav/lib/Command/ExportCalendars.php b/apps/dav/lib/Command/ExportCalendars.php new file mode 100644 index 0000000000000..770d6edc4253a --- /dev/null +++ b/apps/dav/lib/Command/ExportCalendars.php @@ -0,0 +1,83 @@ + + * + * @author Christopher Ng + * + * @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 . + * + */ + +namespace OCA\DAV\Command; + +use OC\Core\Command\Base; +use OCA\DAV\UserMigration\CalendarMigrator; +use OCA\DAV\UserMigration\CalendarMigratorException; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ExportCalendars extends Base { + + /** @var IUserManager */ + private $userManager; + + /** @var CalendarMigrator */ + private $calendarMigrator; + + public function __construct( + IUserManager $userManager, + CalendarMigrator $calendarMigrator + ) { + parent::__construct(); + $this->userManager = $userManager; + $this->calendarMigrator = $calendarMigrator; + } + + protected function configure() { + $this + ->setName('dav:export-calendars') + ->setDescription('Export the calendars of a user') + ->addArgument( + 'user', + InputArgument::REQUIRED, + 'User to export', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $this->userManager->get($input->getArgument('user')); + + if (!$user instanceof IUser) { + $output->writeln('User ' . $input->getArgument('user') . ' does not exist'); + return 1; + } + + try { + $this->calendarMigrator->export($user, $output); + } catch (CalendarMigratorException $e) { + $output->writeln('' . $e->getMessage() . ''); + return $e->getCode() !== 0 ? (int)$e->getCode() : 1; + } + + return 0; + } +} diff --git a/apps/dav/lib/Command/ImportCalendar.php b/apps/dav/lib/Command/ImportCalendar.php new file mode 100644 index 0000000000000..193ecc3b29bc3 --- /dev/null +++ b/apps/dav/lib/Command/ImportCalendar.php @@ -0,0 +1,94 @@ + + * + * @author Christopher Ng + * + * @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 . + * + */ + +namespace OCA\DAV\Command; + +use OC\Core\Command\Base; +use OCA\DAV\UserMigration\CalendarMigrator; +use OCA\DAV\UserMigration\CalendarMigratorException; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ImportCalendar extends Base { + + /** @var IUserManager */ + private $userManager; + + /** @var CalendarMigrator */ + private $calendarMigrator; + + public function __construct( + IUserManager $userManager, + CalendarMigrator $calendarMigrator + ) { + parent::__construct(); + $this->userManager = $userManager; + $this->calendarMigrator = $calendarMigrator; + } + + protected function configure() { + $this + ->setName('dav:import-calendar') + ->setDescription('Import a calendar to a user\'s account') + ->addArgument( + 'user', + InputArgument::REQUIRED, + 'User to import the calendar for', + ) + ->addArgument( + 'path', + InputArgument::REQUIRED, + 'Path to the *.ics file', + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $user = $this->userManager->get($input->getArgument('user')); + + [ + 'basename' => $filename, + 'dirname' => $srcDir, + ] = pathinfo($input->getArgument('path')); + + + if (!$user instanceof IUser) { + $output->writeln('User ' . $input->getArgument('user') . ' does not exist'); + return 1; + } + + try { + $this->calendarMigrator->import($user, $srcDir, $filename, $output); + } catch (CalendarMigratorException $e) { + $output->writeln('' . $e->getMessage() . ''); + return $e->getCode() !== 0 ? (int)$e->getCode() : 1; + } + + return 0; + } +} diff --git a/apps/dav/lib/UserMigration/CalendarMigrator.php b/apps/dav/lib/UserMigration/CalendarMigrator.php new file mode 100644 index 0000000000000..c1252d7517aea --- /dev/null +++ b/apps/dav/lib/UserMigration/CalendarMigrator.php @@ -0,0 +1,457 @@ + + * + * @author Christopher Ng + * + * @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 . + * + */ + +namespace OCA\DAV\UserMigration; + +use function Safe\fopen; +use function Safe\substr; +use OC\Files\Filesystem; +use OC\Files\View; +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 Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Version as SabreDavVersion; +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\FilesystemException; +use Symfony\Component\Console\Output\OutputInterface; + +class CalendarMigrator { + + 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; + + public const USERS_URI_ROOT = 'principals/users/'; + + public const FILENAME_EXT = '.ics'; + + public const MIGRATED_URI_PREFIX = 'migrated-'; + + 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()); + } + + public function getPrincipalUri(IUser $user): string { + return CalendarMigrator::USERS_URI_ROOT . $user->getUID(); + } + + /** + * @return array{name: string, vCalendar: VCalendar} + * + * @throws CalendarMigratorException + * @throws InvalidCalendarException + */ + public function getCalendarExportData(IUser $user, ICalendar $calendar): array { + $userId = $user->getUID(); + $calendarId = $calendar->getKey(); + $calendarInfo = $this->calDavBackend->getCalendarById($calendarId); + + if (!empty($calendarInfo)) { + $uri = $calendarInfo['uri']; + $path = CalDAVPlugin::CALENDAR_ROOT . "/$userId/$uri"; + + // NOTE implementation below based on \Sabre\CalDAV\ICSExportPlugin::httpGet() + + $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(); + } + + // NOTE implementation below based on \Sabre\CalDAV\ICSExportPlugin::generateResponse() + + $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, + ); + + return [ + 'name' => $calendarNode->getName(), + 'vCalendar' => $mergedCalendar, + ]; + } + + throw new CalendarMigratorException(); + } + + /** + * @return array + * + * @throws CalendarMigratorException + */ + public function getCalendarExports(IUser $user): array { + $principalUri = $this->getPrincipalUri($user); + + return array_values(array_filter(array_map( + function (ICalendar $calendar) use ($user) { + try { + return $this->getCalendarExportData($user, $calendar); + } catch (CalendarMigratorException $e) { + throw new CalendarMigratorException(); + } catch (InvalidCalendarException $e) { + // Allow this exception as invalid (e.g. deleted) calendars are not to be exported + } + }, + $this->calendarManager->getCalendarsForPrincipal($principalUri), + ))); + } + + public function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): string { + $principalUri = $this->getPrincipalUri($user); + $initialCalendarUri = substr($initialCalendarUri, 0, strlen(CalendarMigrator::MIGRATED_URI_PREFIX)) === CalendarMigrator::MIGRATED_URI_PREFIX + ? $initialCalendarUri + : CalendarMigrator::MIGRATED_URI_PREFIX . $initialCalendarUri; + + $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; + } + + /** + * @throws CalendarMigratorException + */ + protected function writeExport(IUser $user, string $data, string $destDir, string $filename, OutputInterface $output): void { + $userId = $user->getUID(); + + \OC::$server->getUserFolder($userId); + Filesystem::initMountPoints($userId); + + $view = new View(); + + if ($view->file_put_contents("$destDir/$filename", $data) === false) { + throw new CalendarMigratorException('Could not export calendar'); + } + + $output->writeln("✅ Exported calendar of <$userId> into $destDir/$filename"); + } + + public function export(IUser $user, OutputInterface $output): void { + $userId = $user->getUID(); + + try { + $calendarExports = $this->getCalendarExports($user); + } catch (CalendarMigratorException $e) { + $output->writeln("Error exporting <$userId> calendars"); + } + + if (empty($calendarExports)) { + $output->writeln("User <$userId> has 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; + + $this->writeExport( + $user, + $vCalendar->serialize(), + // TESTING directory does not automatically get created so just write to user directory, this will be put in a zip with all other user_migration data + // "/$userId/export/$appId", + "/$userId", + $filename, + $output, + ); + } + } + + /** + * Return an associative array mapping Time Zone ID to VTimeZone component + * + * @return array + */ + public function getCalendarTimezones(VCalendar $vCalendar): array { + /** @var VTimeZone[] $calendarTimezones */ + $calendarTimezones = array_values(array_filter( + $vCalendar->getComponents(), + fn ($component) => $component->name === 'VTIMEZONE', + )); + + /** @var array $calendarTimezoneMap */ + $calendarTimezoneMap = []; + foreach ($calendarTimezones as $vTimeZone) { + $calendarTimezoneMap[$vTimeZone->getTimeZone()->getName()] = $vTimeZone; + } + + return $calendarTimezoneMap; + } + + /** + * @return VTimeZone[] + */ + public 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, + ))); + } + + public function sanitizeComponent(VObjectComponent $component): VObjectComponent { + // Operate on the component clone to prevent mutation of the original + $componentClone = clone $component; + + // Remove RSVP parameters to prevent automatically sending invitation emails to attendees on import + foreach ($componentClone->children() as $child) { + if ( + $child->name === 'ATTENDEE' + && isset($child->parameters['RSVP']) + ) { + unset($child->parameters['RSVP']); + } + } + + return $componentClone; + } + + /** + * @return VObjectComponent[] + */ + public function getRequiredImportComponents(VCalendar $vCalendar, VObjectComponent $component): array { + $component = $this->sanitizeComponent($component); + /** @var array $timezoneComponents */ + $timezoneComponents = $this->getTimezonesForComponent($vCalendar, $component); + return [ + ...$timezoneComponents, + $component, + ]; + } + + public function initCalendarObject(): VCalendar { + $vCalendarObject = new VCalendar(); + $vCalendarObject->PRODID = $this->sabreDavServer::$exposeVersion + ? '-//SabreDAV//SabreDAV ' . SabreDavVersion::VERSION . '//EN' + : '-//SabreDAV//SabreDAV//EN'; + return $vCalendarObject; + } + + public function importCalendarObject(int $calendarId, VCalendar $vCalendarObject): void { + try { + $this->calDavBackend->createCalendarObject( + $calendarId, + UUIDUtil::getUUID() . CalendarMigrator::FILENAME_EXT, + $vCalendarObject->serialize(), + CalDavBackend::CALENDAR_TYPE_CALENDAR, + ); + } catch (BadRequest $e) { + // Rollback creation of calendar on error + $this->calDavBackend->deleteCalendar($calendarId, true); + } + } + + /** + * @throws CalendarMigratorException + */ + public function importCalendar(IUser $user, string $filename, string $initialCalendarUri, VCalendar $vCalendar): 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 $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 $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); + } + + 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); + } + } + + /** + * @throws FilesystemException + * @throws CalendarMigratorException + */ + public function import(IUser $user, string $srcDir, string $filename, OutputInterface $output): void { + $userId = $user->getUID(); + + try { + /** @var VCalendar $vCalendar */ + $vCalendar = VObjectReader::read( + fopen("$srcDir/$filename", 'r'), + VObjectReader::OPTION_FORGIVING, + ); + } catch (FilesystemException $e) { + throw new FilesystemException("Failed to read file: \"$srcDir/$filename\""); + } + + $problems = $vCalendar->validate(); + if (empty($problems)) { + $splitFilename = explode('_', $filename, 2); + if (count($splitFilename) !== 2) { + $output->writeln("Invalid filename, filename must be of the format: \"_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . "\""); + throw new CalendarMigratorException(); + } + [$initialCalendarUri, $suffix] = $splitFilename; + + $this->importCalendar( + $user, + $filename, + $initialCalendarUri, + $vCalendar, + ); + + $vCalendar->destroy(); + + $output->writeln("✅ Imported calendar \"$filename\" into account of <$userId>"); + } else { + throw new CalendarMigratorException("Invalid data contained in \"$srcDir/$filename\""); + } + } +} diff --git a/apps/dav/lib/UserMigration/CalendarMigratorException.php b/apps/dav/lib/UserMigration/CalendarMigratorException.php new file mode 100644 index 0000000000000..91bac58ffac6d --- /dev/null +++ b/apps/dav/lib/UserMigration/CalendarMigratorException.php @@ -0,0 +1,32 @@ + + * + * @author Christopher Ng + * + * @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 . + * + */ + +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 0000000000000..0e42ef1bc203d --- /dev/null +++ b/apps/dav/lib/UserMigration/InvalidCalendarException.php @@ -0,0 +1,32 @@ + + * + * @author Christopher Ng + * + * @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 . + * + */ + +namespace OCA\DAV\UserMigration; + +use Exception; + +class InvalidCalendarException extends Exception { +} diff --git a/lib/public/Calendar/ICalendar.php b/lib/public/Calendar/ICalendar.php index 551870de20e0b..f2c94cb5400d6 100644 --- a/lib/public/Calendar/ICalendar.php +++ b/lib/public/Calendar/ICalendar.php @@ -39,6 +39,11 @@ interface ICalendar { */ public function getKey(); + /** + * @since 24.0.0 + */ + public function getUri(); + /** * In comparison to getKey() this function returns a human readable (maybe translated) name * @return null|string From 4c3d68381b6bb1847a2b5b22dbfc61a83c529df6 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Thu, 17 Feb 2022 03:36:17 +0000 Subject: [PATCH 2/6] Integration tests Signed-off-by: Christopher Ng --- .../UserMigration/CalendarMigratorTest.php | 133 ++++++++++++++++++ .../UserMigration/assets/event-alarms.ics | 42 ++++++ .../UserMigration/assets/event-attendees.ics | 39 +++++ .../UserMigration/assets/event-categories.ics | 35 +++++ .../assets/event-complex-alarm-recurring.ics | 33 +++++ .../assets/event-complex-recurrence.ics | 87 ++++++++++++ .../assets/event-custom-color.ics | 34 +++++ .../assets/event-multiple-recurring.ics | 74 ++++++++++ .../UserMigration/assets/event-multiple.ics | 62 ++++++++ .../UserMigration/assets/event-recurring.ics | 87 ++++++++++++ .../UserMigration/assets/event-timed.ics | 34 +++++ .../assets/journal-todo-event.ics | 41 ++++++ .../UserMigration/assets/journal.ics | 22 +++ .../integration/UserMigration/assets/todo.ics | 16 +++ 14 files changed, 739 insertions(+) create mode 100644 apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-alarms.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-attendees.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-categories.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-complex-alarm-recurring.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-complex-recurrence.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-custom-color.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-multiple-recurring.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-multiple.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-recurring.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/event-timed.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/journal-todo-event.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/journal.ics create mode 100644 apps/dav/tests/integration/UserMigration/assets/todo.ics diff --git a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php new file mode 100644 index 0000000000000..1f7bf6300943d --- /dev/null +++ b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php @@ -0,0 +1,133 @@ + + * + * @author Christopher Ng + * + * @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 . + * + */ + +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 Test\TestCase; + +/** + * @group DB + */ +class CalendarMigratorTest extends TestCase { + + private IUserManager $userManager; + + private CalendarMigrator $migrator; + + 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); + } + + 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->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->migrator->importCalendar($user, $filename, $initialCalendarUri, $importCalendar); + + $calendarExports = $this->migrator->getCalendarExports($user); + $this->assertCount(1, $calendarExports); + + /** @var VCalendar $exportCalendar */ + ['vCalendar' => $exportCalendar] = reset($calendarExports); + + $this->assertEqualsCanonicalizing( + $this->getProperties($importCalendar), + $this->getProperties($exportCalendar), + ); + + $this->assertEqualsCanonicalizing( + // Components are 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 0000000000000..5dc6acbb605f1 --- /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 0000000000000..4100365926c7a --- /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 0000000000000..7e7054d0251f0 --- /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 0000000000000..8aa8e085c63a3 --- /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 0000000000000..45fb5540af6f1 --- /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 0000000000000..012ec9fbc17c0 --- /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 0000000000000..a9e748b52525f --- /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 0000000000000..ba0f545705286 --- /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 0000000000000..d59627b4a6caf --- /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 0000000000000..980a290a4f77f --- /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 0000000000000..6a74aface8444 --- /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 0000000000000..4add335f075b7 --- /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 0000000000000..48f00e6315a97 --- /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 From 302a67f6853065122ef8648cdbdefc1504e8d350 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Wed, 23 Feb 2022 05:27:49 +0000 Subject: [PATCH 3/6] Integrate migrator Signed-off-by: Christopher Ng --- apps/dav/appinfo/info.xml | 2 - .../composer/composer/autoload_classmap.php | 2 - .../dav/composer/composer/autoload_static.php | 2 - apps/dav/lib/AppInfo/Application.php | 3 + apps/dav/lib/Command/ExportCalendars.php | 83 ----------- apps/dav/lib/Command/ImportCalendar.php | 94 ------------ .../lib/UserMigration/CalendarMigrator.php | 140 +++++++++--------- .../UserMigration/CalendarMigratorTest.php | 8 +- 8 files changed, 74 insertions(+), 260 deletions(-) delete mode 100644 apps/dav/lib/Command/ExportCalendars.php delete mode 100644 apps/dav/lib/Command/ImportCalendar.php diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 88c4ee03ac373..8462ed1816d42 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -56,8 +56,6 @@ OCA\DAV\Command\SyncBirthdayCalendar OCA\DAV\Command\SyncSystemAddressBook OCA\DAV\Command\RemoveInvalidShares - OCA\DAV\Command\ExportCalendars - OCA\DAV\Command\ImportCalendar diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index bf0fe9cbf7502..f4b178148175d 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -124,8 +124,6 @@ 'OCA\\DAV\\Command\\CreateAddressBook' => $baseDir . '/../lib/Command/CreateAddressBook.php', 'OCA\\DAV\\Command\\CreateCalendar' => $baseDir . '/../lib/Command/CreateCalendar.php', 'OCA\\DAV\\Command\\DeleteCalendar' => $baseDir . '/../lib/Command/DeleteCalendar.php', - 'OCA\\DAV\\Command\\ExportCalendars' => $baseDir . '/../lib/Command/ExportCalendars.php', - 'OCA\\DAV\\Command\\ImportCalendar' => $baseDir . '/../lib/Command/ImportCalendar.php', 'OCA\\DAV\\Command\\ListCalendars' => $baseDir . '/../lib/Command/ListCalendars.php', 'OCA\\DAV\\Command\\MoveCalendar' => $baseDir . '/../lib/Command/MoveCalendar.php', 'OCA\\DAV\\Command\\RemoveInvalidShares' => $baseDir . '/../lib/Command/RemoveInvalidShares.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 18bfd64296029..d164ab2b1ce71 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -139,8 +139,6 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Command\\CreateAddressBook' => __DIR__ . '/..' . '/../lib/Command/CreateAddressBook.php', 'OCA\\DAV\\Command\\CreateCalendar' => __DIR__ . '/..' . '/../lib/Command/CreateCalendar.php', 'OCA\\DAV\\Command\\DeleteCalendar' => __DIR__ . '/..' . '/../lib/Command/DeleteCalendar.php', - 'OCA\\DAV\\Command\\ExportCalendars' => __DIR__ . '/..' . '/../lib/Command/ExportCalendars.php', - 'OCA\\DAV\\Command\\ImportCalendar' => __DIR__ . '/..' . '/../lib/Command/ImportCalendar.php', 'OCA\\DAV\\Command\\ListCalendars' => __DIR__ . '/..' . '/../lib/Command/ListCalendars.php', 'OCA\\DAV\\Command\\MoveCalendar' => __DIR__ . '/..' . '/../lib/Command/MoveCalendar.php', 'OCA\\DAV\\Command\\RemoveInvalidShares' => __DIR__ . '/..' . '/../lib/Command/RemoveInvalidShares.php', diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 99521b61e8b2f..f29161d69760a 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -80,6 +80,7 @@ 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 @@ public function register(IRegistrationContext $context): void { $context->registerNotifierService(Notifier::class); $context->registerCalendarProvider(CalendarProvider::class); + + $context->registerUserMigrator(CalendarMigrator::class); } public function boot(IBootContext $context): void { diff --git a/apps/dav/lib/Command/ExportCalendars.php b/apps/dav/lib/Command/ExportCalendars.php deleted file mode 100644 index 770d6edc4253a..0000000000000 --- a/apps/dav/lib/Command/ExportCalendars.php +++ /dev/null @@ -1,83 +0,0 @@ - - * - * @author Christopher Ng - * - * @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 . - * - */ - -namespace OCA\DAV\Command; - -use OC\Core\Command\Base; -use OCA\DAV\UserMigration\CalendarMigrator; -use OCA\DAV\UserMigration\CalendarMigratorException; -use OCP\IUser; -use OCP\IUserManager; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class ExportCalendars extends Base { - - /** @var IUserManager */ - private $userManager; - - /** @var CalendarMigrator */ - private $calendarMigrator; - - public function __construct( - IUserManager $userManager, - CalendarMigrator $calendarMigrator - ) { - parent::__construct(); - $this->userManager = $userManager; - $this->calendarMigrator = $calendarMigrator; - } - - protected function configure() { - $this - ->setName('dav:export-calendars') - ->setDescription('Export the calendars of a user') - ->addArgument( - 'user', - InputArgument::REQUIRED, - 'User to export', - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $user = $this->userManager->get($input->getArgument('user')); - - if (!$user instanceof IUser) { - $output->writeln('User ' . $input->getArgument('user') . ' does not exist'); - return 1; - } - - try { - $this->calendarMigrator->export($user, $output); - } catch (CalendarMigratorException $e) { - $output->writeln('' . $e->getMessage() . ''); - return $e->getCode() !== 0 ? (int)$e->getCode() : 1; - } - - return 0; - } -} diff --git a/apps/dav/lib/Command/ImportCalendar.php b/apps/dav/lib/Command/ImportCalendar.php deleted file mode 100644 index 193ecc3b29bc3..0000000000000 --- a/apps/dav/lib/Command/ImportCalendar.php +++ /dev/null @@ -1,94 +0,0 @@ - - * - * @author Christopher Ng - * - * @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 . - * - */ - -namespace OCA\DAV\Command; - -use OC\Core\Command\Base; -use OCA\DAV\UserMigration\CalendarMigrator; -use OCA\DAV\UserMigration\CalendarMigratorException; -use OCP\IUser; -use OCP\IUserManager; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class ImportCalendar extends Base { - - /** @var IUserManager */ - private $userManager; - - /** @var CalendarMigrator */ - private $calendarMigrator; - - public function __construct( - IUserManager $userManager, - CalendarMigrator $calendarMigrator - ) { - parent::__construct(); - $this->userManager = $userManager; - $this->calendarMigrator = $calendarMigrator; - } - - protected function configure() { - $this - ->setName('dav:import-calendar') - ->setDescription('Import a calendar to a user\'s account') - ->addArgument( - 'user', - InputArgument::REQUIRED, - 'User to import the calendar for', - ) - ->addArgument( - 'path', - InputArgument::REQUIRED, - 'Path to the *.ics file', - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int { - $user = $this->userManager->get($input->getArgument('user')); - - [ - 'basename' => $filename, - 'dirname' => $srcDir, - ] = pathinfo($input->getArgument('path')); - - - if (!$user instanceof IUser) { - $output->writeln('User ' . $input->getArgument('user') . ' does not exist'); - return 1; - } - - try { - $this->calendarMigrator->import($user, $srcDir, $filename, $output); - } catch (CalendarMigratorException $e) { - $output->writeln('' . $e->getMessage() . ''); - return $e->getCode() !== 0 ? (int)$e->getCode() : 1; - } - - return 0; - } -} diff --git a/apps/dav/lib/UserMigration/CalendarMigrator.php b/apps/dav/lib/UserMigration/CalendarMigrator.php index c1252d7517aea..6a36a7204e181 100644 --- a/apps/dav/lib/UserMigration/CalendarMigrator.php +++ b/apps/dav/lib/UserMigration/CalendarMigrator.php @@ -26,10 +26,7 @@ namespace OCA\DAV\UserMigration; -use function Safe\fopen; use function Safe\substr; -use OC\Files\Filesystem; -use OC\Files\View; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin; use OCA\DAV\CalDAV\Plugin as CalDAVPlugin; @@ -41,6 +38,10 @@ 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\DAV\Exception\BadRequest; use Sabre\DAV\Version as SabreDavVersion; use Sabre\VObject\Component as VObjectComponent; @@ -52,7 +53,9 @@ use Safe\Exceptions\FilesystemException; use Symfony\Component\Console\Output\OutputInterface; -class CalendarMigrator { +class CalendarMigrator implements IMigrator { + + use TMigratorBasicVersionHandling; private CalDavBackend $calDavBackend; @@ -67,11 +70,13 @@ class CalendarMigrator { private SabreDavServer $sabreDavServer; - public const USERS_URI_ROOT = 'principals/users/'; + private const USERS_URI_ROOT = 'principals/users/'; + + private const FILENAME_EXT = '.ics'; - public const FILENAME_EXT = '.ics'; + private const MIGRATED_URI_PREFIX = 'migrated-'; - public const MIGRATED_URI_PREFIX = 'migrated-'; + private const EXPORT_ROOT = 'calendars/'; public function __construct( CalDavBackend $calDavBackend, @@ -86,12 +91,15 @@ public function __construct( $this->defaults = $defaults; $this->l10n = $l10n; + // Override trait property + $this->mandatory = true; + $root = new RootCollection(); $this->sabreDavServer = new SabreDavServer(new CachingTree($root)); $this->sabreDavServer->addPlugin(new CalDAVPlugin()); } - public function getPrincipalUri(IUser $user): string { + private function getPrincipalUri(IUser $user): string { return CalendarMigrator::USERS_URI_ROOT . $user->getUID(); } @@ -101,7 +109,7 @@ public function getPrincipalUri(IUser $user): string { * @throws CalendarMigratorException * @throws InvalidCalendarException */ - public function getCalendarExportData(IUser $user, ICalendar $calendar): array { + private function getCalendarExportData(IUser $user, ICalendar $calendar): array { $userId = $user->getUID(); $calendarId = $calendar->getKey(); $calendarInfo = $this->calDavBackend->getCalendarById($calendarId); @@ -157,7 +165,7 @@ public function getCalendarExportData(IUser $user, ICalendar $calendar): array { * * @throws CalendarMigratorException */ - public function getCalendarExports(IUser $user): array { + private function getCalendarExports(IUser $user): array { $principalUri = $this->getPrincipalUri($user); return array_values(array_filter(array_map( @@ -174,7 +182,7 @@ function (ICalendar $calendar) use ($user) { ))); } - public function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): string { + private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): string { $principalUri = $this->getPrincipalUri($user); $initialCalendarUri = substr($initialCalendarUri, 0, strlen(CalendarMigrator::MIGRATED_URI_PREFIX)) === CalendarMigrator::MIGRATED_URI_PREFIX ? $initialCalendarUri @@ -196,24 +204,11 @@ public function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): s } /** - * @throws CalendarMigratorException + * {@inheritDoc} */ - protected function writeExport(IUser $user, string $data, string $destDir, string $filename, OutputInterface $output): void { - $userId = $user->getUID(); - - \OC::$server->getUserFolder($userId); - Filesystem::initMountPoints($userId); - - $view = new View(); + public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void { + $output->writeln("Exporting calendars…"); - if ($view->file_put_contents("$destDir/$filename", $data) === false) { - throw new CalendarMigratorException('Could not export calendar'); - } - - $output->writeln("✅ Exported calendar of <$userId> into $destDir/$filename"); - } - - public function export(IUser $user, OutputInterface $output): void { $userId = $user->getUID(); try { @@ -234,24 +229,16 @@ public function export(IUser $user, OutputInterface $output): void { // 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; - $this->writeExport( - $user, - $vCalendar->serialize(), - // TESTING directory does not automatically get created so just write to user directory, this will be put in a zip with all other user_migration data - // "/$userId/export/$appId", - "/$userId", - $filename, - $output, - ); + if ($exportDestination->addFileContents(CalendarMigrator::EXPORT_ROOT . $filename, $vCalendar->serialize()) === false) { + throw new CalendarMigratorException(); + } } } /** - * Return an associative array mapping Time Zone ID to VTimeZone component - * * @return array */ - public function getCalendarTimezones(VCalendar $vCalendar): array { + private function getCalendarTimezones(VCalendar $vCalendar): array { /** @var VTimeZone[] $calendarTimezones */ $calendarTimezones = array_values(array_filter( $vCalendar->getComponents(), @@ -270,7 +257,7 @@ public function getCalendarTimezones(VCalendar $vCalendar): array { /** * @return VTimeZone[] */ - public function getTimezonesForComponent(VCalendar $vCalendar, VObjectComponent $component): array { + private function getTimezonesForComponent(VCalendar $vCalendar, VObjectComponent $component): array { $componentTimezoneIds = []; foreach ($component->children() as $child) { @@ -290,7 +277,7 @@ public function getTimezonesForComponent(VCalendar $vCalendar, VObjectComponent ))); } - public function sanitizeComponent(VObjectComponent $component): VObjectComponent { + private function sanitizeComponent(VObjectComponent $component): VObjectComponent { // Operate on the component clone to prevent mutation of the original $componentClone = clone $component; @@ -310,7 +297,7 @@ public function sanitizeComponent(VObjectComponent $component): VObjectComponent /** * @return VObjectComponent[] */ - public function getRequiredImportComponents(VCalendar $vCalendar, VObjectComponent $component): array { + private function getRequiredImportComponents(VCalendar $vCalendar, VObjectComponent $component): array { $component = $this->sanitizeComponent($component); /** @var array $timezoneComponents */ $timezoneComponents = $this->getTimezonesForComponent($vCalendar, $component); @@ -320,7 +307,7 @@ public function getRequiredImportComponents(VCalendar $vCalendar, VObjectCompone ]; } - public function initCalendarObject(): VCalendar { + private function initCalendarObject(): VCalendar { $vCalendarObject = new VCalendar(); $vCalendarObject->PRODID = $this->sabreDavServer::$exposeVersion ? '-//SabreDAV//SabreDAV ' . SabreDavVersion::VERSION . '//EN' @@ -328,7 +315,7 @@ public function initCalendarObject(): VCalendar { return $vCalendarObject; } - public function importCalendarObject(int $calendarId, VCalendar $vCalendarObject): void { + private function importCalendarObject(int $calendarId, VCalendar $vCalendarObject): void { try { $this->calDavBackend->createCalendarObject( $calendarId, @@ -345,7 +332,7 @@ public function importCalendarObject(int $calendarId, VCalendar $vCalendarObject /** * @throws CalendarMigratorException */ - public function importCalendar(IUser $user, string $filename, string $initialCalendarUri, VCalendar $vCalendar): void { + private function importCalendar(IUser $user, string $filename, string $initialCalendarUri, VCalendar $vCalendar): void { $principalUri = $this->getPrincipalUri($user); $calendarUri = $this->getUniqueCalendarUri($user, $initialCalendarUri); @@ -415,43 +402,50 @@ function (array $componentNames, VObjectComponent $component) { } /** + * {@inheritDoc} + * * @throws FilesystemException * @throws CalendarMigratorException */ - public function import(IUser $user, string $srcDir, string $filename, OutputInterface $output): void { - $userId = $user->getUID(); - - try { - /** @var VCalendar $vCalendar */ - $vCalendar = VObjectReader::read( - fopen("$srcDir/$filename", 'r'), - VObjectReader::OPTION_FORGIVING, - ); - } catch (FilesystemException $e) { - throw new FilesystemException("Failed to read file: \"$srcDir/$filename\""); + 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; } - $problems = $vCalendar->validate(); - if (empty($problems)) { - $splitFilename = explode('_', $filename, 2); - if (count($splitFilename) !== 2) { - $output->writeln("Invalid filename, filename must be of the format: \"_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . "\""); - throw new CalendarMigratorException(); + $output->writeln("Importing calendars…"); + + foreach ($importSource->getFolderListing(CalendarMigrator::EXPORT_ROOT) as $filename) { + try { + /** @var VCalendar $vCalendar */ + $vCalendar = VObjectReader::read( + $importSource->getFileAsStream(CalendarMigrator::EXPORT_ROOT . $filename), + VObjectReader::OPTION_FORGIVING, + ); + } catch (FilesystemException $e) { + throw new FilesystemException("Failed to read file: \"$filename\""); } - [$initialCalendarUri, $suffix] = $splitFilename; - $this->importCalendar( - $user, - $filename, - $initialCalendarUri, - $vCalendar, - ); + $problems = $vCalendar->validate(); + if (empty($problems)) { + $splitFilename = explode('_', $filename, 2); + if (count($splitFilename) !== 2) { + $output->writeln("Invalid filename, expected filename of the format: \"_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . "\""); + throw new CalendarMigratorException(); + } + [$initialCalendarUri, $suffix] = $splitFilename; - $vCalendar->destroy(); + $this->importCalendar( + $user, + $filename, + $initialCalendarUri, + $vCalendar, + ); - $output->writeln("✅ Imported calendar \"$filename\" into account of <$userId>"); - } else { - throw new CalendarMigratorException("Invalid data contained in \"$srcDir/$filename\""); + $vCalendar->destroy(); + } else { + throw new CalendarMigratorException("Invalid data contained in \"$filename\""); + } } } } diff --git a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php index 1f7bf6300943d..e2eb0c75d9bc0 100644 --- a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php +++ b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php @@ -97,7 +97,7 @@ private function getComponents(VCalendar $vCalendar): array { private function getSanitizedComponents(VCalendar $vCalendar): array { return array_map( // Elements of the serialized blob are sorted - fn (VObjectComponent $component) => $this->migrator->sanitizeComponent($component)->serialize(), + fn (VObjectComponent $component) => $this->invokePrivate($this->migrator, 'sanitizeComponent', [$component])->serialize(), $vCalendar->getComponents(), ); } @@ -111,9 +111,9 @@ public function testImportExportAsset(string $userId, string $filename, string $ $problems = $importCalendar->validate(); $this->assertEmpty($problems); - $this->migrator->importCalendar($user, $filename, $initialCalendarUri, $importCalendar); + $this->invokePrivate($this->migrator, 'importCalendar', [$user, $filename, $initialCalendarUri, $importCalendar]); - $calendarExports = $this->migrator->getCalendarExports($user); + $calendarExports = $this->invokePrivate($this->migrator, 'getCalendarExports', [$user]); $this->assertCount(1, $calendarExports); /** @var VCalendar $exportCalendar */ @@ -125,7 +125,7 @@ public function testImportExportAsset(string $userId, string $filename, string $ ); $this->assertEqualsCanonicalizing( - // Components are sanitized on import + // Components are expected to be sanitized on import $this->getSanitizedComponents($importCalendar), $this->getComponents($exportCalendar), ); From aafb305fed9a006b41456036d174a7f4c4de78af Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Fri, 25 Feb 2022 03:04:31 +0000 Subject: [PATCH 4/6] Minor updates Signed-off-by: Christopher Ng --- .../lib/UserMigration/CalendarMigrator.php | 132 ++++++++++-------- .../UserMigration/CalendarMigratorTest.php | 6 +- 2 files changed, 75 insertions(+), 63 deletions(-) diff --git a/apps/dav/lib/UserMigration/CalendarMigrator.php b/apps/dav/lib/UserMigration/CalendarMigrator.php index 6a36a7204e181..5fc7f3e02e086 100644 --- a/apps/dav/lib/UserMigration/CalendarMigrator.php +++ b/apps/dav/lib/UserMigration/CalendarMigrator.php @@ -27,6 +27,7 @@ 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; @@ -42,16 +43,15 @@ use OCP\UserMigration\IImportSource; use OCP\UserMigration\IMigrator; use OCP\UserMigration\TMigratorBasicVersionHandling; -use Sabre\DAV\Exception\BadRequest; -use Sabre\DAV\Version as SabreDavVersion; 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\FilesystemException; +use Safe\Exceptions\StringsException; use Symfony\Component\Console\Output\OutputInterface; +use Throwable; class CalendarMigrator implements IMigrator { @@ -76,7 +76,7 @@ class CalendarMigrator implements IMigrator { private const MIGRATED_URI_PREFIX = 'migrated-'; - private const EXPORT_ROOT = 'calendars/'; + private const EXPORT_ROOT = Application::APP_ID . '/calendars/'; public function __construct( CalDavBackend $calDavBackend, @@ -114,50 +114,54 @@ private function getCalendarExportData(IUser $user, ICalendar $calendar): array $calendarId = $calendar->getKey(); $calendarInfo = $this->calDavBackend->getCalendarById($calendarId); - if (!empty($calendarInfo)) { - $uri = $calendarInfo['uri']; - $path = CalDAVPlugin::CALENDAR_ROOT . "/$userId/$uri"; - - // NOTE implementation below based on \Sabre\CalDAV\ICSExportPlugin::httpGet() + if (empty($calendarInfo)) { + throw new CalendarMigratorException(); + } - $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', - ]); + $uri = $calendarInfo['uri']; + $path = CalDAVPlugin::CALENDAR_ROOT . "/$userId/$uri"; - // 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::httpGet() implementation reference + */ - // NOTE implementation below based on \Sabre\CalDAV\ICSExportPlugin::generateResponse() + $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', + ]); - $calDataProp = '{' . CalDAVPlugin::NS_CALDAV . '}calendar-data'; - $calendarNode = $this->sabreDavServer->tree->getNodeForPath($path); - $nodes = $this->sabreDavServer->getPropertiesIteratorForPath($path, [$calDataProp], 1); + // Filter out invalid (e.g. deleted) calendars + if (!isset($properties['{DAV:}resourcetype']) || !$properties['{DAV:}resourcetype']->is('{' . CalDAVPlugin::NS_CALDAV . '}calendar')) { + throw new InvalidCalendarException(); + } - $blobs = []; - foreach ($nodes as $node) { - if (isset($node[200][$calDataProp])) { - $blobs[$node['href']] = $node[200][$calDataProp]; - } - } + /** + * @see \Sabre\CalDAV\ICSExportPlugin::generateResponse() implementation reference + */ - $mergedCalendar = $this->icsExportPlugin->mergeObjects( - $properties, - $blobs, - ); + $calDataProp = '{' . CalDAVPlugin::NS_CALDAV . '}calendar-data'; + $calendarNode = $this->sabreDavServer->tree->getNodeForPath($path); + $nodes = $this->sabreDavServer->getPropertiesIteratorForPath($path, [$calDataProp], 1); - return [ - 'name' => $calendarNode->getName(), - 'vCalendar' => $mergedCalendar, - ]; + $blobs = []; + foreach ($nodes as $node) { + if (isset($node[200][$calDataProp])) { + $blobs[$node['href']] = $node[200][$calDataProp]; + } } - throw new CalendarMigratorException(); + $mergedCalendar = $this->icsExportPlugin->mergeObjects( + $properties, + $blobs, + ); + + return [ + 'name' => $calendarNode->getName(), + 'vCalendar' => $mergedCalendar, + ]; } /** @@ -176,6 +180,7 @@ function (ICalendar $calendar) use ($user) { throw new CalendarMigratorException(); } catch (InvalidCalendarException $e) { // Allow this exception as invalid (e.g. deleted) calendars are not to be exported + return null; } }, $this->calendarManager->getCalendarsForPrincipal($principalUri), @@ -184,9 +189,13 @@ function (ICalendar $calendar) use ($user) { private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): string { $principalUri = $this->getPrincipalUri($user); - $initialCalendarUri = substr($initialCalendarUri, 0, strlen(CalendarMigrator::MIGRATED_URI_PREFIX)) === CalendarMigrator::MIGRATED_URI_PREFIX - ? $initialCalendarUri - : CalendarMigrator::MIGRATED_URI_PREFIX . $initialCalendarUri; + 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(); + } $existingCalendarUris = array_map( fn (ICalendar $calendar) => $calendar->getUri(), @@ -207,14 +216,14 @@ private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): * {@inheritDoc} */ public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void { - $output->writeln("Exporting calendars…"); + $output->writeln('Exporting calendars…'); $userId = $user->getUID(); try { $calendarExports = $this->getCalendarExports($user); } catch (CalendarMigratorException $e) { - $output->writeln("Error exporting <$userId> calendars"); + throw new CalendarMigratorException(); } if (empty($calendarExports)) { @@ -240,10 +249,10 @@ public function export(IUser $user, IExportDestination $exportDestination, Outpu */ private function getCalendarTimezones(VCalendar $vCalendar): array { /** @var VTimeZone[] $calendarTimezones */ - $calendarTimezones = array_values(array_filter( + $calendarTimezones = array_filter( $vCalendar->getComponents(), fn ($component) => $component->name === 'VTIMEZONE', - )); + ); /** @var array $calendarTimezoneMap */ $calendarTimezoneMap = []; @@ -279,10 +288,10 @@ private function getTimezonesForComponent(VCalendar $vCalendar, VObjectComponent private function sanitizeComponent(VObjectComponent $component): VObjectComponent { // Operate on the component clone to prevent mutation of the original - $componentClone = clone $component; + $component = clone $component; // Remove RSVP parameters to prevent automatically sending invitation emails to attendees on import - foreach ($componentClone->children() as $child) { + foreach ($component->children() as $child) { if ( $child->name === 'ATTENDEE' && isset($child->parameters['RSVP']) @@ -291,7 +300,7 @@ private function sanitizeComponent(VObjectComponent $component): VObjectComponen } } - return $componentClone; + return $component; } /** @@ -309,13 +318,11 @@ private function getRequiredImportComponents(VCalendar $vCalendar, VObjectCompon private function initCalendarObject(): VCalendar { $vCalendarObject = new VCalendar(); - $vCalendarObject->PRODID = $this->sabreDavServer::$exposeVersion - ? '-//SabreDAV//SabreDAV ' . SabreDavVersion::VERSION . '//EN' - : '-//SabreDAV//SabreDAV//EN'; + $vCalendarObject->PRODID = '-//IDN nextcloud.com//Migrated calendar//EN'; return $vCalendarObject; } - private function importCalendarObject(int $calendarId, VCalendar $vCalendarObject): void { + private function importCalendarObject(int $calendarId, VCalendar $vCalendarObject, OutputInterface $output): void { try { $this->calDavBackend->createCalendarObject( $calendarId, @@ -323,8 +330,9 @@ private function importCalendarObject(int $calendarId, VCalendar $vCalendarObjec $vCalendarObject->serialize(), CalDavBackend::CALENDAR_TYPE_CALENDAR, ); - } catch (BadRequest $e) { + } 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); } } @@ -332,7 +340,7 @@ private function importCalendarObject(int $calendarId, VCalendar $vCalendarObjec /** * @throws CalendarMigratorException */ - private function importCalendar(IUser $user, string $filename, string $initialCalendarUri, VCalendar $vCalendar): void { + private function importCalendar(IUser $user, string $filename, string $initialCalendarUri, VCalendar $vCalendar, OutputInterface $output): void { $principalUri = $this->getPrincipalUri($user); $calendarUri = $this->getUniqueCalendarUri($user, $initialCalendarUri); @@ -388,7 +396,7 @@ function (array $componentNames, VObjectComponent $component) { $vCalendarObject->add($component); } } - $this->importCalendarObject($calendarId, $vCalendarObject); + $this->importCalendarObject($calendarId, $vCalendarObject, $output); } foreach ($ungroupedCalendarComponents as $component) { @@ -397,14 +405,13 @@ function (array $componentNames, VObjectComponent $component) { foreach ($this->getRequiredImportComponents($vCalendar, $component) as $component) { $vCalendarObject->add($component); } - $this->importCalendarObject($calendarId, $vCalendarObject); + $this->importCalendarObject($calendarId, $vCalendarObject, $output); } } /** * {@inheritDoc} * - * @throws FilesystemException * @throws CalendarMigratorException */ public function import(IUser $user, IImportSource $importSource, OutputInterface $output): void { @@ -413,7 +420,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface return; } - $output->writeln("Importing calendars…"); + $output->writeln('Importing calendars…'); foreach ($importSource->getFolderListing(CalendarMigrator::EXPORT_ROOT) as $filename) { try { @@ -422,15 +429,15 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface $importSource->getFileAsStream(CalendarMigrator::EXPORT_ROOT . $filename), VObjectReader::OPTION_FORGIVING, ); - } catch (FilesystemException $e) { - throw new FilesystemException("Failed to read file: \"$filename\""); + } catch (Throwable $e) { + throw new CalendarMigratorException(); } $problems = $vCalendar->validate(); if (empty($problems)) { $splitFilename = explode('_', $filename, 2); if (count($splitFilename) !== 2) { - $output->writeln("Invalid filename, expected filename of the format: \"_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . "\""); + $output->writeln("Invalid filename: \"$filename\" expected filename of the format: \"_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . "\""); throw new CalendarMigratorException(); } [$initialCalendarUri, $suffix] = $splitFilename; @@ -440,6 +447,7 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface $filename, $initialCalendarUri, $vCalendar, + $output, ); $vCalendar->destroy(); diff --git a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php index e2eb0c75d9bc0..d1bac3642dd57 100644 --- a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php +++ b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php @@ -36,6 +36,7 @@ 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; /** @@ -47,6 +48,8 @@ class CalendarMigratorTest extends TestCase { private CalendarMigrator $migrator; + private OutputInterface $output; + private const ASSETS_DIR = __DIR__ . '/assets/'; protected function setUp(): void { @@ -55,6 +58,7 @@ protected function setUp(): void { $this->userManager = $container->get(IUserManager::class); $this->migrator = $container->get(CalendarMigrator::class); + $this->output = $this->createMock(OutputInterface::class); } public function dataAssets(): array { @@ -111,7 +115,7 @@ public function testImportExportAsset(string $userId, string $filename, string $ $problems = $importCalendar->validate(); $this->assertEmpty($problems); - $this->invokePrivate($this->migrator, 'importCalendar', [$user, $filename, $initialCalendarUri, $importCalendar]); + $this->invokePrivate($this->migrator, 'importCalendar', [$user, $filename, $initialCalendarUri, $importCalendar, $this->output]); $calendarExports = $this->invokePrivate($this->migrator, 'getCalendarExports', [$user]); $this->assertCount(1, $calendarExports); From 0cbb6d7ba9b5d3b2c79f2cf0bafe6145da0857f6 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Wed, 2 Mar 2022 01:56:18 +0000 Subject: [PATCH 5/6] Various minor updates Signed-off-by: Christopher Ng --- apps/dav/lib/CalDAV/CalendarImpl.php | 2 +- .../lib/UserMigration/CalendarMigrator.php | 66 ++++++++----------- lib/public/Calendar/ICalendar.php | 2 +- 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php index 87bed32428f7d..406389e3a3dcc 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -72,7 +72,7 @@ public function getKey() { /** * {@inheritDoc} */ - public function getUri() { + public function getUri(): string { return $this->calendarInfo['uri']; } diff --git a/apps/dav/lib/UserMigration/CalendarMigrator.php b/apps/dav/lib/UserMigration/CalendarMigrator.php index 5fc7f3e02e086..bc5cfe3521838 100644 --- a/apps/dav/lib/UserMigration/CalendarMigrator.php +++ b/apps/dav/lib/UserMigration/CalendarMigrator.php @@ -91,9 +91,6 @@ public function __construct( $this->defaults = $defaults; $this->l10n = $l10n; - // Override trait property - $this->mandatory = true; - $root = new RootCollection(); $this->sabreDavServer = new SabreDavServer(new CachingTree($root)); $this->sabreDavServer->addPlugin(new CalDAVPlugin()); @@ -115,7 +112,7 @@ private function getCalendarExportData(IUser $user, ICalendar $calendar): array $calendarInfo = $this->calDavBackend->getCalendarById($calendarId); if (empty($calendarInfo)) { - throw new CalendarMigratorException(); + throw new CalendarMigratorException("Invalid info for calendar ID: $calendarId"); } $uri = $calendarInfo['uri']; @@ -176,8 +173,6 @@ private function getCalendarExports(IUser $user): array { function (ICalendar $calendar) use ($user) { try { return $this->getCalendarExportData($user, $calendar); - } catch (CalendarMigratorException $e) { - throw new CalendarMigratorException(); } catch (InvalidCalendarException $e) { // Allow this exception as invalid (e.g. deleted) calendars are not to be exported return null; @@ -194,7 +189,7 @@ private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): ? $initialCalendarUri : CalendarMigrator::MIGRATED_URI_PREFIX . $initialCalendarUri; } catch (StringsException $e) { - throw new CalendarMigratorException(); + throw new CalendarMigratorException('Failed to get unique calendar URI', 0, $e); } $existingCalendarUris = array_map( @@ -216,18 +211,14 @@ private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): * {@inheritDoc} */ public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void { - $output->writeln('Exporting calendars…'); + $output->writeln('Exporting calendars into ' . CalendarMigrator::EXPORT_ROOT . '…'); $userId = $user->getUID(); - try { - $calendarExports = $this->getCalendarExports($user); - } catch (CalendarMigratorException $e) { - throw new CalendarMigratorException(); - } + $calendarExports = $this->getCalendarExports($user); if (empty($calendarExports)) { - $output->writeln("User <$userId> has no calendars to export"); + $output->writeln("User <$userId> has no calendars to export"); } /** @@ -237,9 +228,10 @@ public function export(IUser $user, IExportDestination $exportDestination, Outpu 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(CalendarMigrator::EXPORT_ROOT . $filename, $vCalendar->serialize()) === false) { - throw new CalendarMigratorException(); + if ($exportDestination->addFileContents($exportPath, $vCalendar->serialize()) === false) { + throw new CalendarMigratorException('Could not export calendars'); } } } @@ -420,40 +412,40 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface return; } - $output->writeln('Importing calendars…'); + $output->writeln('Importing calendars from ' . CalendarMigrator::EXPORT_ROOT . '…'); foreach ($importSource->getFolderListing(CalendarMigrator::EXPORT_ROOT) as $filename) { + $importPath = CalendarMigrator::EXPORT_ROOT . $filename; try { /** @var VCalendar $vCalendar */ $vCalendar = VObjectReader::read( - $importSource->getFileAsStream(CalendarMigrator::EXPORT_ROOT . $filename), + $importSource->getFileAsStream($importPath), VObjectReader::OPTION_FORGIVING, ); } catch (Throwable $e) { - throw new CalendarMigratorException(); + throw new CalendarMigratorException("Failed to read file: \"$importPath\"", 0, $e); } $problems = $vCalendar->validate(); - if (empty($problems)) { - $splitFilename = explode('_', $filename, 2); - if (count($splitFilename) !== 2) { - $output->writeln("Invalid filename: \"$filename\" expected filename of the format: \"_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . "\""); - throw new CalendarMigratorException(); - } - [$initialCalendarUri, $suffix] = $splitFilename; - - $this->importCalendar( - $user, - $filename, - $initialCalendarUri, - $vCalendar, - $output, - ); + if (!empty($problems)) { + throw new CalendarMigratorException("Invalid calendar data contained in: \"$importPath\""); + } - $vCalendar->destroy(); - } else { - throw new CalendarMigratorException("Invalid data contained in \"$filename\""); + $splitFilename = explode('_', $filename, 2); + if (count($splitFilename) !== 2) { + throw new CalendarMigratorException("Invalid filename: \"$filename\", expected filename of the format: \"_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . '"'); } + [$initialCalendarUri, $suffix] = $splitFilename; + + $this->importCalendar( + $user, + $filename, + $initialCalendarUri, + $vCalendar, + $output, + ); + + $vCalendar->destroy(); } } } diff --git a/lib/public/Calendar/ICalendar.php b/lib/public/Calendar/ICalendar.php index f2c94cb5400d6..0d08e2ba268aa 100644 --- a/lib/public/Calendar/ICalendar.php +++ b/lib/public/Calendar/ICalendar.php @@ -42,7 +42,7 @@ public function getKey(); /** * @since 24.0.0 */ - public function getUri(); + public function getUri(): string; /** * In comparison to getKey() this function returns a human readable (maybe translated) name From e97701c6b0509de26dbad86d0d3569be331cb94b Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Wed, 9 Mar 2022 18:58:23 +0000 Subject: [PATCH 6/6] Improve error handling and output messages Signed-off-by: Christopher Ng --- .../lib/UserMigration/CalendarMigrator.php | 35 ++++++++++++------- .../UserMigration/CalendarMigratorTest.php | 2 +- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/apps/dav/lib/UserMigration/CalendarMigrator.php b/apps/dav/lib/UserMigration/CalendarMigrator.php index bc5cfe3521838..d0d9e94351b4f 100644 --- a/apps/dav/lib/UserMigration/CalendarMigrator.php +++ b/apps/dav/lib/UserMigration/CalendarMigrator.php @@ -106,13 +106,13 @@ private function getPrincipalUri(IUser $user): string { * @throws CalendarMigratorException * @throws InvalidCalendarException */ - private function getCalendarExportData(IUser $user, ICalendar $calendar): array { + 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"); + throw new CalendarMigratorException("Invalid info for calendar ID $calendarId"); } $uri = $calendarInfo['uri']; @@ -155,6 +155,12 @@ private function getCalendarExportData(IUser $user, ICalendar $calendar): array $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, @@ -166,13 +172,13 @@ private function getCalendarExportData(IUser $user, ICalendar $calendar): array * * @throws CalendarMigratorException */ - private function getCalendarExports(IUser $user): array { + private function getCalendarExports(IUser $user, OutputInterface $output): array { $principalUri = $this->getPrincipalUri($user); return array_values(array_filter(array_map( - function (ICalendar $calendar) use ($user) { + function (ICalendar $calendar) use ($user, $output) { try { - return $this->getCalendarExportData($user, $calendar); + 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; @@ -213,12 +219,10 @@ private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void { $output->writeln('Exporting calendars into ' . CalendarMigrator::EXPORT_ROOT . '…'); - $userId = $user->getUID(); - - $calendarExports = $this->getCalendarExports($user); + $calendarExports = $this->getCalendarExports($user, $output); if (empty($calendarExports)) { - $output->writeln("User <$userId> has no calendars to export"); + $output->writeln('No calendars to export…'); } /** @@ -414,7 +418,12 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface $output->writeln('Importing calendars from ' . CalendarMigrator::EXPORT_ROOT . '…'); - foreach ($importSource->getFolderListing(CalendarMigrator::EXPORT_ROOT) as $filename) { + $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 */ @@ -423,17 +432,17 @@ public function import(IUser $user, IImportSource $importSource, OutputInterface VObjectReader::OPTION_FORGIVING, ); } catch (Throwable $e) { - throw new CalendarMigratorException("Failed to read file: \"$importPath\"", 0, $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\""); + 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: \"_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . '"'); + throw new CalendarMigratorException("Invalid filename \"$filename\", expected filename of the format \"_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . '"'); } [$initialCalendarUri, $suffix] = $splitFilename; diff --git a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php index d1bac3642dd57..f1ad6dda22e28 100644 --- a/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php +++ b/apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php @@ -117,7 +117,7 @@ public function testImportExportAsset(string $userId, string $filename, string $ $this->invokePrivate($this->migrator, 'importCalendar', [$user, $filename, $initialCalendarUri, $importCalendar, $this->output]); - $calendarExports = $this->invokePrivate($this->migrator, 'getCalendarExports', [$user]); + $calendarExports = $this->invokePrivate($this->migrator, 'getCalendarExports', [$user, $this->output]); $this->assertCount(1, $calendarExports); /** @var VCalendar $exportCalendar */