Merge pull request #51924 from nextcloud/feat/issue-563-calendar-export

feat: Calendar Export
pull/52253/head
Daniel 5 months ago committed by GitHub
commit 31899d95b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      apps/dav/appinfo/info.xml
  2. 2
      apps/dav/composer/composer/autoload_classmap.php
  3. 2
      apps/dav/composer/composer/autoload_static.php
  4. 40
      apps/dav/lib/CalDAV/CalDavBackend.php
  5. 30
      apps/dav/lib/CalDAV/CalendarImpl.php
  6. 107
      apps/dav/lib/CalDAV/Export/ExportService.php
  7. 95
      apps/dav/lib/Command/ExportCalendar.php
  8. 5
      apps/dav/lib/Listener/AddMissingIndicesListener.php
  9. 1
      apps/dav/lib/Migration/Version1006Date20180628111625.php
  10. 71
      apps/dav/tests/unit/CalDAV/CalendarImplTest.php
  11. 80
      apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php
  12. 2
      lib/composer/composer/autoload_classmap.php
  13. 2
      lib/composer/composer/autoload_static.php
  14. 68
      lib/public/Calendar/CalendarExportOptions.php
  15. 31
      lib/public/Calendar/ICalendarExport.php

@ -60,6 +60,7 @@
<command>OCA\DAV\Command\CreateSubscription</command>
<command>OCA\DAV\Command\DeleteCalendar</command>
<command>OCA\DAV\Command\DeleteSubscription</command>
<command>OCA\DAV\Command\ExportCalendar</command>
<command>OCA\DAV\Command\FixCalendarSyncCommand</command>
<command>OCA\DAV\Command\ListAddressbooks</command>
<command>OCA\DAV\Command\ListCalendars</command>

