committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1288 additions and 0 deletions
-
3apps/dav/composer/composer/autoload_classmap.php
-
3apps/dav/composer/composer/autoload_static.php
-
3apps/dav/lib/AppInfo/Application.php
-
7apps/dav/lib/CalDAV/CalendarImpl.php
-
460apps/dav/lib/UserMigration/CalendarMigrator.php
-
32apps/dav/lib/UserMigration/CalendarMigratorException.php
-
32apps/dav/lib/UserMigration/InvalidCalendarException.php
-
137apps/dav/tests/integration/UserMigration/CalendarMigratorTest.php
-
42apps/dav/tests/integration/UserMigration/assets/event-alarms.ics
-
39apps/dav/tests/integration/UserMigration/assets/event-attendees.ics
-
35apps/dav/tests/integration/UserMigration/assets/event-categories.ics
-
33apps/dav/tests/integration/UserMigration/assets/event-complex-alarm-recurring.ics
-
87apps/dav/tests/integration/UserMigration/assets/event-complex-recurrence.ics
-
34apps/dav/tests/integration/UserMigration/assets/event-custom-color.ics
-
74apps/dav/tests/integration/UserMigration/assets/event-multiple-recurring.ics
-
62apps/dav/tests/integration/UserMigration/assets/event-multiple.ics
-
87apps/dav/tests/integration/UserMigration/assets/event-recurring.ics
-
34apps/dav/tests/integration/UserMigration/assets/event-timed.ics
-
41apps/dav/tests/integration/UserMigration/assets/journal-todo-event.ics
-
22apps/dav/tests/integration/UserMigration/assets/journal.ics
-
16apps/dav/tests/integration/UserMigration/assets/todo.ics
-
5lib/public/Calendar/ICalendar.php
@ -0,0 +1,460 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright 2022 Christopher Ng <chrng8@gmail.com> |
|||
* |
|||
* @author Christopher Ng <chrng8@gmail.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\DAV\UserMigration; |
|||
|
|||
use function Safe\substr; |
|||
use OCA\DAV\AppInfo\Application; |
|||
use OCA\DAV\CalDAV\CalDavBackend; |
|||
use OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin; |
|||
use OCA\DAV\CalDAV\Plugin as CalDAVPlugin; |
|||
use OCA\DAV\Connector\Sabre\CachingTree; |
|||
use OCA\DAV\Connector\Sabre\Server as SabreDavServer; |
|||
use OCA\DAV\RootCollection; |
|||
use OCP\Calendar\ICalendar; |
|||
use OCP\Calendar\IManager as ICalendarManager; |
|||
use OCP\Defaults; |
|||
use OCP\IL10N; |
|||
use OCP\IUser; |
|||
use OCP\UserMigration\IExportDestination; |
|||
use OCP\UserMigration\IImportSource; |
|||
use OCP\UserMigration\IMigrator; |
|||
use OCP\UserMigration\TMigratorBasicVersionHandling; |
|||
use Sabre\VObject\Component as VObjectComponent; |
|||
use Sabre\VObject\Component\VCalendar; |
|||
use Sabre\VObject\Component\VTimeZone; |
|||
use Sabre\VObject\Property\ICalendar\DateTime; |
|||
use Sabre\VObject\Reader as VObjectReader; |
|||
use Sabre\VObject\UUIDUtil; |
|||
use Safe\Exceptions\StringsException; |
|||
use Symfony\Component\Console\Output\OutputInterface; |
|||
use Throwable; |
|||
|
|||
class CalendarMigrator implements IMigrator { |
|||
|
|||
use TMigratorBasicVersionHandling; |
|||
|
|||
private CalDavBackend $calDavBackend; |
|||
|
|||
private ICalendarManager $calendarManager; |
|||
|
|||
// ICSExportPlugin is injected as the mergeObjects() method is required and is not to be used as a SabreDAV server plugin
|
|||
private ICSExportPlugin $icsExportPlugin; |
|||
|
|||
private Defaults $defaults; |
|||
|
|||
private IL10N $l10n; |
|||
|
|||
private SabreDavServer $sabreDavServer; |
|||
|
|||
private const USERS_URI_ROOT = 'principals/users/'; |
|||
|
|||
private const FILENAME_EXT = '.ics'; |
|||
|
|||
private const MIGRATED_URI_PREFIX = 'migrated-'; |
|||
|
|||
private const EXPORT_ROOT = Application::APP_ID . '/calendars/'; |
|||
|
|||
public function __construct( |
|||
CalDavBackend $calDavBackend, |
|||
ICalendarManager $calendarManager, |
|||
ICSExportPlugin $icsExportPlugin, |
|||
Defaults $defaults, |
|||
IL10N $l10n |
|||
) { |
|||
$this->calDavBackend = $calDavBackend; |
|||
$this->calendarManager = $calendarManager; |
|||
$this->icsExportPlugin = $icsExportPlugin; |
|||
$this->defaults = $defaults; |
|||
$this->l10n = $l10n; |
|||
|
|||
$root = new RootCollection(); |
|||
$this->sabreDavServer = new SabreDavServer(new CachingTree($root)); |
|||
$this->sabreDavServer->addPlugin(new CalDAVPlugin()); |
|||
} |
|||
|
|||
private function getPrincipalUri(IUser $user): string { |
|||
return CalendarMigrator::USERS_URI_ROOT . $user->getUID(); |
|||
} |
|||
|
|||
/** |
|||
* @return array{name: string, vCalendar: VCalendar} |
|||
* |
|||
* @throws CalendarMigratorException |
|||
* @throws InvalidCalendarException |
|||
*/ |
|||
private function getCalendarExportData(IUser $user, ICalendar $calendar, OutputInterface $output): array { |
|||
$userId = $user->getUID(); |
|||
$calendarId = $calendar->getKey(); |
|||
$calendarInfo = $this->calDavBackend->getCalendarById($calendarId); |
|||
|
|||
if (empty($calendarInfo)) { |
|||
throw new CalendarMigratorException("Invalid info for calendar ID $calendarId"); |
|||
} |
|||
|
|||
$uri = $calendarInfo['uri']; |
|||
$path = CalDAVPlugin::CALENDAR_ROOT . "/$userId/$uri"; |
|||
|
|||
/** |
|||
* @see \Sabre\CalDAV\ICSExportPlugin::httpGet() implementation reference |
|||
*/ |
|||
|
|||
$properties = $this->sabreDavServer->getProperties($path, [ |
|||
'{DAV:}resourcetype', |
|||
'{DAV:}displayname', |
|||
'{http://sabredav.org/ns}sync-token', |
|||
'{DAV:}sync-token', |
|||
'{http://apple.com/ns/ical/}calendar-color', |
|||
]); |
|||
|
|||
// Filter out invalid (e.g. deleted) calendars
|
|||
if (!isset($properties['{DAV:}resourcetype']) || !$properties['{DAV:}resourcetype']->is('{' . CalDAVPlugin::NS_CALDAV . '}calendar')) { |
|||
throw new InvalidCalendarException(); |
|||
} |
|||
|
|||
/** |
|||
* @see \Sabre\CalDAV\ICSExportPlugin::generateResponse() implementation reference |
|||
*/ |
|||
|
|||
$calDataProp = '{' . CalDAVPlugin::NS_CALDAV . '}calendar-data'; |
|||
$calendarNode = $this->sabreDavServer->tree->getNodeForPath($path); |
|||
$nodes = $this->sabreDavServer->getPropertiesIteratorForPath($path, [$calDataProp], 1); |
|||
|
|||
$blobs = []; |
|||
foreach ($nodes as $node) { |
|||
if (isset($node[200][$calDataProp])) { |
|||
$blobs[$node['href']] = $node[200][$calDataProp]; |
|||
} |
|||
} |
|||
|
|||
$mergedCalendar = $this->icsExportPlugin->mergeObjects( |
|||
$properties, |
|||
$blobs, |
|||
); |
|||
|
|||
$problems = $mergedCalendar->validate(); |
|||
if (!empty($problems)) { |
|||
$output->writeln('Skipping calendar "' . $properties['{DAV:}displayname'] . '" containing invalid calendar data'); |
|||
throw new InvalidCalendarException(); |
|||
} |
|||
|
|||
return [ |
|||
'name' => $calendarNode->getName(), |
|||
'vCalendar' => $mergedCalendar, |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* @return array<int, array{name: string, vCalendar: VCalendar}> |
|||
* |
|||
* @throws CalendarMigratorException |
|||
*/ |
|||
private function getCalendarExports(IUser $user, OutputInterface $output): array { |
|||
$principalUri = $this->getPrincipalUri($user); |
|||
|
|||
return array_values(array_filter(array_map( |
|||
function (ICalendar $calendar) use ($user, $output) { |
|||
try { |
|||
return $this->getCalendarExportData($user, $calendar, $output); |
|||
} catch (InvalidCalendarException $e) { |
|||
// Allow this exception as invalid (e.g. deleted) calendars are not to be exported
|
|||
return null; |
|||
} |
|||
}, |
|||
$this->calendarManager->getCalendarsForPrincipal($principalUri), |
|||
))); |
|||
} |
|||
|
|||
private function getUniqueCalendarUri(IUser $user, string $initialCalendarUri): string { |
|||
$principalUri = $this->getPrincipalUri($user); |
|||
try { |
|||
$initialCalendarUri = substr($initialCalendarUri, 0, strlen(CalendarMigrator::MIGRATED_URI_PREFIX)) === CalendarMigrator::MIGRATED_URI_PREFIX |
|||
? $initialCalendarUri |
|||
: CalendarMigrator::MIGRATED_URI_PREFIX . $initialCalendarUri; |
|||
} catch (StringsException $e) { |
|||
throw new CalendarMigratorException('Failed to get unique calendar URI', 0, $e); |
|||
} |
|||
|
|||
$existingCalendarUris = array_map( |
|||
fn (ICalendar $calendar) => $calendar->getUri(), |
|||
$this->calendarManager->getCalendarsForPrincipal($principalUri), |
|||
); |
|||
|
|||
$calendarUri = $initialCalendarUri; |
|||
$acc = 1; |
|||
while (in_array($calendarUri, $existingCalendarUris, true)) { |
|||
$calendarUri = $initialCalendarUri . "-$acc"; |
|||
++$acc; |
|||
} |
|||
|
|||
return $calendarUri; |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
*/ |
|||
public function export(IUser $user, IExportDestination $exportDestination, OutputInterface $output): void { |
|||
$output->writeln('Exporting calendars into ' . CalendarMigrator::EXPORT_ROOT . '…'); |
|||
|
|||
$calendarExports = $this->getCalendarExports($user, $output); |
|||
|
|||
if (empty($calendarExports)) { |
|||
$output->writeln('No calendars to export…'); |
|||
} |
|||
|
|||
/** |
|||
* @var string $name |
|||
* @var VCalendar $vCalendar |
|||
*/ |
|||
foreach ($calendarExports as ['name' => $name, 'vCalendar' => $vCalendar]) { |
|||
// Set filename to sanitized calendar name appended with the date
|
|||
$filename = preg_replace('/[^a-zA-Z0-9-_ ]/um', '', $name) . '_' . date('Y-m-d') . CalendarMigrator::FILENAME_EXT; |
|||
$exportPath = CalendarMigrator::EXPORT_ROOT . $filename; |
|||
|
|||
if ($exportDestination->addFileContents($exportPath, $vCalendar->serialize()) === false) { |
|||
throw new CalendarMigratorException('Could not export calendars'); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @return array<string, VTimeZone> |
|||
*/ |
|||
private function getCalendarTimezones(VCalendar $vCalendar): array { |
|||
/** @var VTimeZone[] $calendarTimezones */ |
|||
$calendarTimezones = array_filter( |
|||
$vCalendar->getComponents(), |
|||
fn ($component) => $component->name === 'VTIMEZONE', |
|||
); |
|||
|
|||
/** @var array<string, VTimeZone> $calendarTimezoneMap */ |
|||
$calendarTimezoneMap = []; |
|||
foreach ($calendarTimezones as $vTimeZone) { |
|||
$calendarTimezoneMap[$vTimeZone->getTimeZone()->getName()] = $vTimeZone; |
|||
} |
|||
|
|||
return $calendarTimezoneMap; |
|||
} |
|||
|
|||
/** |
|||
* @return VTimeZone[] |
|||
*/ |
|||
private function getTimezonesForComponent(VCalendar $vCalendar, VObjectComponent $component): array { |
|||
$componentTimezoneIds = []; |
|||
|
|||
foreach ($component->children() as $child) { |
|||
if ($child instanceof DateTime && isset($child->parameters['TZID'])) { |
|||
$timezoneId = $child->parameters['TZID']->getValue(); |
|||
if (!in_array($timezoneId, $componentTimezoneIds, true)) { |
|||
$componentTimezoneIds[] = $timezoneId; |
|||
} |
|||
} |
|||
} |
|||
|
|||
$calendarTimezoneMap = $this->getCalendarTimezones($vCalendar); |
|||
|
|||
return array_values(array_filter(array_map( |
|||
fn (string $timezoneId) => $calendarTimezoneMap[$timezoneId], |
|||
$componentTimezoneIds, |
|||
))); |
|||
} |
|||
|
|||
private function sanitizeComponent(VObjectComponent $component): VObjectComponent { |
|||
// Operate on the component clone to prevent mutation of the original
|
|||
$component = clone $component; |
|||
|
|||
// Remove RSVP parameters to prevent automatically sending invitation emails to attendees on import
|
|||
foreach ($component->children() as $child) { |
|||
if ( |
|||
$child->name === 'ATTENDEE' |
|||
&& isset($child->parameters['RSVP']) |
|||
) { |
|||
unset($child->parameters['RSVP']); |
|||
} |
|||
} |
|||
|
|||
return $component; |
|||
} |
|||
|
|||
/** |
|||
* @return VObjectComponent[] |
|||
*/ |
|||
private function getRequiredImportComponents(VCalendar $vCalendar, VObjectComponent $component): array { |
|||
$component = $this->sanitizeComponent($component); |
|||
/** @var array<int, VTimeZone> $timezoneComponents */ |
|||
$timezoneComponents = $this->getTimezonesForComponent($vCalendar, $component); |
|||
return [ |
|||
...$timezoneComponents, |
|||
$component, |
|||
]; |
|||
} |
|||
|
|||
private function initCalendarObject(): VCalendar { |
|||
$vCalendarObject = new VCalendar(); |
|||
$vCalendarObject->PRODID = '-//IDN nextcloud.com//Migrated calendar//EN'; |
|||
return $vCalendarObject; |
|||
} |
|||
|
|||
private function importCalendarObject(int $calendarId, VCalendar $vCalendarObject, OutputInterface $output): void { |
|||
try { |
|||
$this->calDavBackend->createCalendarObject( |
|||
$calendarId, |
|||
UUIDUtil::getUUID() . CalendarMigrator::FILENAME_EXT, |
|||
$vCalendarObject->serialize(), |
|||
CalDavBackend::CALENDAR_TYPE_CALENDAR, |
|||
); |
|||
} catch (Throwable $e) { |
|||
// Rollback creation of calendar on error
|
|||
$output->writeln('Error creating calendar object, rolling back creation of calendar…'); |
|||
$this->calDavBackend->deleteCalendar($calendarId, true); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* @throws CalendarMigratorException |
|||
*/ |
|||
private function importCalendar(IUser $user, string $filename, string $initialCalendarUri, VCalendar $vCalendar, OutputInterface $output): void { |
|||
$principalUri = $this->getPrincipalUri($user); |
|||
$calendarUri = $this->getUniqueCalendarUri($user, $initialCalendarUri); |
|||
|
|||
$calendarId = $this->calDavBackend->createCalendar($principalUri, $calendarUri, [ |
|||
'{DAV:}displayname' => isset($vCalendar->{'X-WR-CALNAME'}) ? $vCalendar->{'X-WR-CALNAME'}->getValue() : $this->l10n->t('Migrated calendar (%1$s)', [$filename]), |
|||
'{http://apple.com/ns/ical/}calendar-color' => isset($vCalendar->{'X-APPLE-CALENDAR-COLOR'}) ? $vCalendar->{'X-APPLE-CALENDAR-COLOR'}->getValue() : $this->defaults->getColorPrimary(), |
|||
'components' => implode( |
|||
',', |
|||
array_reduce( |
|||
$vCalendar->getComponents(), |
|||
function (array $componentNames, VObjectComponent $component) { |
|||
/** @var array<int, string> $componentNames */ |
|||
return !in_array($component->name, $componentNames, true) |
|||
? [...$componentNames, $component->name] |
|||
: $componentNames; |
|||
}, |
|||
[], |
|||
) |
|||
), |
|||
]); |
|||
|
|||
/** @var VObjectComponent[] $calendarComponents */ |
|||
$calendarComponents = array_values(array_filter( |
|||
$vCalendar->getComponents(), |
|||
// VTIMEZONE components are handled separately and added to the calendar object only if depended on by the component
|
|||
fn (VObjectComponent $component) => $component->name !== 'VTIMEZONE', |
|||
)); |
|||
|
|||
/** @var array<string, VObjectComponent[]> $groupedCalendarComponents */ |
|||
$groupedCalendarComponents = []; |
|||
/** @var VObjectComponent[] $ungroupedCalendarComponents */ |
|||
$ungroupedCalendarComponents = []; |
|||
|
|||
foreach ($calendarComponents as $component) { |
|||
if (isset($component->UID)) { |
|||
$uid = $component->UID->getValue(); |
|||
// Components with the same UID (e.g. recurring events) are grouped together into a single calendar object
|
|||
if (isset($groupedCalendarComponents[$uid])) { |
|||
$groupedCalendarComponents[$uid][] = $component; |
|||
} else { |
|||
$groupedCalendarComponents[$uid] = [$component]; |
|||
} |
|||
} else { |
|||
$ungroupedCalendarComponents[] = $component; |
|||
} |
|||
} |
|||
|
|||
foreach ($groupedCalendarComponents as $uid => $components) { |
|||
// Construct and import a calendar object containing all components of a group
|
|||
$vCalendarObject = $this->initCalendarObject(); |
|||
foreach ($components as $component) { |
|||
foreach ($this->getRequiredImportComponents($vCalendar, $component) as $component) { |
|||
$vCalendarObject->add($component); |
|||
} |
|||
} |
|||
$this->importCalendarObject($calendarId, $vCalendarObject, $output); |
|||
} |
|||
|
|||
foreach ($ungroupedCalendarComponents as $component) { |
|||
// Construct and import a calendar object for a single component
|
|||
$vCalendarObject = $this->initCalendarObject(); |
|||
foreach ($this->getRequiredImportComponents($vCalendar, $component) as $component) { |
|||
$vCalendarObject->add($component); |
|||
} |
|||
$this->importCalendarObject($calendarId, $vCalendarObject, $output); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* {@inheritDoc} |
|||
* |
|||
* @throws CalendarMigratorException |
|||
*/ |
|||
public function import(IUser $user, IImportSource $importSource, OutputInterface $output): void { |
|||
if ($importSource->getMigratorVersion(static::class) === null) { |
|||
$output->writeln('No version for ' . static::class . ', skipping import…'); |
|||
return; |
|||
} |
|||
|
|||
$output->writeln('Importing calendars from ' . CalendarMigrator::EXPORT_ROOT . '…'); |
|||
|
|||
$calendarImports = $importSource->getFolderListing(CalendarMigrator::EXPORT_ROOT); |
|||
if (empty($calendarImports)) { |
|||
$output->writeln('No calendars to import…'); |
|||
} |
|||
|
|||
foreach ($calendarImports as $filename) { |
|||
$importPath = CalendarMigrator::EXPORT_ROOT . $filename; |
|||
try { |
|||
/** @var VCalendar $vCalendar */ |
|||
$vCalendar = VObjectReader::read( |
|||
$importSource->getFileAsStream($importPath), |
|||
VObjectReader::OPTION_FORGIVING, |
|||
); |
|||
} catch (Throwable $e) { |
|||
throw new CalendarMigratorException("Failed to read file \"$importPath\"", 0, $e);
|
|||
} |
|||
|
|||
$problems = $vCalendar->validate(); |
|||
if (!empty($problems)) { |
|||
throw new CalendarMigratorException("Invalid calendar data contained in \"$importPath\"");
|
|||
} |
|||
|
|||
$splitFilename = explode('_', $filename, 2); |
|||
if (count($splitFilename) !== 2) { |
|||
throw new CalendarMigratorException("Invalid filename \"$filename\", expected filename of the format \"<calendar_name>_YYYY-MM-DD" . CalendarMigrator::FILENAME_EXT . '"'); |
|||
} |
|||
[$initialCalendarUri, $suffix] = $splitFilename; |
|||
|
|||
$this->importCalendar( |
|||
$user, |
|||
$filename, |
|||
$initialCalendarUri, |
|||
$vCalendar, |
|||
$output, |
|||
); |
|||
|
|||
$vCalendar->destroy(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright 2022 Christopher Ng <chrng8@gmail.com> |
|||
* |
|||
* @author Christopher Ng <chrng8@gmail.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\DAV\UserMigration; |
|||
|
|||
use Exception; |
|||
|
|||
class CalendarMigratorException extends Exception { |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright 2022 Christopher Ng <chrng8@gmail.com> |
|||
* |
|||
* @author Christopher Ng <chrng8@gmail.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\DAV\UserMigration; |
|||
|
|||
use Exception; |
|||
|
|||
class InvalidCalendarException extends Exception { |
|||
} |
|||
@ -0,0 +1,137 @@ |
|||
<?php |
|||
|
|||
declare(strict_types=1); |
|||
|
|||
/** |
|||
* @copyright 2022 Christopher Ng <chrng8@gmail.com> |
|||
* |
|||
* @author Christopher Ng <chrng8@gmail.com> |
|||
* |
|||
* @license GNU AGPL version 3 or any later version |
|||
* |
|||
* This program is free software: you can redistribute it and/or modify |
|||
* it under the terms of the GNU Affero General Public License as |
|||
* published by the Free Software Foundation, either version 3 of the |
|||
* License, or (at your option) any later version. |
|||
* |
|||
* This program is distributed in the hope that it will be useful, |
|||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
* GNU Affero General Public License for more details. |
|||
* |
|||
* You should have received a copy of the GNU Affero General Public License |
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
* |
|||
*/ |
|||
|
|||
namespace OCA\DAV\Tests\integration\UserMigration; |
|||
|
|||
use function Safe\scandir; |
|||
use OCA\DAV\AppInfo\Application; |
|||
use OCA\DAV\UserMigration\CalendarMigrator; |
|||
use OCP\AppFramework\App; |
|||
use OCP\IUserManager; |
|||
use Sabre\VObject\Component as VObjectComponent; |
|||
use Sabre\VObject\Component\VCalendar; |
|||
use Sabre\VObject\Property as VObjectProperty; |
|||
use Sabre\VObject\Reader as VObjectReader; |
|||
use Sabre\VObject\UUIDUtil; |
|||
use Symfony\Component\Console\Output\OutputInterface; |
|||
use Test\TestCase; |
|||
|
|||
/** |
|||
* @group DB |
|||
*/ |
|||
class CalendarMigratorTest extends TestCase { |
|||
|
|||
private IUserManager $userManager; |
|||
|
|||
private CalendarMigrator $migrator; |
|||
|
|||
private OutputInterface $output; |
|||
|
|||
private const ASSETS_DIR = __DIR__ . '/assets/'; |
|||
|
|||
protected function setUp(): void { |
|||
$app = new App(Application::APP_ID); |
|||
$container = $app->getContainer(); |
|||
|
|||
$this->userManager = $container->get(IUserManager::class); |
|||
$this->migrator = $container->get(CalendarMigrator::class); |
|||
$this->output = $this->createMock(OutputInterface::class); |
|||
} |
|||
|
|||
public function dataAssets(): array { |
|||
return array_map( |
|||
function (string $filename) { |
|||
/** @var VCalendar $vCalendar */ |
|||
$vCalendar = VObjectReader::read( |
|||
fopen(self::ASSETS_DIR . $filename, 'r'), |
|||
VObjectReader::OPTION_FORGIVING, |
|||
); |
|||
[$initialCalendarUri, $ext] = explode('.', $filename, 2); |
|||
return [UUIDUtil::getUUID(), $filename, $initialCalendarUri, $vCalendar]; |
|||
}, |
|||
array_diff( |
|||
scandir(self::ASSETS_DIR), |
|||
// Exclude current and parent directories
|
|||
['.', '..'], |
|||
), |
|||
); |
|||
} |
|||
|
|||
private function getProperties(VCalendar $vCalendar): array { |
|||
return array_map( |
|||
fn (VObjectProperty $property) => $property->serialize(), |
|||
array_values(array_filter( |
|||
$vCalendar->children(), |
|||
fn (mixed $child) => $child instanceof VObjectProperty, |
|||
)), |
|||
); |
|||
} |
|||
|
|||
private function getComponents(VCalendar $vCalendar): array { |
|||
return array_map( |
|||
// Elements of the serialized blob are sorted
|
|||
fn (VObjectComponent $component) => $component->serialize(), |
|||
$vCalendar->getComponents(), |
|||
); |
|||
} |
|||
|
|||
private function getSanitizedComponents(VCalendar $vCalendar): array { |
|||
return array_map( |
|||
// Elements of the serialized blob are sorted
|
|||
fn (VObjectComponent $component) => $this->invokePrivate($this->migrator, 'sanitizeComponent', [$component])->serialize(), |
|||
$vCalendar->getComponents(), |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* @dataProvider dataAssets |
|||
*/ |
|||
public function testImportExportAsset(string $userId, string $filename, string $initialCalendarUri, VCalendar $importCalendar): void { |
|||
$user = $this->userManager->createUser($userId, 'topsecretpassword'); |
|||
|
|||
$problems = $importCalendar->validate(); |
|||
$this->assertEmpty($problems); |
|||
|
|||
$this->invokePrivate($this->migrator, 'importCalendar', [$user, $filename, $initialCalendarUri, $importCalendar, $this->output]); |
|||
|
|||
$calendarExports = $this->invokePrivate($this->migrator, 'getCalendarExports', [$user, $this->output]); |
|||
$this->assertCount(1, $calendarExports); |
|||
|
|||
/** @var VCalendar $exportCalendar */ |
|||
['vCalendar' => $exportCalendar] = reset($calendarExports); |
|||
|
|||
$this->assertEqualsCanonicalizing( |
|||
$this->getProperties($importCalendar), |
|||
$this->getProperties($exportCalendar), |
|||
); |
|||
|
|||
$this->assertEqualsCanonicalizing( |
|||
// Components are expected to be sanitized on import
|
|||
$this->getSanitizedComponents($importCalendar), |
|||
$this->getComponents($exportCalendar), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
CALSCALE:GREGORIAN |
|||
X-WR-CALNAME:Alarms |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
BEGIN:VTIMEZONE |
|||
TZID:Europe/Berlin |
|||
BEGIN:DAYLIGHT |
|||
TZOFFSETFROM:+0100 |
|||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU |
|||
DTSTART:19810329T020000 |
|||
TZNAME:GMT+2 |
|||
TZOFFSETTO:+0200 |
|||
END:DAYLIGHT |
|||
BEGIN:STANDARD |
|||
TZOFFSETFROM:+0200 |
|||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU |
|||
DTSTART:19961027T030000 |
|||
TZNAME:GMT+1 |
|||
TZOFFSETTO:+0100 |
|||
END:STANDARD |
|||
END:VTIMEZONE |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
DTEND;TZID=Europe/Berlin:20160816T100000 |
|||
TRANSP:OPAQUE |
|||
SUMMARY:Test Europe Berlin |
|||
DTSTART;TZID=Europe/Berlin:20160816T090000 |
|||
DTSTAMP:20160809T163632Z |
|||
SEQUENCE:0 |
|||
BEGIN:VALARM |
|||
ACTION:DISPLAY |
|||
TRIGGER;RELATED=START:P1DT9H |
|||
END:VALARM |
|||
BEGIN:VALARM |
|||
ACTION:DISPLAY |
|||
TRIGGER;VALUE=DATE-TIME:20200306T083000Z |
|||
END:VALARM |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,39 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
CALSCALE:GREGORIAN |
|||
X-WR-CALNAME:Attendees |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
BEGIN:VTIMEZONE |
|||
TZID:Europe/Berlin |
|||
BEGIN:DAYLIGHT |
|||
TZOFFSETFROM:+0100 |
|||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU |
|||
DTSTART:19810329T020000 |
|||
TZNAME:GMT+2 |
|||
TZOFFSETTO:+0200 |
|||
END:DAYLIGHT |
|||
BEGIN:STANDARD |
|||
TZOFFSETFROM:+0200 |
|||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU |
|||
DTSTART:19961027T030000 |
|||
TZNAME:GMT+1 |
|||
TZOFFSETTO:+0100 |
|||
END:STANDARD |
|||
END:VTIMEZONE |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
DTEND;TZID=Europe/Berlin:20160816T100000 |
|||
TRANSP:OPAQUE |
|||
SUMMARY:Test Europe Berlin |
|||
DTSTART;TZID=Europe/Berlin:20160816T090000 |
|||
DTSTAMP:20160809T163632Z |
|||
SEQUENCE:0 |
|||
ORGANIZER;CN=John Smith:mailto:jsmith@example.com |
|||
ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT:mailto:jsmith@example.com |
|||
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Henry Cabot:mailto:hcabot@example.com |
|||
ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="mailto:bob@example.com";PARTSTAT=ACCEPTED;CN=Jane Doe:mailto:jdoe@example.com |
|||
ATTENDEE;ROLE=NON-PARTICIPANT;PARTSTAT=DELEGATED;DELEGATED-TO="mailto:hcabot@example.com";CN=The Big Cheese:mailto:iamboss@example.com |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,35 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
CALSCALE:GREGORIAN |
|||
X-WR-CALNAME:Categories |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
BEGIN:VTIMEZONE |
|||
TZID:Europe/Berlin |
|||
BEGIN:DAYLIGHT |
|||
TZOFFSETFROM:+0100 |
|||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU |
|||
DTSTART:19810329T020000 |
|||
TZNAME:GMT+2 |
|||
TZOFFSETTO:+0200 |
|||
END:DAYLIGHT |
|||
BEGIN:STANDARD |
|||
TZOFFSETFROM:+0200 |
|||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU |
|||
DTSTART:19961027T030000 |
|||
TZNAME:GMT+1 |
|||
TZOFFSETTO:+0100 |
|||
END:STANDARD |
|||
END:VTIMEZONE |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
DTEND;TZID=Europe/Berlin:20160816T100000 |
|||
TRANSP:OPAQUE |
|||
SUMMARY:Test Europe Berlin |
|||
DTSTART;TZID=Europe/Berlin:20160816T090000 |
|||
DTSTAMP:20160809T163632Z |
|||
SEQUENCE:0 |
|||
CATEGORIES:BUSINESS,HUMAN RESOURCES |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,33 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
CALSCALE:GREGORIAN |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
X-WR-CALNAME:Complex alarm recurring |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T031205Z |
|||
DTSTAMP:20220218T031409Z |
|||
LAST-MODIFIED:20220218T031409Z |
|||
SEQUENCE:2 |
|||
UID:b78f3a65-413d-4fa7-b125-1232bc6a2c72 |
|||
DTSTART;VALUE=DATE:20220217 |
|||
DTEND;VALUE=DATE:20220218 |
|||
STATUS:TENTATIVE |
|||
SUMMARY:Complex recurring event |
|||
LOCATION:Antarctica |
|||
DESCRIPTION:Event description |
|||
CLASS:CONFIDENTIAL |
|||
TRANSP:TRANSPARENT |
|||
CATEGORIES:Personal,Travel,Special occasion |
|||
COLOR:khaki |
|||
RRULE:FREQ=MONTHLY;COUNT=10;BYDAY=WE;BYSETPOS=2 |
|||
BEGIN:VALARM |
|||
ACTION:DISPLAY |
|||
TRIGGER;RELATED=START:-P6DT15H |
|||
END:VALARM |
|||
BEGIN:VALARM |
|||
ACTION:DISPLAY |
|||
TRIGGER;RELATED=START:-PT15H |
|||
END:VALARM |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,87 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
CALSCALE:GREGORIAN |
|||
X-WR-CALNAME:Complex recurrence |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
BEGIN:VTIMEZONE |
|||
TZID:Europe/Berlin |
|||
BEGIN:DAYLIGHT |
|||
TZOFFSETFROM:+0100 |
|||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU |
|||
DTSTART:19810329T020000 |
|||
TZNAME:GMT+2 |
|||
TZOFFSETTO:+0200 |
|||
END:DAYLIGHT |
|||
BEGIN:STANDARD |
|||
TZOFFSETFROM:+0200 |
|||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU |
|||
DTSTART:19961027T030000 |
|||
TZNAME:GMT+1 |
|||
TZOFFSETTO:+0100 |
|||
END:STANDARD |
|||
END:VTIMEZONE |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST |
|||
RRULE:FREQ=WEEKLY |
|||
DTSTART;TZID=Europe/Berlin:20200301T150000 |
|||
DTEND;TZID=Europe/Berlin:20200301T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 1 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200308T150000 |
|||
DTSTART;TZID=Europe/Berlin:20200401T150000 |
|||
DTEND;TZID=Europe/Berlin:20200401T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 2 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200315T150000 |
|||
DTSTART;TZID=Europe/Berlin:20201101T150000 |
|||
DTEND;TZID=Europe/Berlin:20201101T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 3 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200405T150000 |
|||
DTSTART;TZID=Europe/Berlin:20200406T150000 |
|||
DTEND;TZID=Europe/Berlin:20200406T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 4 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200412T150000 |
|||
DTSTART;TZID=Europe/Berlin:20201201T150000 |
|||
DTEND;TZID=Europe/Berlin:20201201T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 5 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200426T150000 |
|||
DTSTART;TZID=Europe/Berlin:20200410T150000 |
|||
DTEND;TZID=Europe/Berlin:20200410T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:INVALID RECURRENCE-ID |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200427T150000 |
|||
DTSTART;TZID=Europe/Berlin:20200420T150000 |
|||
DTEND;TZID=Europe/Berlin:20200420T160000 |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,34 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
X-WR-CALNAME:Personal |
|||
X-APPLE-CALENDAR-COLOR:#f264ab |
|||
CALSCALE:GREGORIAN |
|||
BEGIN:VTIMEZONE |
|||
TZID:Europe/Berlin |
|||
BEGIN:DAYLIGHT |
|||
TZOFFSETFROM:+0100 |
|||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU |
|||
DTSTART:19810329T020000 |
|||
TZNAME:GMT+2 |
|||
TZOFFSETTO:+0200 |
|||
END:DAYLIGHT |
|||
BEGIN:STANDARD |
|||
TZOFFSETFROM:+0200 |
|||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU |
|||
DTSTART:19961027T030000 |
|||
TZNAME:GMT+1 |
|||
TZOFFSETTO:+0100 |
|||
END:STANDARD |
|||
END:VTIMEZONE |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
DTEND;TZID=Europe/Berlin:20160816T100000 |
|||
TRANSP:OPAQUE |
|||
SUMMARY:Test Europe Berlin |
|||
DTSTART;TZID=Europe/Berlin:20160816T090000 |
|||
DTSTAMP:20160809T163632Z |
|||
SEQUENCE:0 |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,74 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
CALSCALE:GREGORIAN |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
X-WR-CALNAME:Multiple and recurring |
|||
X-APPLE-CALENDAR-COLOR:#795AAB |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044833Z |
|||
DTSTAMP:20220218T044837Z |
|||
LAST-MODIFIED:20220218T044837Z |
|||
SEQUENCE:2 |
|||
UID:dc343863-b57c-43a5-9ba4-19ae2740cd7e |
|||
DTSTART;VALUE=DATE:20220607 |
|||
DTEND;VALUE=DATE:20220608 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 4 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044806Z |
|||
DTSTAMP:20220218T044809Z |
|||
LAST-MODIFIED:20220218T044809Z |
|||
SEQUENCE:2 |
|||
UID:ae28b642-7e11-4e16-818a-06c89ae74f44 |
|||
DTSTART;VALUE=DATE:20220218 |
|||
DTEND;VALUE=DATE:20220219 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 1 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044820Z |
|||
DTSTAMP:20220218T044827Z |
|||
LAST-MODIFIED:20220218T044827Z |
|||
SEQUENCE:2 |
|||
UID:5edfb90e-44b3-47c6-863b-f632327f46f0 |
|||
DTSTART;VALUE=DATE:20220518 |
|||
DTEND;VALUE=DATE:20220519 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 3 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044810Z |
|||
DTSTAMP:20220218T044814Z |
|||
LAST-MODIFIED:20220218T044814Z |
|||
SEQUENCE:2 |
|||
UID:9789f684-1cf9-4ee7-90cb-54cdec6a6f03 |
|||
DTSTART;VALUE=DATE:20220223 |
|||
DTEND;VALUE=DATE:20220224 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 2 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044932Z |
|||
DTSTAMP:20220218T044945Z |
|||
LAST-MODIFIED:20220218T044945Z |
|||
SEQUENCE:2 |
|||
UID:d5bdaf0e-d6c7-4e30-a730-04928976a1d2 |
|||
DTSTART;VALUE=DATE:20221102 |
|||
DTEND;VALUE=DATE:20221103 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Recurring event |
|||
RRULE:FREQ=WEEKLY;BYDAY=WE |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044915Z |
|||
DTSTAMP:20220218T044918Z |
|||
LAST-MODIFIED:20220218T044918Z |
|||
SEQUENCE:2 |
|||
UID:11c3d9fd-fb54-4384-ab68-40b5d6acef6f |
|||
DTSTART;VALUE=DATE:20221010 |
|||
DTEND;VALUE=DATE:20221011 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 5 |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,62 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
CALSCALE:GREGORIAN |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
X-WR-CALNAME:Multiple |
|||
X-APPLE-CALENDAR-COLOR:#795AAB |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044833Z |
|||
DTSTAMP:20220218T044837Z |
|||
LAST-MODIFIED:20220218T044837Z |
|||
SEQUENCE:2 |
|||
UID:dc343863-b57c-43a5-9ba4-19ae2740cd7e |
|||
DTSTART;VALUE=DATE:20220607 |
|||
DTEND;VALUE=DATE:20220608 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 4 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044806Z |
|||
DTSTAMP:20220218T044809Z |
|||
LAST-MODIFIED:20220218T044809Z |
|||
SEQUENCE:2 |
|||
UID:ae28b642-7e11-4e16-818a-06c89ae74f44 |
|||
DTSTART;VALUE=DATE:20220218 |
|||
DTEND;VALUE=DATE:20220219 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 1 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044820Z |
|||
DTSTAMP:20220218T044827Z |
|||
LAST-MODIFIED:20220218T044827Z |
|||
SEQUENCE:2 |
|||
UID:5edfb90e-44b3-47c6-863b-f632327f46f0 |
|||
DTSTART;VALUE=DATE:20220518 |
|||
DTEND;VALUE=DATE:20220519 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 3 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044810Z |
|||
DTSTAMP:20220218T044814Z |
|||
LAST-MODIFIED:20220218T044814Z |
|||
SEQUENCE:2 |
|||
UID:9789f684-1cf9-4ee7-90cb-54cdec6a6f03 |
|||
DTSTART;VALUE=DATE:20220223 |
|||
DTEND;VALUE=DATE:20220224 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 2 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20220218T044915Z |
|||
DTSTAMP:20220218T044918Z |
|||
LAST-MODIFIED:20220218T044918Z |
|||
SEQUENCE:2 |
|||
UID:11c3d9fd-fb54-4384-ab68-40b5d6acef6f |
|||
DTSTART;VALUE=DATE:20221010 |
|||
DTEND;VALUE=DATE:20221011 |
|||
STATUS:CONFIRMED |
|||
SUMMARY:Event 5 |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,87 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
CALSCALE:GREGORIAN |
|||
X-WR-CALNAME:Recurring |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
BEGIN:VTIMEZONE |
|||
TZID:Europe/Berlin |
|||
BEGIN:DAYLIGHT |
|||
TZOFFSETFROM:+0100 |
|||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU |
|||
DTSTART:19810329T020000 |
|||
TZNAME:GMT+2 |
|||
TZOFFSETTO:+0200 |
|||
END:DAYLIGHT |
|||
BEGIN:STANDARD |
|||
TZOFFSETFROM:+0200 |
|||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU |
|||
DTSTART:19961027T030000 |
|||
TZNAME:GMT+1 |
|||
TZOFFSETTO:+0100 |
|||
END:STANDARD |
|||
END:VTIMEZONE |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST |
|||
RRULE:FREQ=WEEKLY |
|||
DTSTART;TZID=Europe/Berlin:20200301T150000 |
|||
DTEND;TZID=Europe/Berlin:20200301T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 1 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200308T150000 |
|||
DTSTART;TZID=Europe/Berlin:20200401T150000 |
|||
DTEND;TZID=Europe/Berlin:20200401T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 2 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200315T150000 |
|||
DTSTART;TZID=Europe/Berlin:20201101T150000 |
|||
DTEND;TZID=Europe/Berlin:20201101T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 3 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200405T150000 |
|||
DTSTART;TZID=Europe/Berlin:20200406T150000 |
|||
DTEND;TZID=Europe/Berlin:20200406T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 4 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200412T150000 |
|||
DTSTART;TZID=Europe/Berlin:20201201T150000 |
|||
DTEND;TZID=Europe/Berlin:20201201T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:TEST EX 5 |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200426T150000 |
|||
DTSTART;TZID=Europe/Berlin:20200410T150000 |
|||
DTEND;TZID=Europe/Berlin:20200410T160000 |
|||
END:VEVENT |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
DTSTAMP:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
SUMMARY:INVALID RECURRENCE-ID |
|||
RECURRENCE-ID;TZID=Europe/Berlin:20200427T150000 |
|||
DTSTART;TZID=Europe/Berlin:20200420T150000 |
|||
DTEND;TZID=Europe/Berlin:20200420T160000 |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,34 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
X-WR-CALNAME:Timed |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
CALSCALE:GREGORIAN |
|||
BEGIN:VTIMEZONE |
|||
TZID:Europe/Berlin |
|||
BEGIN:DAYLIGHT |
|||
TZOFFSETFROM:+0100 |
|||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU |
|||
DTSTART:19810329T020000 |
|||
TZNAME:GMT+2 |
|||
TZOFFSETTO:+0200 |
|||
END:DAYLIGHT |
|||
BEGIN:STANDARD |
|||
TZOFFSETFROM:+0200 |
|||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU |
|||
DTSTART:19961027T030000 |
|||
TZNAME:GMT+1 |
|||
TZOFFSETTO:+0100 |
|||
END:STANDARD |
|||
END:VTIMEZONE |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
DTEND;TZID=Europe/Berlin:20160816T100000 |
|||
TRANSP:OPAQUE |
|||
SUMMARY:Test Europe Berlin |
|||
DTSTART;TZID=Europe/Berlin:20160816T090000 |
|||
DTSTAMP:20160809T163632Z |
|||
SEQUENCE:0 |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,41 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
CALSCALE:GREGORIAN |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
X-WR-CALNAME:Journal Todo Event |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
BEGIN:VJOURNAL |
|||
UID:19970901T130000Z-123405@example.com |
|||
DTSTAMP:19970901T130000Z |
|||
DTSTART;VALUE=DATE:19970317 |
|||
SUMMARY:Staff meeting minutes |
|||
DESCRIPTION:1. Staff meeting: Participants include Joe\, |
|||
Lisa\, and Bob. Aurora project plans were reviewed. |
|||
There is currently no budget reserves for this project. |
|||
Lisa will escalate to management. Next meeting on Tuesday.\n |
|||
2. Telephone Conference: ABC Corp. sales representative |
|||
called to discuss new printer. Promised to get us a demo by |
|||
Friday.\n3. Henry Miller (Handsoff Insurance): Car was |
|||
totaled by tree. Is looking into a loaner car. 555-2323 |
|||
(tel). |
|||
END:VJOURNAL |
|||
BEGIN:VTODO |
|||
UID:20070313T123432Z-456553@example.com |
|||
DTSTAMP:20070313T123432Z |
|||
DUE;VALUE=DATE:20070501 |
|||
SUMMARY:Submit Quebec Income Tax Return for 2006 |
|||
CLASS:CONFIDENTIAL |
|||
CATEGORIES:FAMILY,FINANCE |
|||
STATUS:NEEDS-ACTION |
|||
END:VTODO |
|||
BEGIN:VEVENT |
|||
CREATED:20160809T163629Z |
|||
UID:0AD16F58-01B3-463B-A215-FD09FC729A02 |
|||
DTEND:20160816T100000 |
|||
TRANSP:OPAQUE |
|||
SUMMARY:Test Event |
|||
DTSTART:20160816T090000 |
|||
DTSTAMP:20160809T163632Z |
|||
SEQUENCE:0 |
|||
END:VEVENT |
|||
END:VCALENDAR |
|||
@ -0,0 +1,22 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
CALSCALE:GREGORIAN |
|||
X-WR-CALNAME:Journal |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
BEGIN:VJOURNAL |
|||
UID:19970901T130000Z-123405@example.com |
|||
DTSTAMP:19970901T130000Z |
|||
DTSTART;VALUE=DATE:19970317 |
|||
SUMMARY:Staff meeting minutes |
|||
DESCRIPTION:1. Staff meeting: Participants include Joe\, |
|||
Lisa\, and Bob. Aurora project plans were reviewed. |
|||
There is currently no budget reserves for this project. |
|||
Lisa will escalate to management. Next meeting on Tuesday.\n |
|||
2. Telephone Conference: ABC Corp. sales representative |
|||
called to discuss new printer. Promised to get us a demo by |
|||
Friday.\n3. Henry Miller (Handsoff Insurance): Car was |
|||
totaled by tree. Is looking into a loaner car. 555-2323 |
|||
(tel). |
|||
END:VJOURNAL |
|||
END:VCALENDAR |
|||
@ -0,0 +1,16 @@ |
|||
BEGIN:VCALENDAR |
|||
VERSION:2.0 |
|||
PRODID:-//SabreDAV//SabreDAV//EN |
|||
CALSCALE:GREGORIAN |
|||
X-WR-CALNAME:Todo |
|||
X-APPLE-CALENDAR-COLOR:#0082c9 |
|||
BEGIN:VTODO |
|||
UID:20070313T123432Z-456553@example.com |
|||
DTSTAMP:20070313T123432Z |
|||
DUE;VALUE=DATE:20070501 |
|||
SUMMARY:Submit Quebec Income Tax Return for 2006 |
|||
CLASS:CONFIDENTIAL |
|||
CATEGORIES:FAMILY,FINANCE |
|||
STATUS:NEEDS-ACTION |
|||
END:VTODO |
|||
END:VCALENDAR |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue