Merge pull request #30963 from nextcloud/feat/calendar-migration
commit
eeec6142ca
@ -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
|
||||
Loading…
Reference in New Issue