@ -64,6 +64,7 @@ return array(
'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',
@ -159,6 +160,7 @@ return array(
'OCA\\DAV\\Command\\CreateSubscription' => $baseDir . '/../lib/Command/CreateSubscription.php',
'OCA\\DAV\\Command\\DeleteCalendar' => $baseDir . '/../lib/Command/DeleteCalendar.php',
'OCA\\DAV\\Command\\DeleteSubscription' => $baseDir . '/../lib/Command/DeleteSubscription.php',
'OCA\\DAV\\Command\\ExportCalendar' => $baseDir . '/../lib/Command/ExportCalendar.php',
'OCA\\DAV\\Command\\FixCalendarSyncCommand' => $baseDir . '/../lib/Command/FixCalendarSyncCommand.php',
'OCA\\DAV\\Command\\ListAddressbooks' => $baseDir . '/../lib/Command/ListAddressbooks.php',
'OCA\\DAV\\Command\\ListCalendars' => $baseDir . '/../lib/Command/ListCalendars.php',

@ -79,6 +79,7 @@ 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',
@ -174,6 +175,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Command\\CreateSubscription' => __DIR__ . '/..' . '/../lib/Command/CreateSubscription.php',
'OCA\\DAV\\Command\\DeleteCalendar' => __DIR__ . '/..' . '/../lib/Command/DeleteCalendar.php',
'OCA\\DAV\\Command\\DeleteSubscription' => __DIR__ . '/..' . '/../lib/Command/DeleteSubscription.php',
'OCA\\DAV\\Command\\ExportCalendar' => __DIR__ . '/..' . '/../lib/Command/ExportCalendar.php',
'OCA\\DAV\\Command\\FixCalendarSyncCommand' => __DIR__ . '/..' . '/../lib/Command/FixCalendarSyncCommand.php',
'OCA\\DAV\\Command\\ListAddressbooks' => __DIR__ . '/..' . '/../lib/Command/ListAddressbooks.php',
'OCA\\DAV\\Command\\ListCalendars' => __DIR__ . '/..' . '/../lib/Command/ListCalendars.php',

@ -9,6 +9,7 @@ namespace OCA\DAV\CalDAV;
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;
@ -28,6 +29,7 @@ use OCA\DAV\Events\SubscriptionCreatedEvent;
use OCA\DAV\Events\SubscriptionDeletedEvent;
use OCA\DAV\Events\SubscriptionUpdatedEvent;
use OCP\AppFramework\Db\TTransactional;
use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\Events\CalendarObjectCreatedEvent;
use OCP\Calendar\Events\CalendarObjectDeletedEvent;
use OCP\Calendar\Events\CalendarObjectMovedEvent;
@ -987,6 +989,44 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}, $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 {
// extract options
$rangeStart = $options?->getRangeStart();
$rangeCount = $options?->getRangeCount();
// construct query
$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 ($rangeStart !== null) {
$qb->andWhere($qb->expr()->gt('uid', $qb->createNamedParameter($rangeStart)));
}
if ($rangeCount !== null) {
$qb->setMaxResults($rangeCount);
}
if ($rangeStart !== null || $rangeCount !== null) {
$qb->orderBy('uid', 'ASC');
}
$rs = $qb->executeQuery();
// iterate through results
try {
while (($row = $rs->fetch()) !== false) {
yield $row;
}
} finally {
$rs->closeCursor();
}
}
/**
* Returns all calendar objects with limited metadata for a calendar
*

@ -8,9 +8,14 @@ declare(strict_types=1);
*/
namespace OCA\DAV\CalDAV;
use Generator;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICalendarExport;
use OCP\Calendar\ICalendarIsShared;
use OCP\Calendar\ICalendarIsWritable;
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IHandleImipMessage;
use OCP\Constants;
@ -24,7 +29,7 @@ use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use function Sabre\Uri\split as uriSplit;
class CalendarImpl implements ICreateFromString, IHandleImipMessage {
class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIsWritable, ICalendarIsShared, ICalendarExport {
public function __construct(
private Calendar $calendar,
/** @var array<string, mixed> */
@ -257,4 +262,27 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage {
public function getInvitationResponseServer(): InvitationResponseServer {
return new InvitationResponseServer(false);
}
/**
* Export objects
*
* @since 32.0.0
*
* @return Generator<mixed, \Sabre\VObject\Component\VCalendar, mixed, mixed>
*/
public function export(?CalendarExportOptions $options = null): Generator {
foreach (
$this->backend->exportCalendar(
$this->calendarInfo['id'],
$this->backend::CALENDAR_TYPE_CALENDAR,
$options
) as $event
) {
$vObject = Reader::read($event['calendardata']);
if ($vObject instanceof VCalendar) {
yield $vObject;
}
}
}
}

@ -0,0 +1,107 @@
<?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\ICalendarExport;
use OCP\ServerVersion;
use Sabre\VObject\Component;
use Sabre\VObject\Writer;
/**
* Calendar Export Service
*/
class ExportService {
public const FORMATS = ['ical', 'jcal', 'xcal'];
private string $systemVersion;
public function __construct(ServerVersion $serverVersion) {
$this->systemVersion = $serverVersion->getVersionString();
}
/**
* Generates serialized content stream for a calendar and objects based in selected format
*
* @return Generator<string>
*/
public function export(ICalendarExport $calendar, CalendarExportOptions $options): Generator {
// output start of serialized content based on selected format
yield $this->exportStart($options->getFormat());
// 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->getFormat(), $consecutive);
$consecutive = true;
}
}
}
// iterate through each saved vTimezone entry, convert to appropriate format and output
foreach ($timezones as $vComponent) {
yield $this->exportObject($vComponent, $options->getFormat(), $consecutive);
$consecutive = true;
}
// output end of serialized content based on selected format
yield $this->exportFinish($options->getFormat());
}
/**
* Generates serialized content start based on selected format
*/
private function exportStart(string $format): string {
return match ($format) {
'jcal' => '["vcalendar",[["version",{},"text","2.0"],["prodid",{},"text","-\/\/IDN nextcloud.com\/\/Calendar Export v' . $this->systemVersion . '\/\/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 Export v' . $this->systemVersion . '//EN</text></prodid></properties><components>',
default => "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//IDN nextcloud.com//Calendar Export v" . $this->systemVersion . "//EN\n"
};
}
/**
* Generates serialized content end based on selected format
*/
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
*/
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
*/
private function exportObjectXml(Component $vobject): string {
$writer = new \Sabre\Xml\Writer();
$writer->openMemory();
$writer->setIndent(false);
$vobject->xmlSerialize($writer);
return $writer->outputMemory();
}
}

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Command;
use InvalidArgumentException;
use OCA\DAV\CalDAV\Export\ExportService;
use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\ICalendarExport;
use OCP\Calendar\IManager;
use OCP\IUserManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Calendar Export Command
*
* Used to export data from supported calendars to disk or stdout
*/
#[AsCommand(
name: 'calendar:export',
description: 'Export calendar data from supported calendars to disk or stdout',
hidden: false
)]
class ExportCalendar extends Command {
public function __construct(
private IUserManager $userManager,
private IManager $calendarManager,
private ExportService $exportService,
) {
parent::__construct();
}
protected function configure(): void {
$this->setName('calendar:export')
->setDescription('Export calendar data from supported calendars to disk or stdout')
->addArgument('uid', InputArgument::REQUIRED, 'Id of system user')
->addArgument('uri', InputArgument::REQUIRED, 'Uri of calendar')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'Format of output (ical, jcal, xcal) defaults to ical', 'ical')
->addOption('location', null, InputOption::VALUE_REQUIRED, '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('uri');
$format = $input->getOption('format');
$location = $input->getOption('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> does not support exporting");
}
// construct options object
$options = new CalendarExportOptions();
// evaluate if provided format is supported
if (!in_array($format, ExportService::FORMATS, true)) {
throw new InvalidArgumentException("Format <$format> is not valid.");
}
$options->setFormat($format);
// evaluate is a valid location was given and is usable otherwise output to stdout
if ($location !== null) {
$handle = fopen($location, 'wb');
if ($handle === false) {
throw new InvalidArgumentException("Location <$location> is not valid. Can not open location for write operation.");
}
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;
}
}

@ -30,6 +30,11 @@ class AddMissingIndicesListener implements IEventListener {
'dav_shares_resourceid_access',
['resourceid', 'access']
);
$event->addMissingIndex(
'calendarobjects',
'calobjects_by_uid_index',
['calendarid', 'calendartype', 'uid']
);
}
}

