Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ocp): calendar event builder api #49888

Merged
merged 3 commits into from
Jan 8, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
@@ -192,6 +192,7 @@
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => $baseDir . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
'OCP\\Calendar\\Exceptions\\CalendarException' => $baseDir . '/lib/public/Calendar/Exceptions/CalendarException.php',
'OCP\\Calendar\\ICalendar' => $baseDir . '/lib/public/Calendar/ICalendar.php',
'OCP\\Calendar\\ICalendarEventBuilder' => $baseDir . '/lib/public/Calendar/ICalendarEventBuilder.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',
@@ -1116,6 +1117,7 @@
'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php',
'OC\\Cache\\CappedMemoryCache' => $baseDir . '/lib/private/Cache/CappedMemoryCache.php',
'OC\\Cache\\File' => $baseDir . '/lib/private/Cache/File.php',
'OC\\Calendar\\CalendarEventBuilder' => $baseDir . '/lib/private/Calendar/CalendarEventBuilder.php',
'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php',
'OC\\Calendar\\Manager' => $baseDir . '/lib/private/Calendar/Manager.php',
'OC\\Calendar\\Resource\\Manager' => $baseDir . '/lib/private/Calendar/Resource/Manager.php',
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
@@ -241,6 +241,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => __DIR__ . '/../../..' . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
'OCP\\Calendar\\Exceptions\\CalendarException' => __DIR__ . '/../../..' . '/lib/public/Calendar/Exceptions/CalendarException.php',
'OCP\\Calendar\\ICalendar' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendar.php',
'OCP\\Calendar\\ICalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarEventBuilder.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',
@@ -1165,6 +1166,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php',
'OC\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/private/Cache/CappedMemoryCache.php',
'OC\\Cache\\File' => __DIR__ . '/../../..' . '/lib/private/Cache/File.php',
'OC\\Calendar\\CalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarEventBuilder.php',
'OC\\Calendar\\CalendarQuery' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarQuery.php',
'OC\\Calendar\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Manager.php',
'OC\\Calendar\\Resource\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Resource/Manager.php',
132 changes: 132 additions & 0 deletions lib/private/Calendar/CalendarEventBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OC\Calendar;

use DateTimeInterface;
use InvalidArgumentException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\ICalendarEventBuilder;
use OCP\Calendar\ICreateFromString;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;

