Skip to content

Commit

Permalink
feat: OCC and OCS Calendar Import/Export
Browse files Browse the repository at this point in the history
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
  • Loading branch information
SebastianKrupinski committed Jan 27, 2025
1 parent 6dc83b9 commit 67dc029
Show file tree
Hide file tree
Showing 20 changed files with 1,450 additions and 1 deletion.
2 changes: 2 additions & 0 deletions apps/dav/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
<command>OCA\DAV\Command\SyncBirthdayCalendar</command>
<command>OCA\DAV\Command\SyncSystemAddressBook</command>
<command>OCA\DAV\Command\RemoveInvalidShares</command>
<command>OCA\DAV\Command\ImportCalendar</command>
<command>OCA\DAV\Command\ExportCalendar</command>
</commands>

<settings>
Expand Down
8 changes: 8 additions & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@
'OCA\\DAV\\CalDAV\\EventReader' => $baseDir . '/../lib/CalDAV/EventReader.php',
'OCA\\DAV\\CalDAV\\EventReaderRDate' => $baseDir . '/../lib/CalDAV/EventReaderRDate.php',
'OCA\\DAV\\CalDAV\\EventReaderRRule' => $baseDir . '/../lib/CalDAV/EventReaderRRule.php',
'OCA\\DAV\\CalDAV\\Export\\ExportService' => $baseDir . '/../lib/CalDAV/Export/ExportService.php',
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => $baseDir . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php',
'OCA\\DAV\\CalDAV\\Import\\ImportService' => $baseDir . '/../lib/CalDAV/Import/ImportService.php',
'OCA\\DAV\\CalDAV\\Import\\TextImporter' => $baseDir . '/../lib/CalDAV/Import/TextImporter.php',
'OCA\\DAV\\CalDAV\\Import\\XmlImporter' => $baseDir . '/../lib/CalDAV/Import/XmlImporter.php',
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => $baseDir . '/../lib/CalDAV/Integration/ExternalCalendar.php',
'OCA\\DAV\\CalDAV\\Integration\\ICalendarProvider' => $baseDir . '/../lib/CalDAV/Integration/ICalendarProvider.php',
'OCA\\DAV\\CalDAV\\InvitationResponse\\InvitationResponseServer' => $baseDir . '/../lib/CalDAV/InvitationResponse/InvitationResponseServer.php',
Expand Down Expand Up @@ -157,7 +161,9 @@
'OCA\\DAV\\Command\\CreateCalendar' => $baseDir . '/../lib/Command/CreateCalendar.php',
'OCA\\DAV\\Command\\CreateSubscription' => $baseDir . '/../lib/Command/CreateSubscription.php',
'OCA\\DAV\\Command\\DeleteCalendar' => $baseDir . '/../lib/Command/DeleteCalendar.php',
'OCA\\DAV\\Command\\ExportCalendar' => $baseDir . '/../lib/Command/ExportCalendar.php',
'OCA\\DAV\\Command\\FixCalendarSyncCommand' => $baseDir . '/../lib/Command/FixCalendarSyncCommand.php',
'OCA\\DAV\\Command\\ImportCalendar' => $baseDir . '/../lib/Command/ImportCalendar.php',
'OCA\\DAV\\Command\\ListAddressbooks' => $baseDir . '/../lib/Command/ListAddressbooks.php',
'OCA\\DAV\\Command\\ListCalendars' => $baseDir . '/../lib/Command/ListCalendars.php',
'OCA\\DAV\\Command\\MoveCalendar' => $baseDir . '/../lib/Command/MoveCalendar.php',
Expand Down Expand Up @@ -218,6 +224,8 @@
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => $baseDir . '/../lib/Connector/Sabre/TagsPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\ZipFolderPlugin' => $baseDir . '/../lib/Connector/Sabre/ZipFolderPlugin.php',
'OCA\\DAV\\Controller\\BirthdayCalendarController' => $baseDir . '/../lib/Controller/BirthdayCalendarController.php',
'OCA\\DAV\\Controller\\CalendarExportController' => $baseDir . '/../lib/Controller/CalendarExportController.php',
'OCA\\DAV\\Controller\\CalendarImportController' => $baseDir . '/../lib/Controller/CalendarImportController.php',
'OCA\\DAV\\Controller\\DirectController' => $baseDir . '/../lib/Controller/DirectController.php',
'OCA\\DAV\\Controller\\InvitationResponseController' => $baseDir . '/../lib/Controller/InvitationResponseController.php',
'OCA\\DAV\\Controller\\OutOfOfficeController' => $baseDir . '/../lib/Controller/OutOfOfficeController.php',
Expand Down
8 changes: 8 additions & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,13 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\EventReader' => __DIR__ . '/..' . '/../lib/CalDAV/EventReader.php',
'OCA\\DAV\\CalDAV\\EventReaderRDate' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRDate.php',
'OCA\\DAV\\CalDAV\\EventReaderRRule' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRRule.php',
'OCA\\DAV\\CalDAV\\Export\\ExportService' => __DIR__ . '/..' . '/../lib/CalDAV/Export/ExportService.php',
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => __DIR__ . '/..' . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php',
'OCA\\DAV\\CalDAV\\Import\\ImportService' => __DIR__ . '/..' . '/../lib/CalDAV/Import/ImportService.php',
'OCA\\DAV\\CalDAV\\Import\\TextImporter' => __DIR__ . '/..' . '/../lib/CalDAV/Import/TextImporter.php',
'OCA\\DAV\\CalDAV\\Import\\XmlImporter' => __DIR__ . '/..' . '/../lib/CalDAV/Import/XmlImporter.php',
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ExternalCalendar.php',
'OCA\\DAV\\CalDAV\\Integration\\ICalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ICalendarProvider.php',
'OCA\\DAV\\CalDAV\\InvitationResponse\\InvitationResponseServer' => __DIR__ . '/..' . '/../lib/CalDAV/InvitationResponse/InvitationResponseServer.php',
Expand Down Expand Up @@ -172,7 +176,9 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Command\\CreateCalendar' => __DIR__ . '/..' . '/../lib/Command/CreateCalendar.php',
'OCA\\DAV\\Command\\CreateSubscription' => __DIR__ . '/..' . '/../lib/Command/CreateSubscription.php',
'OCA\\DAV\\Command\\DeleteCalendar' => __DIR__ . '/..' . '/../lib/Command/DeleteCalendar.php',
'OCA\\DAV\\Command\\ExportCalendar' => __DIR__ . '/..' . '/../lib/Command/ExportCalendar.php',
'OCA\\DAV\\Command\\FixCalendarSyncCommand' => __DIR__ . '/..' . '/../lib/Command/FixCalendarSyncCommand.php',
'OCA\\DAV\\Command\\ImportCalendar' => __DIR__ . '/..' . '/../lib/Command/ImportCalendar.php',
'OCA\\DAV\\Command\\ListAddressbooks' => __DIR__ . '/..' . '/../lib/Command/ListAddressbooks.php',
'OCA\\DAV\\Command\\ListCalendars' => __DIR__ . '/..' . '/../lib/Command/ListCalendars.php',
'OCA\\DAV\\Command\\MoveCalendar' => __DIR__ . '/..' . '/../lib/Command/MoveCalendar.php',
Expand Down Expand Up @@ -233,6 +239,8 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Connector\\Sabre\\TagsPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/TagsPlugin.php',
'OCA\\DAV\\Connector\\Sabre\\ZipFolderPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ZipFolderPlugin.php',
'OCA\\DAV\\Controller\\BirthdayCalendarController' => __DIR__ . '/..' . '/../lib/Controller/BirthdayCalendarController.php',
'OCA\\DAV\\Controller\\CalendarExportController' => __DIR__ . '/..' . '/../lib/Controller/CalendarExportController.php',
'OCA\\DAV\\Controller\\CalendarImportController' => __DIR__ . '/..' . '/../lib/Controller/CalendarImportController.php',
'OCA\\DAV\\Controller\\DirectController' => __DIR__ . '/..' . '/../lib/Controller/DirectController.php',
'OCA\\DAV\\Controller\\InvitationResponseController' => __DIR__ . '/..' . '/../lib/Controller/InvitationResponseController.php',
'OCA\\DAV\\Controller\\OutOfOfficeController' => __DIR__ . '/..' . '/../lib/Controller/OutOfOfficeController.php',
Expand Down
31 changes: 31 additions & 0 deletions apps/dav/lib/CalDAV/CalDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use Generator;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\CalDAV\Sharing\Backend;
use OCA\DAV\Connector\Sabre\Principal;
Expand All @@ -34,6 +35,7 @@
use OCA\DAV\Events\SubscriptionDeletedEvent;
use OCA\DAV\Events\SubscriptionUpdatedEvent;
use OCP\AppFramework\Db\TTransactional;
use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
Expand Down Expand Up @@ -950,6 +952,35 @@ public function restoreCalendar(int $id): void {
}, $this->db);
}