@ -49,6 +49,7 @@ class Version1006Date20180628111625 extends SimpleMigrationStep {
$calendarObjectsTable->dropIndex('calobjects_index');
}
$calendarObjectsTable->addUniqueIndex(['calendarid', 'calendartype', 'uri'], 'calobjects_index');
$calendarObjectsTable->addUniqueIndex(['calendarid', 'calendartype', 'uid'], 'calobjects_by_uid_index');
}
if ($schema->hasTable('calendarobjects_props')) {

@ -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;
@ -20,24 +21,19 @@ use Sabre\VObject\ITip\Message;
use Sabre\VObject\Reader;
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 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 +41,16 @@ class CalendarImplTest extends \Test\TestCase {
];
$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
);
}
public function testGetKey(): void {
$this->assertEquals($this->calendarImpl->getKey(), 'fancy_id_123');
$this->assertEquals($this->calendarImpl->getKey(), 1);
}
public function testGetDisplayname(): void {
@ -261,4 +260,48 @@ EOF;
$iTipMessage->message = $vObject;
return $iTipMessage;
}
protected function mockExportGenerator(): Generator {
foreach ($this->mockExportCollection as $entry) {
yield $entry;
}
}
public function testExport(): void {
// Arrange
// 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());
// Act
foreach ($this->calendarImpl->export(null) as $entry) {
$exported[] = $entry;
}
// Assert
$this->assertCount(1, $exported, 'Invalid exported items count');
}
}

