From: Christopher Ng Date: Tue, 8 Feb 2022 06:54:07 +0000 (+0000) Subject: Calendar export and import X-Git-Tag: v24.0.0beta1~89^2~5 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=4eff3594416481dbbf34057f53072d92119ababd;p=nextcloud-server.git Calendar export and import Signed-off-by: Christopher Ng --- diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 8462ed1816d..88c4ee03ac3 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 9cfefaed1c8..bf0fe9cbf75 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -124,6 +124,8 @@ return array( '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 @@ return array( 'OCA\\DAV\\Upload\\UploadFile' => $baseDir . '/../lib/Upload/UploadFile.php', 'OCA\\DAV\\Upload\\UploadFolder' => $baseDir . '/../lib/Upload/UploadFolder.php', 'OCA\\DAV\\Upload\\UploadHome' => $baseDir . '/../lib/Upload/UploadHome.php', + 'OCA\\DAV\\UserMigration\\CalendarMigrator' => $baseDir . '/../lib/UserMigration/CalendarMigrator.php', + 'OCA\\DAV\\UserMigration\\CalendarMigratorException' => $baseDir . '/../lib/UserMigration/CalendarMigratorException.php', + 'OCA\\DAV\\UserMigration\\InvalidCalendarException' => $baseDir . '/../lib/UserMigration/InvalidCalendarException.php', ); diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 01c24fe38c8..18bfd642960 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 1c36db68cca..87bed32428f 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -69,6 +69,13 @@ class CalendarImpl implements ICreateFromString { 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 00000000000..770d6edc425 --- /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 00000000000..193ecc3b29b --- /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 00000000000..c1252d7517a --- /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 00000000000..91bac58ffac --- /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 00000000000..0e42ef1bc20 --- /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 551870de20e..f2c94cb5400 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