/**
* Returns all calendar entries as a stream of data
*
* @since 32.0.0
*
* @return Generator<array>
*/
public function exportCalendar(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR, ?CalendarExportOptions $options = null): Generator {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('calendarobjects')
->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
->andWhere($qb->expr()->isNull('deleted_at'));
if ($options?->rangeStart !== null) {
$qb->setFirstResult($options?->rangeStart);
}
if ($options?->rangeCount !== null) {
$qb->setMaxResults($options->rangeCount);
}
$rs = $qb->executeQuery();

while (($row = $rs->fetch()) !== false) {
yield $row;
}

$rs->closeCursor();
}

/**
* Returns all calendar objects with limited metadata for a calendar
*
Expand Down
123 changes: 122 additions & 1 deletion apps/dav/lib/CalDAV/CalendarImpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@
*/
namespace OCA\DAV\CalDAV;

use Generator;
use InvalidArgumentException;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\CalendarImportOptions;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICalendarExport;
use OCP\Calendar\ICalendarImport;
use OCP\Calendar\ICalendarIsShared;
use OCP\Calendar\ICalendarIsWritable;
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IHandleImipMessage;
use OCP\Constants;
Expand All @@ -20,11 +28,14 @@
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Node;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use Sabre\VObject\UUIDUtil;

