diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml
index a438a4dd06346..d2a4261fd3523 100644
--- a/apps/dav/appinfo/info.xml
+++ b/apps/dav/appinfo/info.xml
@@ -65,6 +65,8 @@
OCA\DAV\Command\SyncBirthdayCalendar
OCA\DAV\Command\SyncSystemAddressBook
OCA\DAV\Command\RemoveInvalidShares
+ OCA\DAV\Command\ImportCalendar
+ OCA\DAV\Command\ExportCalendar
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index a70aba9f84218..526e37eee14fd 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -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',
@@ -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',
@@ -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',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index 2d00105b54892..a65d20b2fcb5d 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -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',
@@ -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',
@@ -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',
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php
index 0c8b52a7491c7..d2606bb140d33 100644
--- a/apps/dav/lib/CalDAV/CalDavBackend.php
+++ b/apps/dav/lib/CalDAV/CalDavBackend.php
@@ -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;
@@ -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;
@@ -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
+ */
+ 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
*
diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php
index b3062f005ee27..d45f7d59ec907 100644
--- a/apps/dav/lib/CalDAV/CalendarImpl.php
+++ b/apps/dav/lib/CalDAV/CalendarImpl.php
@@ -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;
@@ -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 */
@@ -257,4 +268,123 @@ public function handleIMipMessage(string $name, string $calendarData): void {
public function getInvitationResponseServer(): InvitationResponseServer {
return new InvitationResponseServer(false);
}
+
+ /**
+ * Export objects
+ *
+ * @since 32.0.0
+ *
+ * @return Generator
+ */
+ 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
+ *
+ * @return array
+ */
+ public function import(CalendarImportOptions $options, callable $generator): array {
+
+ $calendarId = $this->getKey();
+ $outcome = [];
+ foreach ($generator($options) as $vObject) {
+
+ $components = $vObject->getBaseComponents();
+ // determine if the object has no base component types
+ if (count($components) === 0) {
+ if ($options->errors === 1) {
+ throw new InvalidArgumentException('Error importing calendar object, discovered object with no base component types');
+ }
+ $outcome['nbct'] = ['outcome' => 'error', 'errors' => ['One or more objects discovered with no base component types']];
+ continue;
+ }
+ // determine if the object has more than 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;
+
+ }
+
+ 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;
+ }
+
}
diff --git a/apps/dav/lib/CalDAV/Export/ExportService.php b/apps/dav/lib/CalDAV/Export/ExportService.php
new file mode 100644
index 0000000000000..11fb57fa245a9
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Export/ExportService.php
@@ -0,0 +1,117 @@
+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' => '2.0-//IDN nextcloud.com//Calendar App//EN',
+ 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' => '',
+ 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();
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Import/ImportService.php b/apps/dav/lib/CalDAV/Import/ImportService.php
new file mode 100644
index 0000000000000..734a51ef54e0d
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Import/ImportService.php
@@ -0,0 +1,225 @@
+source = $source;
+
+ switch ($options->format) {
+ case 'ical':
+ return $calendar->import($options, $this->importText(...));
+ break;
+ case 'jcal':
+ return $calendar->import($options, $this->importJson(...));
+ break;
+ case 'xcal':
+ return $calendar->import($options, $this->importXml(...));
+ break;
+ default:
+ throw new \InvalidArgumentException('Invalid import format');
+ }
+
+ }
+
+ /**
+ * Generates object stream from a text formatted source (ical)
+ *
+ * @since 32.0.0
+ *
+ * @return Generator<\Sabre\VObject\Component\VCalendar>
+ */
+ private function importText(CalendarImportOptions $options): Generator {
+
+ $importer = new TextImporter($this->source);
+
+ $structure = $importer->structure();
+
+ $sObjectPrefix = $importer::OBJECT_PREFIX;
+ $sObjectSuffix = $importer::OBJECT_SUFFIX;
+
+ // calendar properties
+ foreach ($structure['VCALENDAR'] as $entry) {
+ $sObjectPrefix .= $entry;
+ if (substr($entry, -1) !== "\n" || substr($entry, -2) !== "\r\n") {
+ $sObjectPrefix .= PHP_EOL;
+ }
+ }
+
+ // calendar time zones
+ $timezones = [];
+ foreach ($structure['VTIMEZONE'] as $tid => $collection) {
+ $instance = $collection[0];
+ $sObjectContents = $importer->extract($instance[2], $instance[3]);
+ $vObject = Reader::read($sObjectPrefix . $sObjectContents . $sObjectSuffix);
+ $timezones[$tid] = clone $vObject->VTIMEZONE;
+ }
+
+ // calendar components
+ foreach (['VEVENT', 'VTODO', 'VJOURNAL'] as $type) {
+ foreach ($structure[$type] as $cid => $instances) {
+ // extract all instances of component and unserialize to object
+ $sObjectContents = '';
+ foreach ($instances as $instance) {
+ $sObjectContents .= $importer->extract($instance[2], $instance[3]);
+ }
+ /** @var VCalendar $vObject */
+ $vObject = Reader::read($sObjectPrefix . $sObjectContents . $sObjectSuffix);
+ // add time zones to object
+ foreach ($this->findTimeZones($vObject) as $zone) {
+ if (isset($timezones[$zone])) {
+ $vObject->add(clone $timezones[$zone]);
+ }
+ }
+ // return object
+ yield $vObject;
+
+ }
+ }
+ }
+
+ /**
+ * Generates object stream from a xml formatted source (xcal)
+ *
+ * @since 32.0.0
+ *
+ * @return Generator<\Sabre\VObject\Component\VCalendar>
+ */
+ private function importXml(CalendarImportOptions $options): Generator {
+
+ $importer = new XmlImporter($this->source);
+
+ $structure = $importer->structure();
+
+ $sObjectPrefix = $importer::OBJECT_PREFIX;
+ $sObjectSuffix = $importer::OBJECT_SUFFIX;
+
+ // calendar time zones
+ $timezones = [];
+ foreach ($structure['VTIMEZONE'] as $tid => $collection) {
+ $instance = $collection[0];
+ $sObjectContents = $importer->extract($instance[2], $instance[3]);
+ $vObject = Reader::readXml($sObjectPrefix . $sObjectContents . $sObjectSuffix);
+ $timezones[$tid] = clone $vObject->VTIMEZONE;
+ }
+
+ // calendar components
+ foreach (['VEVENT', 'VTODO', 'VJOURNAL'] as $type) {
+ foreach ($structure[$type] as $cid => $instances) {
+ // extract all instances of component and unserialize to object
+ $sObjectContents = '';
+ foreach ($instances as $instance) {
+ $sObjectContents .= $importer->extract($instance[2], $instance[3]);
+ }
+ /** @var VCalendar $vObject */
+ $vObject = Reader::readXml($sObjectPrefix . $sObjectContents . $sObjectSuffix);
+ // add time zones to object
+ foreach ($this->findTimeZones($vObject) as $zone) {
+ if (isset($timezones[$zone])) {
+ $vObject->add(clone $timezones[$zone]);
+ }
+ }
+ // return object
+ yield $vObject;
+
+ }
+ }
+
+ }
+
+ /**
+ * Generates object stream from a json formatted source (jcal)
+ *
+ * @since 32.0.0
+ *
+ * @return Generator<\Sabre\VObject\Component\VCalendar>
+ */
+ private function importJson(CalendarImportOptions $options): Generator {
+
+ /** @var VCALENDAR $importer */
+ $importer = Reader::readJson($this->source);
+
+ // calendar time zones
+ $timezones = [];
+ foreach ($importer->VTIMEZONE as $timezone) {
+ $tzid = $timezone->TZID?->getValue();
+ if ($tzid !== null) {
+ $timezones[$tzid] = clone $timezone;
+ }
+ }
+
+ // calendar components
+ foreach ($importer->getBaseComponents() as $base) {
+ /** @var VCalendar $vObject */
+ $vObject = new VCalendar;
+ $vObject->VERSION = clone $importer->VERSION;
+ $vObject->PRODID = clone $importer->PRODID;
+ // extract all instances of component
+ foreach ($importer->getByUID($base->UID->getValue()) as $instance) {
+ $vObject->add(clone $instance);
+ }
+ // add time zones to object
+ foreach ($this->findTimeZones($vObject) as $zone) {
+ if (isset($timezones[$zone])) {
+ $vObject->add(clone $timezones[$zone]);
+ }
+ }
+ // return object
+ yield $vObject;
+
+ }
+ }
+
+ /**
+ * Searches through all component properties looking for defined timezones
+ *
+ * @since 32.0.0
+ *
+ * @return array
+ */
+ private function findTimeZones(VCalendar $vObject): array {
+ $timezones = [];
+ foreach ($vObject->getComponents() as $vComponent) {
+ if ($vComponent->name !== 'VTIMEZONE') {
+ foreach (['DTSTART', 'DTEND', 'DUE', 'RDATE', 'EXDATE'] as $property) {
+ if (isset($vComponent->$property?->parameters['TZID'])) {
+ $tid = $vComponent->$property->parameters['TZID']->getValue();
+ $timezones[$tid] = true;
+ }
+ }
+ }
+ }
+ return array_keys($timezones);
+ }
+}
diff --git a/apps/dav/lib/CalDAV/Import/TextImporter.php b/apps/dav/lib/CalDAV/Import/TextImporter.php
new file mode 100644
index 0000000000000..3cf92d8c80019
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Import/TextImporter.php
@@ -0,0 +1,123 @@
+ [], 'VEVENT' => [], 'VTODO' => [], 'VJOURNAL' => [], 'VTIMEZONE' => []];
+
+ public function __construct(
+ protected $source,
+ ) {
+ //Ensure that the $data var is of the right type
+ if (!is_string($source) && (!is_resource($source) || get_resource_type($source) !== 'stream')) {
+ throw new Exception('Source must be a string or a stream resource');
+ }
+ }
+
+ protected function analyze() {
+
+ $componentStart = null;
+ $componentEnd = null;
+ $componentId = null;
+ $componentType = null;
+
+ fseek($this->source, 0);
+ while (!feof($this->source)) {
+ $data = fgets($this->source);
+
+ if ($data === false || empty(trim($data))) {
+ continue;
+ }
+
+ if (ctype_space($data[0]) === false) {
+
+ if (str_starts_with($data, 'BEGIN:')) {
+ $type = trim(substr($data, 6));
+ if (in_array($type, $this->types)) {
+ $componentStart = ftell($this->source) - strlen($data);
+ $componentType = $type;
+ }
+ unset($type);
+ }
+
+ if (str_starts_with($data, 'END:')) {
+ $type = trim(substr($data, 4));
+ if ($componentType === $type) {
+ $componentEnd = ftell($this->source);
+ }
+ unset($type);
+ }
+
+ if ($componentStart !== null && str_starts_with($data, 'UID:')) {
+ $componentId = trim(substr($data, 5));
+ }
+
+ if ($componentStart !== null && str_starts_with($data, 'TZID:')) {
+ $componentId = trim(substr($data, 5));
+ }
+
+ }
+
+ if ($componentStart === null) {
+ if (!str_starts_with($data, 'BEGIN:VCALENDAR') && !str_starts_with($data, 'END:VCALENDAR')) {
+ $components['VCALENDAR'][] = $data;
+ }
+ }
+
+ if ($componentEnd !== null) {
+ if ($componentId !== null) {
+ $this->structure[$componentType][$componentId][] = [
+ $componentType,
+ $componentId,
+ $componentStart,
+ $componentEnd
+ ];
+ } else {
+ $this->structure[$componentType][] = [
+ $componentType,
+ $componentId,
+ $componentStart,
+ $componentEnd
+ ];
+ }
+ $componentId = null;
+ $componentType = null;
+ $componentStart = null;
+ $componentEnd = null;
+ }
+
+ }
+
+ }
+
+ public function structure(): array {
+
+ if (!$this->analyzed) {
+ $this->analyze();
+ }
+
+ return $this->structure;
+ }
+
+ public function extract(int $start, int $end): string {
+
+ fseek($this->source, $start);
+ return fread($this->source, $end - $start);
+
+ }
+
+}
diff --git a/apps/dav/lib/CalDAV/Import/XmlImporter.php b/apps/dav/lib/CalDAV/Import/XmlImporter.php
new file mode 100644
index 0000000000000..08d6bea6b1385
--- /dev/null
+++ b/apps/dav/lib/CalDAV/Import/XmlImporter.php
@@ -0,0 +1,168 @@
+';
+ public const OBJECT_SUFFIX = '';
+
+ protected array $types = ['VEVENT', 'VTODO', 'VJOURNAL', 'VTIMEZONE'];
+ protected bool $analyzed = false;
+ protected array $structure = ['VCALENDAR' => [], 'VEVENT' => [], 'VTODO' => [], 'VJOURNAL' => [], 'VTIMEZONE' => []];
+ protected int $praseLevel = 0;
+ protected array $prasePath = [];
+ protected ?int $componentStart = null;
+ protected ?int $componentEnd = null;
+ protected int $componentLevel = 0;
+ protected ?string $componentId = null;
+ protected ?string $componentType = null;
+ protected bool $componentIdProperty = false;
+
+
+ public function __construct(
+ protected $source,
+ ) {
+ //Ensure that the $data var is of the right type
+ if (!is_string($source) && (!is_resource($source) || get_resource_type($source) !== 'stream')) {
+ throw new Exception('Source must be a string or a stream resource');
+ }
+ }
+
+ protected function analyze() {
+
+ $this->praseLevel = 0;
+ $this->prasePath = [];
+ $this->componentStart = null;
+ $this->componentEnd = null;
+ $this->componentLevel = 0;
+ $this->componentId = null;
+ $this->componentType = null;
+ $this->componentIdProperty = false;
+ //Create the parser
+ $parser = xml_parser_create();
+ // assign handlers
+ xml_set_object($parser, $this);
+ xml_set_element_handler($parser, $this->tagStart(...), $this->tagEnd(...));
+ xml_set_default_handler($parser, $this->tagContents(...));
+ //If the data is a resource then loop through it, otherwise just parse the string
+ if (is_resource($this->source)) {
+ //Not all resources support fseek. For those that don't, suppress the error
+ @fseek($this->source, 0);
+ while ($chunk = fread($this->source, 4096)) {
+ if (!xml_parse($parser, $chunk, feof($this->source))) {
+ throw new Exception(
+ xml_error_string(xml_get_error_code($parser))
+ . ' At line: ' .
+ xml_get_current_line_number($parser)
+ );
+ }
+ }
+ } else {
+ if (!xml_parse($parser, $this->source, true)) {
+ throw new Exception(
+ xml_error_string(xml_get_error_code($parser))
+ . ' At line: ' .
+ xml_get_current_line_number($parser)
+ );
+ }
+ }
+
+ //Free up the parser
+ xml_parser_free($parser);
+
+ }
+
+ protected function tagStart($parser, $tag, $attributes) {
+
+ $this->praseLevel++;
+ $this->prasePath[$this->praseLevel] = $tag;
+
+ if (in_array($tag, $this->types)) {
+ $this->componentStart = xml_get_current_byte_index($parser) - (strlen($tag) + 1);
+ $this->componentType = $tag;
+ $this->componentLevel = $this->praseLevel;
+ }
+
+ if ($this->componentStart !== null &&
+ ($this->componentLevel + 2) === $this->praseLevel &&
+ ($tag === 'UID' || $tag === 'TZID')
+ ) {
+ $this->componentIdProperty = true;
+ }
+
+ return $parser;
+ }
+
+ protected function tagEnd($parser, $tag) {
+
+ if ($tag === 'UID' || $tag === 'TZID') {
+ $this->componentIdProperty = false;
+ } elseif ($this->componentType === $tag) {
+ $this->componentEnd = xml_get_current_byte_index($parser);
+
+ if ($this->componentId !== null) {
+ $this->structure[$this->componentType][$this->componentId][] = [
+ $this->componentType,
+ $this->componentId,
+ $this->componentStart,
+ $this->componentEnd,
+ implode('/', $this->prasePath)
+ ];
+ } else {
+ $this->structure[$this->componentType][] = [
+ $this->componentType,
+ $this->componentId,
+ $this->componentStart,
+ $this->componentEnd,
+ implode('/', $this->prasePath)
+ ];
+ }
+ $this->componentStart = null;
+ $this->componentEnd = null;
+ $this->componentId = null;
+ $this->componentType = null;
+ $this->componentIdProperty = false;
+ }
+
+ unset($this->prasePath[$this->praseLevel]);
+ $this->praseLevel--;
+
+ return $parser;
+ }
+
+ protected function tagContents($parser, $data) {
+
+ if ($this->componentIdProperty) {
+ $this->componentId = $data;
+ }
+
+ return $parser;
+ }
+
+
+ public function structure(): array {
+
+ if (!$this->analyzed) {
+ $this->analyze();
+ }
+
+ return $this->structure;
+ }
+
+ public function extract(int $start, int $end): string {
+
+ fseek($this->source, $start);
+ return fread($this->source, $end - $start);
+
+ }
+
+}
diff --git a/apps/dav/lib/Command/ExportCalendar.php b/apps/dav/lib/Command/ExportCalendar.php
new file mode 100644
index 0000000000000..abc2760c74658
--- /dev/null
+++ b/apps/dav/lib/Command/ExportCalendar.php
@@ -0,0 +1,96 @@
+setName('calendar:export')
+ ->setDescription('Export calendar data from supported calendars to disk or stdout')
+ ->addArgument('uid', InputArgument::REQUIRED, 'Id of system user')
+ ->addArgument('cid', InputArgument::REQUIRED, 'Id of calendar')
+ ->addArgument('format', InputArgument::OPTIONAL, 'Format of output (iCal, jCal, xCal) default to iCal')
+ ->addArgument('location', InputArgument::OPTIONAL, 'location of where to write the output. defaults to stdout');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+
+ $userId = $input->getArgument('uid');
+ $calendarId = $input->getArgument('cid');
+ $format = $input->getArgument('format');
+ $location = $input->getArgument('location');
+
+ if (!$this->userManager->userExists($userId)) {
+ throw new InvalidArgumentException("User <$userId> not found.");
+ }
+ // retrieve calendar and evaluate if export is supported
+ $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]);
+ if ($calendars === []) {
+ throw new InvalidArgumentException("Calendar <$calendarId> not found.");
+ }
+ $calendar = $calendars[0];
+ if (!$calendar instanceof ICalendarExport) {
+ throw new InvalidArgumentException("Calendar <$calendarId> dose support this function");
+ }
+ // construct options object
+ $options = new CalendarExportOptions();
+ // evaluate if provided format is supported
+ if ($format !== null && !in_array($format, $this->exportService::FORMATS)) {
+ throw new \InvalidArgumentException("Format <$format> is not valid.");
+ } else {
+ $options->format = $format ?? 'ical';
+ }
+ // evaluate is a valid location was given and is usable otherwise output to stdout
+ if ($location !== null) {
+ $handle = fopen($location, 'w');
+ if ($handle === false) {
+ throw new InvalidArgumentException("Location <$location> is not valid. Can not open location for write operation.");
+ } else {
+ foreach ($this->exportService->export($calendar, $options) as $chunk) {
+ fwrite($handle, $chunk);
+ }
+ fclose($handle);
+ }
+ } else {
+ foreach ($this->exportService->export($calendar, $options) as $chunk) {
+ $output->writeln($chunk);
+ }
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Command/ImportCalendar.php b/apps/dav/lib/Command/ImportCalendar.php
new file mode 100644
index 0000000000000..7b508375bfa81
--- /dev/null
+++ b/apps/dav/lib/Command/ImportCalendar.php
@@ -0,0 +1,206 @@
+setName('calendar:import')
+ ->setDescription('Import calendar data to supported calendars from disk or stdin')
+ ->addArgument('uid', InputArgument::REQUIRED, 'Id of system user')
+ ->addArgument('cid', InputArgument::REQUIRED, 'Id of calendar')
+ ->addArgument('format', InputArgument::OPTIONAL, 'Format of output (iCal, jCal, xCal) default to iCal')
+ ->addArgument('location', InputArgument::OPTIONAL, 'location of where to write the output. defaults to stdin')
+ ->addOption('errors', null, InputOption::VALUE_REQUIRED, 'how to handel item errors (0 - continue, 1 - fail)')
+ ->addOption('validation', null, InputOption::VALUE_REQUIRED, 'how to handel item validation (0 - no validation, 1 - validate and skip on issue, 2 - validate and fail on issue)')
+ ->addOption('supersede', null, InputOption::VALUE_NONE, 'override/replace existing items')
+ ->addOption('show-created', null, InputOption::VALUE_NONE, 'show all created items after processing')
+ ->addOption('show-updated', null, InputOption::VALUE_NONE, 'show all updated items after processing')
+ ->addOption('show-skipped', null, InputOption::VALUE_NONE, 'show all skipped items after processing')
+ ->addOption('show-errors', null, InputOption::VALUE_NONE, 'show all errored items after processing')
+ ;
+
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+
+ $userId = $input->getArgument('uid');
+ $calendarId = $input->getArgument('cid');
+ $format = $input->getArgument('format');
+ $location = $input->getArgument('location');
+ $errors = $input->getOption('errors');
+ $validation = $input->getOption('validation');
+ $supersede = $input->getOption('supersede') ? true : false;
+ $showCreated = $input->getOption('show-created') ? true : false;
+ $showUpdated = $input->getOption('show-updated') ? true : false;
+ $showSkipped = $input->getOption('show-skipped') ? true : false;
+ $showErrors = $input->getOption('show-errors') ? true : false;
+
+ if (!$this->userManager->userExists($userId)) {
+ throw new InvalidArgumentException("User <$userId> not found.");
+ }
+ // retrieve calendar and evaluate if import is supported and writeable
+ $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]);
+ if ($calendars === []) {
+ throw new InvalidArgumentException("Calendar <$calendarId> not found");
+ }
+ $calendar = $calendars[0];
+ if (!$calendar instanceof ICalendarImport || !$calendar instanceof ICalendarIsWritable) {
+ throw new InvalidArgumentException("Calendar <$calendarId> dose support this function");
+ }
+ if (!$calendar->isWritable()) {
+ throw new InvalidArgumentException("Calendar <$calendarId> is not writeable");
+ }
+ if ($calendar->isDeleted()) {
+ throw new InvalidArgumentException("Calendar <$calendarId> is deleted");
+ }
+ // construct options object
+ $options = new CalendarImportOptions();
+ $options->supersede = $supersede;
+ if ($errors !== null) {
+ if ($errors < 0 || $errors > 1) {
+ throw new InvalidArgumentException('Invalid errors option specified');
+ }
+ $options->errors = $errors;
+ }
+ if ($validation !== null) {
+ if ($validation < 0 || $validation > 2) {
+ throw new InvalidArgumentException('Invalid validation option specified');
+ }
+ $options->validate = $validation;
+ }
+ // evaluate if provided format is supported
+ if ($format !== null && !in_array($format, $this->importService::FORMATS)) {
+ throw new \InvalidArgumentException("Format <$format> is not valid.");
+ } else {
+ $options->format = $format ?? 'ical';
+ }
+ // evaluate if a valid location was given and is usable otherwise default to stdin
+ $timeStarted = microtime(true);
+ if ($location !== null) {
+ $input = fopen($location, 'r');
+ if ($input === false) {
+ throw new \InvalidArgumentException("Location <$location> is not valid. Can not open location for read operation.");
+ } else {
+ try {
+ $outcome = $this->importService->import($input, $calendar, $options);
+ } finally {
+ fclose($input);
+ }
+ }
+ } else {
+ $input = fopen('php://stdin', 'r');
+ if ($input === false) {
+ throw new \InvalidArgumentException('Can not open stdin for read operation.');
+ } else {
+ try {
+ $temp = tmpfile();
+ while (!feof($input)) {
+ fwrite($temp, fread($input, 8192));
+ }
+ fseek($temp, 0);
+ $outcome = $this->importService->import($temp, $calendar, $options);
+ } finally {
+ fclose($input);
+ fclose($temp);
+ }
+ }
+ }
+ $timeFinished = microtime(true);
+
+ $totalCreated = 0;
+ $totalUpdated = 0;
+ $totalSkipped = 0;
+ $totalErrors = 0;
+
+ if ($outcome !== []) {
+
+ if ($showCreated || $showUpdated || $showSkipped || $showErrors) {
+ $output->writeln('');
+ }
+
+ foreach ($outcome as $id => $result) {
+ if (isset($result['outcome'])) {
+ switch ($result['outcome']) {
+ case 'created':
+ $totalCreated++;
+ if ($showCreated) {
+ $output->writeln(['created: ' . $id]);
+ }
+ break;
+ case 'updated':
+ $totalUpdated++;
+ if ($showUpdated) {
+ $output->writeln(['updated: ' . $id]);
+ }
+ break;
+ case 'exists':
+ $totalSkipped++;
+ if ($showSkipped) {
+ $output->writeln(['skipped: ' . $id]);
+ }
+ break;
+ case 'error':
+ $totalErrors++;
+ if ($showErrors) {
+ $output->writeln(['errors: ' . $id]);
+ $output->writeln($result['errors']);
+ }
+ break;
+ }
+ }
+
+ }
+ }
+
+ $output->writeln([
+ '',
+ 'Import Completed',
+ '================',
+ 'Execution Time: ' . (string)($timeFinished - $timeStarted) . ' sec',
+ 'Total Created: ' . (string)$totalCreated,
+ 'Total Updated: ' . (string)$totalUpdated,
+ 'Total Skipped: ' . (string)$totalSkipped,
+ 'Total Errors: ' . (string)$totalErrors,
+ ''
+ ]);
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/dav/lib/Controller/CalendarExportController.php b/apps/dav/lib/Controller/CalendarExportController.php
new file mode 100644
index 0000000000000..e840dfacb1c90
--- /dev/null
+++ b/apps/dav/lib/Controller/CalendarExportController.php
@@ -0,0 +1,91 @@
+userSession->isLoggedIn()) {
+ return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ }
+ if ($userId !== null) {
+ if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID()) &&
+ $this->userSession->getUser()->getUID() !== $userId) {
+ return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ }
+ if (!$this->userManager->userExists($userId)) {
+ return new DataResponse(['error' => 'user not found'], Http::STATUS_BAD_REQUEST);
+ }
+ } else {
+ $userId = $this->userSession->getUser()->getUID();
+ }
+ // retrieve calendar and evaluate if export is supported
+ $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]);
+ if ($calendars === []) {
+ return new DataResponse(['error' => 'calendar not found'], Http::STATUS_BAD_REQUEST);
+ }
+ $calendar = $calendars[0];
+ if (!$calendar instanceof ICalendarExport) {
+ return new DataResponse(['error' => 'calendar export not supported'], Http::STATUS_BAD_REQUEST);
+ }
+ // construct options object
+ $options = new CalendarExportOptions();
+ // evaluate if provided format is supported
+ if ($format !== null && !in_array($format, $this->exportService::FORMATS)) {
+ throw new \InvalidArgumentException("Format <$format> is not valid.");
+ } else {
+ $options->format = $format ?? 'ical';
+ }
+ $contentType = match (strtolower($format)) {
+ 'jcal' => 'application/calendar+json; charset=UTF-8',
+ 'xcal' => 'application/calendar+xml; charset=UTF-8',
+ default => 'text/calendar; charset=UTF-8'
+ };
+
+ return new StreamGeneratorResponse($this->exportService->export($calendar, $options), $contentType);
+
+ }
+}
diff --git a/apps/dav/lib/Controller/CalendarImportController.php b/apps/dav/lib/Controller/CalendarImportController.php
new file mode 100644
index 0000000000000..90f83db42140d
--- /dev/null
+++ b/apps/dav/lib/Controller/CalendarImportController.php
@@ -0,0 +1,171 @@
+userSession->isLoggedIn()) {
+ return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ }
+ if ($userId !== null) {
+ if (!$this->groupManager->isAdmin($this->userSession->getUser()->getUID()) &&
+ $this->userSession->getUser()->getUID() !== $userId) {
+ return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ }
+ if (!$this->userManager->userExists($userId)) {
+ return new DataResponse(['error' => 'user not found'], Http::STATUS_BAD_REQUEST);
+ }
+ } else {
+ $userId = $this->userSession->getUser()->getUID();
+ }
+ // retrieve calendar and evaluate if export is supported
+ $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]);
+ if ($calendars === []) {
+ return new DataResponse(['error' => 'calendar not found'], Http::STATUS_BAD_REQUEST);
+ }
+ $calendar = $calendars[0];
+ if (!$calendar instanceof ICalendarImport) {
+ return new DataResponse(['error' => 'calendar export not supported'], Http::STATUS_BAD_REQUEST);
+ }
+ // evaluate if requested format is supported and convert to output content type
+ if ($format !== null && !in_array($format, $this->importService::FORMATS)) {
+ return new DataResponse(['error' => 'format invalid'], Http::STATUS_BAD_REQUEST);
+ } elseif ($format === null) {
+ $format = 'ical';
+ }
+ // retrieve calendar and evaluate if import is supported and writeable
+ $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId, [$calendarId]);
+ if ($calendars === []) {
+ return new DataResponse(['error' => "Calendar <$calendarId> not found"], Http::STATUS_BAD_REQUEST);
+ }
+ $calendar = $calendars[0];
+ if (!$calendar instanceof ICalendarImport || !$calendar instanceof ICalendarIsWritable) {
+ return new DataResponse(['error' => "Calendar <$calendarId> dose support this function"], Http::STATUS_BAD_REQUEST);
+ }
+ if (!$calendar->isWritable()) {
+ return new DataResponse(['error' => "Calendar <$calendarId> is not writeable"], Http::STATUS_BAD_REQUEST);
+ }
+ if ($calendar->isDeleted()) {
+ return new DataResponse(['error' => "Calendar <$calendarId> is deleted"], Http::STATUS_BAD_REQUEST);
+ }
+ // construct options object
+ $options = new CalendarImportOptions();
+ $options->supersede = $supersede;
+ if ($errors !== null) {
+ if ($errors < 0 || $errors > 1) {
+ return new DataResponse(['error' => 'Invalid errors option specified'], Http::STATUS_BAD_REQUEST);
+ }
+ $options->errors = $errors;
+ }
+ if ($validation !== null) {
+ if ($validation < 0 || $validation > 2) {
+ return new DataResponse(['error' => 'Invalid validation option specified'], Http::STATUS_BAD_REQUEST);
+ }
+ $options->validate = $validation;
+ }
+ // evaluate if provided format is supported
+ if ($format !== null && !in_array($format, $this->importService::FORMATS)) {
+ throw new \InvalidArgumentException("Format <$format> is not valid.");
+ } else {
+ $options->format = $format ?? 'ical';
+ }
+ //
+ $timeStarted = microtime(true);
+ $input = fopen('php://stdin', 'r');
+ try {
+ $temp = tmpfile();
+ fwrite($temp, $data);
+ fseek($temp, 0);
+ $outcome = $this->importService->import($temp, $calendar, $options);
+ } finally {
+ fclose($input);
+ fclose($temp);
+ }
+ $timeFinished = microtime(true);
+
+ $totalCreated = 0;
+ $totalUpdated = 0;
+ $totalSkipped = 0;
+ $totalErrors = 0;
+
+ if ($outcome !== []) {
+ foreach ($outcome as $id => $result) {
+ if (isset($result['outcome'])) {
+ switch ($result['outcome']) {
+ case 'created':
+ $totalCreated++;
+ break;
+ case 'updated':
+ $totalUpdated++;
+ break;
+ case 'exists':
+ $totalSkipped++;
+ break;
+ case 'error':
+ $totalErrors++;
+ break;
+ }
+ }
+
+ }
+ }
+
+ $summary = [
+ 'time' => ($timeFinished - $timeStarted),
+ 'created' => $totalCreated,
+ 'updated' => $totalUpdated,
+ 'skipped' => $totalSkipped,
+ 'errors' => $totalErrors,
+ ];
+
+ return new DataResponse($summary, Http::STATUS_OK);
+
+ }
+}
diff --git a/apps/dav/tests/unit/CalDAV/CalendarImplTest.php b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
index ee9b85fafe8b5..144f935bb03f9 100644
--- a/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
+++ b/apps/dav/tests/unit/CalDAV/CalendarImplTest.php
@@ -5,6 +5,7 @@
*/
namespace OCA\DAV\Tests\unit\CalDAV;
+use Generator;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
@@ -12,32 +13,31 @@
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCA\DAV\CalDAV\Schedule\Plugin;
use OCA\DAV\Connector\Sabre\Server;
+use OCP\Calendar\CalendarImportOptions;
use OCP\Calendar\Exceptions\CalendarException;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Reader;
+use Sabre\VObject\UUIDUtil;
class CalendarImplTest extends \Test\TestCase {
- /** @var CalendarImpl */
- private $calendarImpl;
- /** @var Calendar | \PHPUnit\Framework\MockObject\MockObject */
- private $calendar;
-
- /** @var array */
- private $calendarInfo;
-
- /** @var CalDavBackend | \PHPUnit\Framework\MockObject\MockObject */
- private $backend;
+ private Calendar|MockObject $calendar;
+ private array $calendarInfo;
+ private CalDavBackend|MockObject $backend;
+ private CalendarImpl|MockObject $calendarImpl;
+ private UUIDUtil|MockObject $uuidUtil;
+ private array $mockImportCollection;
+ private array $mockExportCollection;
protected function setUp(): void {
parent::setUp();
$this->calendar = $this->createMock(Calendar::class);
$this->calendarInfo = [
- 'id' => 'fancy_id_123',
+ 'id' => 1,
'{DAV:}displayname' => 'user readable name 123',
'{http://apple.com/ns/ical/}calendar-color' => '#AABBCC',
'uri' => '/this/is/a/uri',
@@ -45,13 +45,18 @@ protected function setUp(): void {
];
$this->backend = $this->createMock(CalDavBackend::class);
- $this->calendarImpl = new CalendarImpl($this->calendar,
- $this->calendarInfo, $this->backend);
+ $this->calendarImpl = new CalendarImpl(
+ $this->calendar,
+ $this->calendarInfo,
+ $this->backend
+ );
+
+ $this->uuidUtil = $this->createMock(UUIDUtil::class);
}
public function testGetKey(): void {
- $this->assertEquals($this->calendarImpl->getKey(), 'fancy_id_123');
+ $this->assertEquals($this->calendarImpl->getKey(), 1);
}
public function testGetDisplayname(): void {
@@ -261,4 +266,239 @@ private function getITipMessage($calendarData): Message {
$iTipMessage->message = $vObject;
return $iTipMessage;
}
+
+ protected function mockImportGenerator(CalendarImportOptions $options): Generator {
+ foreach ($this->mockImportCollection as $entry) {
+ yield $entry;
+ }
+ }
+
+ protected function mockExportGenerator(): Generator {
+ foreach ($this->mockExportCollection as $entry) {
+ yield $entry;
+ }
+ }
+
+ public function testExport(): void {
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // construct data store return
+ $this->mockExportCollection[] = [
+ 'id' => 1,
+ 'calendardata' => $vCalendar->serialize()
+ ];
+ $this->backend->expects($this->once())
+ ->method('exportCalendar')
+ ->with(1, $this->backend::CALENDAR_TYPE_CALENDAR, null)
+ ->willReturn($this->mockExportGenerator());
+ // test export
+ foreach ($this->calendarImpl->export(null) as $entry) {
+ $exported[] = $entry;
+ }
+ $this->assertCount(1, $exported, 'Invalid exported items count');
+ }
+
+ public function testImportNewObject(): void {
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $this->mockImportCollection[] = $vCalendar;
+ // construct mock backend
+ $this->backend->expects($this->once())
+ ->method('getCalendarObjectByUID')
+ ->with(
+ $this->calendarInfo['principaluri'],
+ $vEvent->UID->getValue()
+ )
+ ->willReturn(null);
+ $this->backend->expects($this->once())
+ ->method('createCalendarObject')
+ ->withAnyParameters();
+
+ $options = new CalendarImportOptions();
+ // test import
+ $outcome = $this->calendarImpl->import($options, $this->mockImportGenerator(...));
+ $this->assertCount(1, $outcome, 'No import status returned');
+ $this->assertArrayHasKey('96a0e6b1-d886-4a55-a60d-152b31401dcc', $outcome, 'No import status returned for object');
+ $this->assertEquals('created', $outcome['96a0e6b1-d886-4a55-a60d-152b31401dcc']['outcome'], 'Invalid import status returned for object');
+
+ }
+
+ public function testImportExistingObjectUpdated(): void {
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $this->mockImportCollection[] = $vCalendar;
+ // construct mock backend
+ $this->backend->expects($this->once())
+ ->method('getCalendarObjectByUID')
+ ->with(
+ $this->calendarInfo['principaluri'],
+ $vEvent->UID->getValue()
+ )
+ ->willReturn($this->calendarInfo['id'] . '/' . $vEvent->UID->getValue());
+ $this->backend->expects($this->once())
+ ->method('updateCalendarObject')
+ ->withAnyParameters();
+
+ $options = new CalendarImportOptions();
+ $options->supersede = true;
+ // test import
+ $outcome = $this->calendarImpl->import($options, $this->mockImportGenerator(...));
+ $this->assertCount(1, $outcome, 'No import status returned');
+ $this->assertArrayHasKey('96a0e6b1-d886-4a55-a60d-152b31401dcc', $outcome, 'No import status returned for object');
+ $this->assertEquals('updated', $outcome['96a0e6b1-d886-4a55-a60d-152b31401dcc']['outcome'], 'Invalid import status returned for object');
+
+ }
+
+ public function testImportExistingObjectExists(): void {
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $this->mockImportCollection[] = $vCalendar;
+ // construct mock backend
+ $this->backend->expects($this->once())
+ ->method('getCalendarObjectByUID')
+ ->with(
+ $this->calendarInfo['principaluri'],
+ $vEvent->UID->getValue()
+ )
+ ->willReturn($this->calendarInfo['id'] . '/' . $vEvent->UID->getValue());
+
+ $options = new CalendarImportOptions();
+ // test import
+ $outcome = $this->calendarImpl->import($options, $this->mockImportGenerator(...));
+ $this->assertCount(1, $outcome, 'No import status returned');
+ $this->assertArrayHasKey('96a0e6b1-d886-4a55-a60d-152b31401dcc', $outcome, 'No import status returned for object');
+ $this->assertEquals('exists', $outcome['96a0e6b1-d886-4a55-a60d-152b31401dcc']['outcome'], 'Invalid import status returned for object');
+
+ }
+
+ public function testImportErrorNoBaseObject(): void {
+
+ // construct calendar object
+ $vCalendar = new VCalendar();
+ $this->mockImportCollection[] = $vCalendar;
+
+ $options = new CalendarImportOptions();
+ $options->errors = 0;
+ // test import
+ $outcome = $this->calendarImpl->import($options, $this->mockImportGenerator(...));
+ $this->assertCount(1, $outcome, 'No import status returned');
+ $this->assertArrayHasKey('nbct', $outcome, 'No import status returned for object');
+ $this->assertEquals('error', $outcome['nbct']['outcome'], 'Invalid import status returned for object');
+
+ }
+
+ public function testImportErrorMultipleBaseObjects(): void {
+
+ // construct calendar object
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', []);
+ $vCalendar->add('VTODO', []);
+ $this->mockImportCollection[] = $vCalendar;
+
+ $options = new CalendarImportOptions();
+ $options->errors = 0;
+ // test import
+ $outcome = $this->calendarImpl->import($options, $this->mockImportGenerator(...));
+ $this->assertCount(1, $outcome, 'No import status returned');
+ $this->assertArrayHasKey('mbct', $outcome, 'No import status returned for object');
+ $this->assertEquals('error', $outcome['mbct']['outcome'], 'Invalid import status returned for object');
+
+ }
+
+ public function testImportErrorNoUid(): void {
+
+ // construct calendar object
+ $vCalendar = new VCalendar();
+ $vCalendar->add('VEVENT', []);
+ $vCalendar->VEVENT->remove('UID');
+ $this->mockImportCollection[] = $vCalendar;
+
+ $options = new CalendarImportOptions();
+ $options->errors = 0;
+ // test import
+ $outcome = $this->calendarImpl->import($options, $this->mockImportGenerator(...));
+ $this->assertCount(1, $outcome, 'No import status returned');
+ $this->assertArrayHasKey('noid', $outcome, 'No import status returned for object');
+ $this->assertEquals('error', $outcome['noid']['outcome'], 'Invalid import status returned for object');
+
+ }
+
+ public function testImportErrorValidation(): void {
+
+ // construct calendar object
+ $vCalendar = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $this->mockImportCollection[] = $vCalendar;
+
+ $options = new CalendarImportOptions();
+ $options->errors = 0;
+ // test import
+ $outcome = $this->calendarImpl->import($options, $this->mockImportGenerator(...));
+ $this->assertCount(1, $outcome, 'No import status returned');
+ $this->assertArrayHasKey('96a0e6b1-d886-4a55-a60d-152b31401dcc', $outcome, 'No import status returned for object');
+ $this->assertEquals('error', $outcome['96a0e6b1-d886-4a55-a60d-152b31401dcc']['outcome'], 'Invalid import status returned for object');
+
+ }
+
}
diff --git a/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php b/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php
new file mode 100644
index 0000000000000..aab423610b9ef
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php
@@ -0,0 +1,72 @@
+service = new ExportService();
+ $this->calendar = $this->createMock(ICalendarExport::class);
+
+ }
+
+ protected function mockGenerator(): Generator {
+ foreach ($this->mockExportCollection as $entry) {
+ yield $entry;
+ }
+ }
+
+ public function testExport(): void {
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // construct calendar return
+ $options = new CalendarExportOptions();
+ $this->mockExportCollection[] = $vCalendar;
+ $this->calendar->expects($this->once())
+ ->method('export')
+ ->with($options)
+ ->willReturn($this->mockGenerator());
+ // test export
+ $document = '';
+ foreach ($this->service->export($this->calendar, $options) as $chunk) {
+ $document .= $chunk;
+ }
+ $this->assertTrue(str_contains($document, 'BEGIN:VCALENDAR'), 'Exported document calendar start missing');
+ $this->assertTrue(str_contains($document, 'BEGIN:VEVENT'), 'Exported document event start missing');
+ $this->assertTrue(str_contains($document, 'END:VEVENT'), 'Exported document event end missing');
+ $this->assertTrue(str_contains($document, 'END:VCALENDAR'), 'Exported document calendar end missing');
+
+ }
+
+}
diff --git a/apps/dav/tests/unit/CalDAV/Import/ImportServiceTest.php b/apps/dav/tests/unit/CalDAV/Import/ImportServiceTest.php
new file mode 100644
index 0000000000000..405ddc81e0db3
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/Import/ImportServiceTest.php
@@ -0,0 +1,70 @@
+service = new ImportService();
+ $this->calendar = $this->createMock(ICalendarImport::class);
+
+ }
+
+ public function mockCollector(CalendarImportOptions $options, callable $generator): array {
+ foreach ($generator($options) as $entry) {
+ $this->mockImportCollection[] = $entry;
+ }
+ return [];
+ }
+
+ public function testImport(): void {
+
+ // construct calendar with a 1 hour event and same start/end time zones
+ $vCalendar = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $vCalendar->add('VEVENT', []);
+ $vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Recurrence Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ // construct stream from mock calendar
+ $stream = fopen('php://memory', 'r+');
+ fwrite($stream, $vCalendar->serialize());
+ rewind($stream);
+ // construct import options
+ $options = new CalendarImportOptions();
+ $this->calendar->expects($this->once())
+ ->method('import')
+ ->willReturnCallback($this->mockCollector(...));
+ // test import
+ $this->service->import($stream, $this->calendar, $options);
+ $this->assertCount(1, $this->mockImportCollection, 'Imported items count is invalid');
+ $this->assertTrue(isset($this->mockImportCollection[0]->VEVENT), 'Imported item missing VEVENT');
+ $this->assertCount(1, $this->mockImportCollection[0]->VEVENT, 'Imported items count is invalid');
+
+ }
+
+}
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index cbadc2deb15b4..0caa74b43fd4b 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -110,6 +110,7 @@
'OCP\\AppFramework\\Http\\RedirectToDefaultAppResponse' => $baseDir . '/lib/public/AppFramework/Http/RedirectToDefaultAppResponse.php',
'OCP\\AppFramework\\Http\\Response' => $baseDir . '/lib/public/AppFramework/Http/Response.php',
'OCP\\AppFramework\\Http\\StandaloneTemplateResponse' => $baseDir . '/lib/public/AppFramework/Http/StandaloneTemplateResponse.php',
+ 'OCP\\AppFramework\\Http\\StreamGeneratorResponse' => $baseDir . '/lib/public/AppFramework/Http/StreamGeneratorResponse.php',
'OCP\\AppFramework\\Http\\StreamResponse' => $baseDir . '/lib/public/AppFramework/Http/StreamResponse.php',
'OCP\\AppFramework\\Http\\StrictContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/StrictContentSecurityPolicy.php',
'OCP\\AppFramework\\Http\\StrictEvalContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/StrictEvalContentSecurityPolicy.php',
@@ -190,10 +191,14 @@
'OCP\\Broadcast\\Events\\IBroadcastEvent' => $baseDir . '/lib/public/Broadcast/Events/IBroadcastEvent.php',
'OCP\\Cache\\CappedMemoryCache' => $baseDir . '/lib/public/Cache/CappedMemoryCache.php',
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => $baseDir . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
+ 'OCP\\Calendar\\CalendarExportOptions' => $baseDir . '/lib/public/Calendar/CalendarExportOptions.php',
+ 'OCP\\Calendar\\CalendarImportOptions' => $baseDir . '/lib/public/Calendar/CalendarImportOptions.php',
'OCP\\Calendar\\Exceptions\\CalendarException' => $baseDir . '/lib/public/Calendar/Exceptions/CalendarException.php',
'OCP\\Calendar\\IAvailabilityResult' => $baseDir . '/lib/public/Calendar/IAvailabilityResult.php',
'OCP\\Calendar\\ICalendar' => $baseDir . '/lib/public/Calendar/ICalendar.php',
'OCP\\Calendar\\ICalendarEventBuilder' => $baseDir . '/lib/public/Calendar/ICalendarEventBuilder.php',
+ 'OCP\\Calendar\\ICalendarExport' => $baseDir . '/lib/public/Calendar/ICalendarExport.php',
+ 'OCP\\Calendar\\ICalendarImport' => $baseDir . '/lib/public/Calendar/ICalendarImport.php',
'OCP\\Calendar\\ICalendarIsShared' => $baseDir . '/lib/public/Calendar/ICalendarIsShared.php',
'OCP\\Calendar\\ICalendarIsWritable' => $baseDir . '/lib/public/Calendar/ICalendarIsWritable.php',
'OCP\\Calendar\\ICalendarProvider' => $baseDir . '/lib/public/Calendar/ICalendarProvider.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 10086cabce066..7d0bef4bfa318 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -159,6 +159,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\AppFramework\\Http\\RedirectToDefaultAppResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/RedirectToDefaultAppResponse.php',
'OCP\\AppFramework\\Http\\Response' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Response.php',
'OCP\\AppFramework\\Http\\StandaloneTemplateResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StandaloneTemplateResponse.php',
+ 'OCP\\AppFramework\\Http\\StreamGeneratorResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StreamGeneratorResponse.php',
'OCP\\AppFramework\\Http\\StreamResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StreamResponse.php',
'OCP\\AppFramework\\Http\\StrictContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StrictContentSecurityPolicy.php',
'OCP\\AppFramework\\Http\\StrictEvalContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StrictEvalContentSecurityPolicy.php',
@@ -239,10 +240,14 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Broadcast\\Events\\IBroadcastEvent' => __DIR__ . '/../../..' . '/lib/public/Broadcast/Events/IBroadcastEvent.php',
'OCP\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/public/Cache/CappedMemoryCache.php',
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => __DIR__ . '/../../..' . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
+ 'OCP\\Calendar\\CalendarExportOptions' => __DIR__ . '/../../..' . '/lib/public/Calendar/CalendarExportOptions.php',
+ 'OCP\\Calendar\\CalendarImportOptions' => __DIR__ . '/../../..' . '/lib/public/Calendar/CalendarImportOptions.php',
'OCP\\Calendar\\Exceptions\\CalendarException' => __DIR__ . '/../../..' . '/lib/public/Calendar/Exceptions/CalendarException.php',
'OCP\\Calendar\\IAvailabilityResult' => __DIR__ . '/../../..' . '/lib/public/Calendar/IAvailabilityResult.php',
'OCP\\Calendar\\ICalendar' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendar.php',
'OCP\\Calendar\\ICalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarEventBuilder.php',
+ 'OCP\\Calendar\\ICalendarExport' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarExport.php',
+ 'OCP\\Calendar\\ICalendarImport' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarImport.php',
'OCP\\Calendar\\ICalendarIsShared' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarIsShared.php',
'OCP\\Calendar\\ICalendarIsWritable' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarIsWritable.php',
'OCP\\Calendar\\ICalendarProvider' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarProvider.php',
diff --git a/lib/public/AppFramework/Http/StreamGeneratorResponse.php b/lib/public/AppFramework/Http/StreamGeneratorResponse.php
new file mode 100644
index 0000000000000..9c12acb9d8c5d
--- /dev/null
+++ b/lib/public/AppFramework/Http/StreamGeneratorResponse.php
@@ -0,0 +1,58 @@
+>
+ */
+class StreamGeneratorResponse extends Response implements ICallbackResponse {
+ protected $generator;
+
+ /**
+ * @since 32.0.0
+ *
+ * @param \Generator $generator the function to call to generate the response
+ * @param String $contentType http response content type e.g. 'application/json; charset=UTF-8'
+ * @param int $status http response status
+ */
+ public function __construct(Generator $generator, string $contentType, int $status = 200) {
+ parent::__construct();
+
+ $this->generator = $generator;
+
+ $this->setStatus($status);
+ $this->cacheFor(0);
+ $this->addHeader('Content-Type', $contentType);
+
+ }
+
+ /**
+ * Streams content directly to client
+ *
+ * @since 32.0.0
+ *
+ * @param IOutput $output a small wrapper that handles output
+ */
+ public function callback(IOutput $output) {
+
+ foreach ($this->generator as $chunk) {
+ print($chunk);
+ flush();
+ }
+
+ }
+
+}
diff --git a/lib/public/Calendar/CalendarExportOptions.php b/lib/public/Calendar/CalendarExportOptions.php
new file mode 100644
index 0000000000000..51e55b5faaa5a
--- /dev/null
+++ b/lib/public/Calendar/CalendarExportOptions.php
@@ -0,0 +1,22 @@
+