class CalendarEventBuilder implements ICalendarEventBuilder {
private ?DateTimeInterface $startDate = null;
private ?DateTimeInterface $endDate = null;
private ?string $summary = null;
private ?string $description = null;
private ?string $location = null;
private ?array $organizer = null;
private array $attendees = [];

public function __construct(
private readonly string $uid,
private readonly ITimeFactory $timeFactory,
) {
}

public function setStartDate(DateTimeInterface $start): ICalendarEventBuilder {
$this->startDate = $start;
return $this;
}

public function setEndDate(DateTimeInterface $end): ICalendarEventBuilder {
$this->endDate = $end;
return $this;
}

public function setSummary(string $summary): ICalendarEventBuilder {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a max length that we should check, or does caldav take care of that, same for description?

$this->summary = $summary;
return $this;
}

public function setDescription(string $description): ICalendarEventBuilder {
$this->description = $description;
return $this;
}

public function setLocation(string $location): ICalendarEventBuilder {
$this->location = $location;
return $this;
}

public function setOrganizer(string $email, ?string $commonName = null): ICalendarEventBuilder {
$this->organizer = [$email, $commonName];
return $this;
}

public function addAttendee(string $email, ?string $commonName = null): ICalendarEventBuilder {
$this->attendees[] = [$email, $commonName];
return $this;
}

public function toIcs(): string {
if ($this->startDate === null) {
throw new InvalidArgumentException('Event is missing a start date');
}

if ($this->endDate === null) {
throw new InvalidArgumentException('Event is missing an end date');
}

if ($this->summary === null) {
throw new InvalidArgumentException('Event is missing a summary');
}

if ($this->organizer === null && $this->attendees !== []) {
throw new InvalidArgumentException('Event has attendees but is missing an organizer');
}

$vcalendar = new VCalendar();
$props = [
'UID' => $this->uid,
'DTSTAMP' => $this->timeFactory->now(),
'SUMMARY' => $this->summary,
'DTSTART' => $this->startDate,
'DTEND' => $this->endDate,
];
if ($this->description !== null) {
$props['DESCRIPTION'] = $this->description;
}
if ($this->location !== null) {
$props['LOCATION'] = $this->location;
}
/** @var VEvent $vevent */
$vevent = $vcalendar->add('VEVENT', $props);
if ($this->organizer !== null) {
self::addAttendeeToVEvent($vevent, 'ORGANIZER', $this->organizer);
}
foreach ($this->attendees as $attendee) {
self::addAttendeeToVEvent($vevent, 'ATTENDEE', $attendee);
}
return $vcalendar->serialize();
}

public function createInCalendar(ICreateFromString $calendar): string {
$fileName = $this->uid . '.ics';
$calendar->createFromString($fileName, $this->toIcs());
return $fileName;
}

/**
* @param array{0: string, 1: ?string} $tuple A tuple of [$email, $commonName] where $commonName may be null.
*/
private static function addAttendeeToVEvent(VEvent $vevent, string $name, array $tuple): void {
[$email, $cn] = $tuple;
if (!str_starts_with($email, 'mailto:')) {
$email = "mailto:$email";
}
$params = [];
if ($cn !== null) {
$params['CN'] = $cn;
}
$vevent->add($name, $email, $params);
}
}
30 changes: 19 additions & 11 deletions lib/private/Calendar/Manager.php
Original file line number Diff line number Diff line change
@@ -12,13 +12,15 @@
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICalendar;
use OCP\Calendar\ICalendarEventBuilder;
use OCP\Calendar\ICalendarIsShared;
use OCP\Calendar\ICalendarIsWritable;
use OCP\Calendar\ICalendarProvider;
use OCP\Calendar\ICalendarQuery;
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IHandleImipMessage;
use OCP\Calendar\IManager;
use OCP\Security\ISecureRandom;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VCalendar;
@@ -45,6 +47,7 @@ public function __construct(
private ContainerInterface $container,
private LoggerInterface $logger,
private ITimeFactory $timeFactory,
private ISecureRandom $random,
) {
}

