mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 08:44:07 -04:00
feat: Calendar Export
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
This commit is contained in:
parent
cd9f0350b0
commit
a2d4f8d3f1
15 changed files with 522 additions and 15 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
107
apps/dav/lib/CalDAV/Export/ExportService.php
Normal file
107
apps/dav/lib/CalDAV/Export/ExportService.php
Normal file
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
95
apps/dav/lib/Command/ExportCalendar.php
Normal file
95
apps/dav/lib/Command/ExportCalendar.php
Normal file
|
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
80
apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php
Normal file
80
apps/dav/tests/unit/CalDAV/Export/ExportServiceTest.php
Normal file
|
|
@ -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',
|
||||
|
|
|
|||
68
lib/public/Calendar/CalendarExportOptions.php
Normal file
68
lib/public/Calendar/CalendarExportOptions.php
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
31
lib/public/Calendar/ICalendarExport.php
Normal file
31
lib/public/Calendar/ICalendarExport.php
Normal file
|
|
@ -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…
Reference in a new issue