@ -0,0 +1,80 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV\Export;
use Generator;
use OCA\DAV\CalDAV\Export\ExportService;
use OCP\Calendar\CalendarExportOptions;
use OCP\Calendar\ICalendarExport;
use OCP\ServerVersion;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Component\VCalendar;
class ExportServiceTest extends \Test\TestCase {
private ServerVersion|MockObject $serverVersion;
private ExportService $service;
private ICalendarExport|MockObject $calendar;
private array $mockExportCollection;
protected function setUp(): void {
parent::setUp();
$this->serverVersion = $this->createMock(ServerVersion::class);
$this->serverVersion->method('getVersionString')
->willReturn('32.0.0.0');
$this->service = new ExportService($this->serverVersion);
$this->calendar = $this->createMock(ICalendarExport::class);
}
protected function mockGenerator(): Generator {
foreach ($this->mockExportCollection as $entry) {
yield $entry;
}
}
public function testExport(): void {
// Arrange
// construct calendar with a 1 hour event and same start/end time zones
$vCalendar = new VCalendar();
/** @var \Sabre\VObject\Component\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());
// Act
$document = '';
foreach ($this->service->export($this->calendar, $options) as $chunk) {
$document .= $chunk;
}
// Assert
$this->assertStringContainsString('BEGIN:VCALENDAR', $document, 'Exported document calendar start missing');
$this->assertStringContainsString('BEGIN:VEVENT', $document, 'Exported document event start missing');
$this->assertStringContainsString('END:VEVENT', $document, 'Exported document event end missing');
$this->assertStringContainsString('END:VCALENDAR', $document, 'Exported document calendar end missing');
}
}

@ -191,6 +191,7 @@ return array(
'OCP\\Cache\\CappedMemoryCache' => $baseDir . '/lib/public/Cache/CappedMemoryCache.php',
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => $baseDir . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
'OCP\\Calendar\\CalendarEventStatus' => $baseDir . '/lib/public/Calendar/CalendarEventStatus.php',
'OCP\\Calendar\\CalendarExportOptions' => $baseDir . '/lib/public/Calendar/CalendarExportOptions.php',
'OCP\\Calendar\\Events\\AbstractCalendarObjectEvent' => $baseDir . '/lib/public/Calendar/Events/AbstractCalendarObjectEvent.php',
'OCP\\Calendar\\Events\\CalendarObjectCreatedEvent' => $baseDir . '/lib/public/Calendar/Events/CalendarObjectCreatedEvent.php',
'OCP\\Calendar\\Events\\CalendarObjectDeletedEvent' => $baseDir . '/lib/public/Calendar/Events/CalendarObjectDeletedEvent.php',
@ -202,6 +203,7 @@ return array(
'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\\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',

@ -232,6 +232,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/public/Cache/CappedMemoryCache.php',
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => __DIR__ . '/../../..' . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
'OCP\\Calendar\\CalendarEventStatus' => __DIR__ . '/../../..' . '/lib/public/Calendar/CalendarEventStatus.php',
'OCP\\Calendar\\CalendarExportOptions' => __DIR__ . '/../../..' . '/lib/public/Calendar/CalendarExportOptions.php',
'OCP\\Calendar\\Events\\AbstractCalendarObjectEvent' => __DIR__ . '/../../..' . '/lib/public/Calendar/Events/AbstractCalendarObjectEvent.php',
'OCP\\Calendar\\Events\\CalendarObjectCreatedEvent' => __DIR__ . '/../../..' . '/lib/public/Calendar/Events/CalendarObjectCreatedEvent.php',
'OCP\\Calendar\\Events\\CalendarObjectDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/Calendar/Events/CalendarObjectDeletedEvent.php',
@ -243,6 +244,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'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\\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',

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Calendar;
/**
* Calendar Export Options
*
* @since 32.0.0
*/
final class CalendarExportOptions {
/** @var 'ical'|'jcal'|'xcal' */
private string $format = 'ical';
private ?string $rangeStart = null;
private ?int $rangeCount = null;
/**
* Gets the export format
*
* @return 'ical'|'jcal'|'xcal' (defaults to ical)
*/
public function getFormat(): string {
return $this->format;
}
/**
* Sets the export format
*
* @param 'ical'|'jcal'|'xcal' $format
*/
public function setFormat(string $format): void {
$this->format = $format;
}
/**
* Gets the start of the range to export
*/
public function getRangeStart(): ?string {
return $this->rangeStart;
}
/**
* Sets the start of the range to export
*/
public function setRangeStart(?string $rangeStart): void {
$this->rangeStart = $rangeStart;
}
/**
* Gets the number of objects to export
*/
public function getRangeCount(): ?int {
return $this->rangeCount;
}
/**
* Sets the number of objects to export
*/
public function setRangeCount(?int $rangeCount): void {
$this->rangeCount = $rangeCount;
}
}

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Calendar;
use Generator;
/**
* ICalendar Interface Extension to export data
*
* @since 32.0.0
*/
interface ICalendarExport {
/**
* Export objects
*
* @since 32.0.0
*
* @param CalendarExportOptions|null $options
*
* @return Generator<\Sabre\VObject\Component\VCalendar>
*/
public function export(?CalendarExportOptions $options): Generator;
}
Loading…
Cancel
Save