use function Sabre\Uri\split as uriSplit;

class CalendarImpl implements ICreateFromString, IHandleImipMessage {
class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIsWritable, ICalendarIsShared, ICalendarImport, ICalendarExport {
public function __construct(
private Calendar $calendar,
/** @var array<string, mixed> */
Expand Down Expand Up @@ -257,4 +268,114 @@ public function handleIMipMessage(string $name, string $calendarData): void {
public function getInvitationResponseServer(): InvitationResponseServer {
return new InvitationResponseServer(false);
}

/**
* Export objects
*
* @since 32.0.0
*
* @return Generator<\Sabre\VObject\Component\VCalendar>

Check failure on line 277 in apps/dav/lib/CalDAV/CalendarImpl.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

MoreSpecificReturnType

apps/dav/lib/CalDAV/CalendarImpl.php:277:13: MoreSpecificReturnType: The declared return type 'Generator<mixed, Sabre\VObject\Component\VCalendar, mixed, mixed>' for OCA\DAV\CalDAV\CalendarImpl::export is more specific than the inferred return type 'Generator<int, Sabre\VObject\Document, mixed, void>' (see https://psalm.dev/070)
*/
public function export(?CalendarExportOptions $options = null): Generator {
foreach (
$this->backend->exportCalendar(
$this->calendarInfo['id'],
$this->backend::CALENDAR_TYPE_CALENDAR,
$options
) as $event
) {
yield Reader::read($event['calendardata']);
}
}

/**
* Import objects
*
* @since 32.0.0
*
*/
public function import(CalendarImportOptions $options, callable $generator): array {

Check failure on line 297 in apps/dav/lib/CalDAV/CalendarImpl.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidReturnType

apps/dav/lib/CalDAV/CalendarImpl.php:297:79: InvalidReturnType: The declared return type 'array<string, array<string, string>>' for OCA\DAV\CalDAV\CalendarImpl::import is incorrect, got 'array<array-key, array{errors?: non-empty-array<array-key, mixed>, outcome: 'created'|'error'|'exists'|'updated'}>' (see https://psalm.dev/011)

$calendarId = $this->getKey();
$outcome = [];
foreach ($generator($options) as $vObject) {

$components = $vObject->getBaseComponents();
// determine if the object has only one base component type
if (count($components) > 1) {
if ($options->errors === 1) {
throw new InvalidArgumentException('Error importing calendar object, discovered object with multiple base component types');
}
$outcome['mbct'] = ['outcome' => 'error', 'errors' => ['One or more objects discovered with multiple base component types']];
continue;
}
// determine if the object has a uid
if (!isset($components[0]->UID)) {
if ($options->errors === 1) {
throw new InvalidArgumentException('Error importing calendar object, discovered object without a UID');
}
$outcome['noid'] = ['outcome' => 'error', 'errors' => ['One or more objects discovered without a UID']];
continue;
}
$uid = $components[0]->UID->getValue();
// validate object
if ($options->validate !== 0) {
$issues = $this->validateComponent($vObject, true, 3);
if ($options->validate === 1 && $issues !== []) {
$outcome[$uid] = ['outcome' => 'error', 'errors' => $issues];
continue;
} elseif ($options->validate === 2 && $issues !== []) {
throw new InvalidArgumentException('Error importing calendar object <' . $uid . '>, ' . $issues[0]);
}
}

$objectId = $this->backend->getCalendarObjectByUID($this->calendarInfo['principaluri'], $uid);
$objectData = $vObject->serialize();

// create or update object
if ($objectId === null) {
$objectId = UUIDUtil::getUUID();
$this->backend->createCalendarObject(
$calendarId,
$objectId,
$objectData
);
$outcome[$uid] = ['outcome' => 'created'];
} elseif ($objectId !== null) {
[$cid, $oid] = explode('/', $objectId);
if ($options->supersede) {
$this->backend->updateCalendarObject(
$calendarId,
$oid,
$objectData
);
$outcome[$uid] = ['outcome' => 'updated'];
} else {
$outcome[$uid] = ['outcome' => 'exists'];
}
}
}

return $outcome;

Check failure on line 359 in apps/dav/lib/CalDAV/CalendarImpl.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

InvalidReturnStatement

apps/dav/lib/CalDAV/CalendarImpl.php:359:10: InvalidReturnStatement: The inferred type 'array<array-key, array{errors?: non-empty-array<array-key, mixed>, outcome: 'created'|'error'|'exists'|'updated'}>' does not match the declared return type 'array<string, array<string, string>>' for OCA\DAV\CalDAV\CalendarImpl::import (see https://psalm.dev/128)

}

private function validateComponent(VCalendar $vObject, bool $repair, int $level): array {
// validate component(S)
$issues = $vObject->validate(Node::PROFILE_CALDAV);
// attempt to repair
if ($repair && count($issues) > 0) {
$issues = $vObject->validate(Node::REPAIR);
}
// filter out messages based on level
$result = [];
foreach ($issues as $key => $issue) {
if (isset($issue['level']) && $issue['level'] >= $level) {
$result[] = $issue['message'];
}
}

return $result;
}

}
118 changes: 118 additions & 0 deletions apps/dav/lib/CalDAV/Export/ExportService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Export;

use Generator;
use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\ICalendar;
use OCP\Calendar\ICalendarExport;
use Sabre\VObject\Component;
use Sabre\VObject\Writer;

/**
* Calendar Export Service
*
* @since 32.0.0
*/
class ExportService {

public const FORMATS = ['ical', 'jcal', 'xcal'];

public function __construct() {
}

/**
* Generates serialized content stream for a calendar and objects based in selected format
*
* @since 32.0.0
*/
public function export(ICalendar&ICalendarExport $calendar, CalendarExportOptions $options): Generator {

yield $this->exportStart($options->format);

// iterate through each returned vCalendar entry
// extract each component except timezones, convert to appropriate format and output
// extract any timezones and save them but do not output
$timezones = [];
foreach ($calendar->export($options) as $entry) {
$consecutive = false;
foreach ($entry->getComponents() as $vComponent) {
if ($vComponent->name === 'VTIMEZONE') {
if (isset($vComponent->TZID) && !isset($timezones[$vComponent->TZID->getValue()])) {
$timezones[$vComponent->TZID->getValue()] = clone $vComponent;
}
} else {
yield $this->exportObject($vComponent, $options->format, $consecutive);
$consecutive = true;
}
}
}
// iterate through each vTimezone entry, convert to appropriate format and output
foreach ($timezones as $vComponent) {
yield $this->exportObject($vComponent, $options->format, $consecutive);
$consecutive = true;
}

yield $this->exportFinish($options->format);

}

/**
* Generates serialized content start based on selected format
*
* @since 32.0.0
*/
private function exportStart(string $format): string {
return match ($format) {
'jcal' => '["vcalendar",[["version",{},"text","2.0"],["prodid",{},"text","-\/\/IDN nextcloud.com\/\/Calendar App\/\/EN"]],[',
'xcal' => '<?xml version="1.0" encoding="UTF-8"?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><vcalendar><properties><version><text>2.0</text></version><prodid><text>-//IDN nextcloud.com//Calendar App//EN</text></prodid></properties><components>',
default => "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//IDN nextcloud.com//Calendar App//EN\n"
};
}

/**
* Generates serialized content end based on selected format
*
* @since 32.0.0
*/
private function exportFinish(string $format): string {
return match ($format) {
'jcal' => ']]',
'xcal' => '</components></vcalendar></icalendar>',
default => "END:VCALENDAR\n"
};
}

/**
* Generates serialized content for a component based on selected format
*
* @since 32.0.0
*/
private function exportObject(Component $vobject, string $format, bool $consecutive): string {
return match ($format) {
'jcal' => $consecutive ? ',' . Writer::writeJson($vobject) : Writer::writeJson($vobject),
'xcal' => $this->exportObjectXml($vobject),
default => Writer::write($vobject)
};
}

/**
* Generates serialized content for a component in xml format
*
* @since 32.0.0
*/
private function exportObjectXml(Component $vobject): string {
$writer = new \Sabre\Xml\Writer();
$writer->openMemory();
$writer->setIndent(false);
$vobject->xmlSerialize($writer);
return $writer->outputMemory();
}

}
Loading

0 comments on commit 67dc029

Please sign in to comment.