@@ -216,21 +219,21 @@ public function handleIMipRequest(
string $recipient,
string $calendarData,
): bool {

$userCalendars = $this->getCalendarsForPrincipal($principalUri);
if (empty($userCalendars)) {
$this->logger->warning('iMip message could not be processed because user has no calendars');
return false;
}

/** @var VCalendar $vObject|null */
$calendarObject = Reader::read($calendarData);

if (!isset($calendarObject->METHOD) || $calendarObject->METHOD->getValue() !== 'REQUEST') {
$this->logger->warning('iMip message contains an incorrect or invalid method');
return false;
}

if (!isset($calendarObject->VEVENT)) {
$this->logger->warning('iMip message contains no event');
return false;
@@ -242,12 +245,12 @@ public function handleIMipRequest(
$this->logger->warning('iMip message event dose not contains a UID');
return false;
}

if (!isset($eventObject->ATTENDEE)) {
$this->logger->warning('iMip message event dose not contains any attendees');
return false;
}

foreach ($eventObject->ATTENDEE as $entry) {
$address = trim(str_replace('mailto:', '', $entry->getValue()));
if ($address === $recipient) {
@@ -259,17 +262,17 @@ public function handleIMipRequest(
$this->logger->warning('iMip message event does not contain a attendee that matches the recipient');
return false;
}

foreach ($userCalendars as $calendar) {

if (!$calendar instanceof ICalendarIsWritable && !$calendar instanceof ICalendarIsShared) {
continue;
}

if ($calendar->isDeleted() || !$calendar->isWritable() || $calendar->isShared()) {
continue;
}

if (!empty($calendar->search($recipient, ['ATTENDEE'], ['uid' => $eventObject->UID->getValue()]))) {
try {
if ($calendar instanceof IHandleImipMessage) {
@@ -282,7 +285,7 @@ public function handleIMipRequest(
}
}
}

$this->logger->warning('iMip message event could not be processed because the no corresponding event was found in any calendar');
return false;
}
@@ -464,4 +467,9 @@ public function handleIMipCancel(
return false;
}
}

public function createEventBuilder(): ICalendarEventBuilder {
$uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC);
return new CalendarEventBuilder($uid, $this->timeFactory);
}
}
110 changes: 110 additions & 0 deletions lib/public/Calendar/ICalendarEventBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCP\Calendar;

use DateTimeInterface;
use InvalidArgumentException;
use OCP\Calendar\Exceptions\CalendarException;

/**
* The calendar event builder can be used to conveniently build a calendar event and then serialize
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved
* it to a ICS string. The ICS string can be submitted to calendar instances implementing the
* {@see \OCP\Calendar\ICreateFromString} interface.
*
* Also note this class can not be injected directly with dependency injection.
* Instead, inject {@see \OCP\Calendar\IManager} and use
* {@see \OCP\Calendar\IManager::createEventBuilder()} afterwards.
*
* All setters return self to allow chaining method calls.
*
* @since 31.0.0
*/
interface ICalendarEventBuilder {
/**
* Set the start date, time and time zone.
* This property is required!
*
* @since 31.0.0
*/
public function setStartDate(DateTimeInterface $start): self;

/**
* Set the end date, time and time zone.
* This property is required!
*
* @since 31.0.0
*/
public function setEndDate(DateTimeInterface $end): self;

/**
* Set the event summary or title.
* This property is required!
*
* @since 31.0.0
*/
public function setSummary(string $summary): self;

/**
* Set the event description.
*
* @since 31.0.0
*/
public function setDescription(string $description): self;

/**
* Set the event location. It can either be a physical address or a URL.
*
* @since 31.0.0
*/
public function setLocation(string $location): self;

/**
* Set the event organizer.
* This property is required if attendees are added!
*
* The "mailto:" prefix is optional and will be added automatically if it is missing.
*
* @since 31.0.0
*/
public function setOrganizer(string $email, ?string $commonName = null): self;

/**
* Add a new attendee to the event.
* Adding at least one attendee requires also setting the organizer!
*
* The "mailto:" prefix is optional and will be added automatically if it is missing.
*
* @since 31.0.0
*/
public function addAttendee(string $email, ?string $commonName = null): self;

/**
* Serialize the built event to an ICS string if all required properties set.
*
* @since 31.0.0
*
* @return string The serialized ICS string
*
* @throws InvalidArgumentException If required properties were not set
*/
public function toIcs(): string;

/**
* Create the event in the given calendar.
*
* @since 31.0.0
*
* @return string The filename of the created event
*
* @throws InvalidArgumentException If required properties were not set
* @throws CalendarException If writing the event to the calendar fails
*/
public function createInCalendar(ICreateFromString $calendar): string;
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved
}
8 changes: 8 additions & 0 deletions lib/public/Calendar/IManager.php
Original file line number Diff line number Diff line change
@@ -157,4 +157,12 @@ public function handleIMipReply(string $principalUri, string $sender, string $re
* @since 25.0.0
*/
public function handleIMipCancel(string $principalUri, string $sender, ?string $replyTo, string $recipient, string $calendarData): bool;

/**
* Create a new event builder instance. Please have a look at its documentation and the
* \OCP\Calendar\ICreateFromString interface on how to use it.
*
* @since 31.0.0
*/
public function createEventBuilder(): ICalendarEventBuilder;
}
Loading
Loading