namespace OCA\DAV\UserMigration;
use function Safe\substr;
+use OCA\DAV\AppInfo\Application;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin;
use OCA\DAV\CalDAV\Plugin as CalDAVPlugin;
use 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 {
private const MIGRATED_URI_PREFIX = 'migrated-';
- private const EXPORT_ROOT = 'calendars/';
+ private const EXPORT_ROOT = Application::APP_ID . '/calendars/';
public function __construct(
CalDavBackend $calDavBackend,
$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,
+ ];
}
/**
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),
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(),
* {@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>Error exporting <$userId> calendars</error>");
+ throw new CalendarMigratorException();
}
if (empty($calendarExports)) {
*/
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<string, VTimeZone> $calendarTimezoneMap */
$calendarTimezoneMap = [];
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'])
}
}
- return $componentClone;
+ return $component;
}
/**
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,
$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);
}
}
/**
* @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);
$vCalendarObject->add($component);
}
}
- $this->importCalendarObject($calendarId, $vCalendarObject);
+ $this->importCalendarObject($calendarId, $vCalendarObject, $output);
}
foreach ($ungroupedCalendarComponents as $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 {
return;
}
- $output->writeln("Importing calendars…");
+ $output->writeln('Importing calendars…');
foreach ($importSource->getFolderListing(CalendarMigrator::EXPORT_ROOT) as $filename) {
try {
$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("<error>Invalid filename, expected filename of the format: \"<calendar_name>_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . "\"</error>");
+ $output->writeln("<error>Invalid filename: \"$filename\" expected filename of the format: \"<calendar_name>_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . "\"</error>");
throw new CalendarMigratorException();
}
[$initialCalendarUri, $suffix] = $splitFilename;
$filename,
$initialCalendarUri,
$vCalendar,
+ $output,
);
$vCalendar->destroy();