Merge pull request #45547 from nextcloud/feature/recurrence-invitations2

feature: Improved Recurrence Invitations Messages
pull/46607/head
Sebastian Krupinski 10 months ago committed by GitHub
commit 31d051ae74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      apps/dav/composer/composer/autoload_classmap.php
  2. 3
      apps/dav/composer/composer/autoload_static.php
  3. 758
      apps/dav/lib/CalDAV/EventReader.php
  4. 35
      apps/dav/lib/CalDAV/EventReaderRDate.php
  5. 87
      apps/dav/lib/CalDAV/EventReaderRRule.php
  6. 515
      apps/dav/lib/CalDAV/Schedule/IMipService.php
  7. 1025
      apps/dav/tests/unit/CalDAV/EventReaderTest.php
  8. 1307
      apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php

@ -59,6 +59,9 @@ return array(
'OCA\\DAV\\CalDAV\\CalendarProvider' => $baseDir . '/../lib/CalDAV/CalendarProvider.php',
'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php',
'OCA\\DAV\\CalDAV\\EventComparisonService' => $baseDir . '/../lib/CalDAV/EventComparisonService.php',
'OCA\\DAV\\CalDAV\\EventReader' => $baseDir . '/../lib/CalDAV/EventReader.php',
'OCA\\DAV\\CalDAV\\EventReaderRDate' => $baseDir . '/../lib/CalDAV/EventReaderRDate.php',
'OCA\\DAV\\CalDAV\\EventReaderRRule' => $baseDir . '/../lib/CalDAV/EventReaderRRule.php',
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => $baseDir . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php',

@ -74,6 +74,9 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\CalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarProvider.php',
'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php',
'OCA\\DAV\\CalDAV\\EventComparisonService' => __DIR__ . '/..' . '/../lib/CalDAV/EventComparisonService.php',
'OCA\\DAV\\CalDAV\\EventReader' => __DIR__ . '/..' . '/../lib/CalDAV/EventReader.php',
'OCA\\DAV\\CalDAV\\EventReaderRDate' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRDate.php',
'OCA\\DAV\\CalDAV\\EventReaderRRule' => __DIR__ . '/..' . '/../lib/CalDAV/EventReaderRRule.php',
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => __DIR__ . '/..' . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php',

@ -0,0 +1,758 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
use DateTime;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Reader;
class EventReader {
protected VEvent $baseEvent;
protected DateTimeInterface $baseEventStartDate;
protected DateTimeZone $baseEventStartTimeZone;
protected DateTimeInterface $baseEventEndDate;
protected DateTimeZone $baseEventEndTimeZone;
protected bool $baseEventStartDateFloating = false;
protected bool $baseEventEndDateFloating = false;
protected int $baseEventDuration;
protected ?EventReaderRRule $rruleIterator = null;
protected ?EventReaderRDate $rdateIterator = null;
protected ?EventReaderRRule $eruleIterator = null;
protected ?EventReaderRDate $edateIterator = null;
protected array $recurrenceModified;
protected ?DateTimeInterface $recurrenceCurrentDate;
protected array $dayNamesMap = [
'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday', 'SU' => 'Sunday'
];
protected array $monthNamesMap = [
1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April', 5 => 'May', 6 => 'June',
7 => 'July', 8 => 'August', 9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
];
protected array $relativePositionNamesMap = [
1 => 'First', 2 => 'Second', 3 => 'Third', 4 => 'Fourth', 5 => 'Fifty',
-1 => 'Last', -2 => 'Second Last', -3 => 'Third Last', -4 => 'Fourth Last', -5 => 'Fifty Last'
];
/**
* Initilizes the Event Reader
*
* There is several ways to set up the iterator.
*
* 1. You can pass a VCALENDAR component (as object or string) and a UID.
* 2. You can pass an array of VEVENTs (all UIDS should match).
* 3. You can pass a single VEVENT component (as object or string).
*
* Only the second method is recommended. The other 1 and 3 will be removed
* at some point in the future.
*
* The $uid parameter is only required for the first method.
*
* @since 30.0.0
*
* @param VCalendar|VEvent|Array|String $input
* @param string|null $uid
* @param DateTimeZone|null $timeZone reference timezone for floating dates and times
*/
public function __construct(VCalendar|VEvent|array|string $input, ?string $uid = null, ?DateTimeZone $timeZone = null) {
// evaluate if the input is a string and convert it to and vobject if required
if (is_string($input)) {
$input = Reader::read($input);
}
// evaluate if input is a single event vobject and convert it to a collection
if ($input instanceof VEvent) {
$events = [$input];
}
// evaluate if input is a calendar vobject
elseif ($input instanceof VCalendar) {
// Calendar + UID mode.
if ($uid === null) {
throw new InvalidArgumentException('The UID argument is required when a VCALENDAR object is used');
}
// extract events from calendar
$events = $input->getByUID($uid);
// evaluate if any event where found
if (count($events) === 0) {
throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid);
}
// extract calendar timezone
if (isset($input->VTIMEZONE) && isset($input->VTIMEZONE->TZID)) {
$calendarTimeZone = new DateTimeZone($input->VTIMEZONE->TZID->getValue());
}
}
// evaluate if input is a collection of event vobjects
elseif (is_array($input)) {
$events = $input;
} else {
throw new InvalidArgumentException('Invalid input data type');
}
// find base event instance and remove it from events collection
foreach ($events as $key => $vevent) {
if (!isset($vevent->{'RECURRENCE-ID'})) {
$this->baseEvent = $vevent;
unset($events[$key]);
}
}
// No base event was found. CalDAV does allow cases where only
// overridden instances are stored.
//
// In this particular case, we're just going to grab the first
// event and use that instead. This may not always give the
// desired result.
if (!isset($this->baseEvent) && count($events) > 0) {
$this->baseEvent = array_shift($events);
}
// determain the event starting time zone
// we require this to align all other dates times
// evaluate if timezone paramater was used (treat this as a override)
if ($timeZone !== null) {
$this->baseEventStartTimeZone = $timeZone;
}
// evaluate if event start date has a timezone parameter
elseif (isset($this->baseEvent->DTSTART->parameters['TZID'])) {
$this->baseEventStartTimeZone = new DateTimeZone($this->baseEvent->DTSTART->parameters['TZID']->getValue());
}
// evaluate if event parent calendar has a time zone
elseif (isset($calendarTimeZone)) {
$this->baseEventStartTimeZone = clone $calendarTimeZone;
}
// otherwise, as a last resort use the UTC timezone
else {
$this->baseEventStartTimeZone = new DateTimeZone('UTC');
}
// determain the event end time zone
// we require this to align all other dates and times
// evaluate if timezone paramater was used (treat this as a override)
if ($timeZone !== null) {
$this->baseEventEndTimeZone = $timeZone;
}
// evaluate if event end date has a timezone parameter
elseif (isset($this->baseEvent->DTEND->parameters['TZID'])) {
$this->baseEventEndTimeZone = new DateTimeZone($this->baseEvent->DTEND->parameters['TZID']->getValue());
}
// evaluate if event parent calendar has a time zone
elseif (isset($calendarTimeZone)) {
$this->baseEventEndTimeZone = clone $calendarTimeZone;
}
// otherwise, as a last resort use the start date time zone
else {
$this->baseEventEndTimeZone = clone $this->baseEventStartTimeZone;
}
// extract start date and time
$this->baseEventStartDate = $this->baseEvent->DTSTART->getDateTime($this->baseEventStartTimeZone);
$this->baseEventStartDateFloating = $this->baseEvent->DTSTART->isFloating();
// determine event end date and duration
// evaluate if end date exists
// extract end date and calculate duration
if (isset($this->baseEvent->DTEND)) {
$this->baseEventEndDate = $this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone);
$this->baseEventEndDateFloating = $this->baseEvent->DTEND->isFloating();
$this->baseEventDuration =
$this->baseEvent->DTEND->getDateTime($this->baseEventEndTimeZone)->getTimeStamp() -
$this->baseEventStartDate->getTimeStamp();
}
// evaluate if duration exists
// extract duration and calculate end date
elseif (isset($this->baseEvent->DURATION)) {
$this->baseEventDuration = $this->baseEvent->DURATION->getDateInterval();
$this->baseEventEndDate = ((clone $this->baseEventStartDate)->add($this->baseEventDuration));
}
// evaluate if start date is floating
// set duration to 24 hours and calculate the end date
// according to the rfc any event without a end date or duration is a complete day
elseif ($this->baseEventStartDateFloating == true) {
$this->baseEventDuration = 86400;
$this->baseEventEndDate = ((clone $this->baseEventStartDate)->add($this->baseEventDuration));
}
// otherwise, set duration to zero this should never happen
else {
$this->baseEventDuration = 0;
$this->baseEventEndDate = $this->baseEventStartDate;
}
// evaluate if RRULE exist and construct iterator
if (isset($this->baseEvent->RRULE)) {
$this->rruleIterator = new EventReaderRRule(
$this->baseEvent->RRULE->getParts(),
$this->baseEventStartDate
);
}
// evaluate if RDATE exist and construct iterator
if (isset($this->baseEvent->RDATE)) {
$this->rdateIterator = new EventReaderRDate(
$this->baseEvent->RDATE->getValue(),
$this->baseEventStartDate
);
}
// evaluate if EXRULE exist and construct iterator
if (isset($this->baseEvent->EXRULE)) {
$this->eruleIterator = new EventReaderRRule(
$this->baseEvent->EXRULE->getParts(),
$this->baseEventStartDate
);
}
// evaluate if EXDATE exist and construct iterator
if (isset($this->baseEvent->EXDATE)) {
$this->edateIterator = new EventReaderRDate(
$this->baseEvent->EXDATE->getValue(),
$this->baseEventStartDate
);
}
// construct collection of modified events with recurrence id as hash
foreach ($events as $vevent) {
$this->recurrenceModified[$vevent->{'RECURRENCE-ID'}->getDateTime($this->baseEventStartTimeZone)->getTimeStamp()] = $vevent;
}
$this->recurrenceCurrentDate = clone $this->baseEventStartDate;
}
/**
* retrieve date and time of event start
*
* @since 30.0.0
*
* @return DateTime
*/
public function startDateTime(): DateTime {
return DateTime::createFromInterface($this->baseEventStartDate);
}
/**
* retrieve time zone of event start
*
* @since 30.0.0
*
* @return DateTimeZone
*/
public function startTimeZone(): DateTimeZone {
return $this->baseEventStartTimeZone;
}
/**
* retrieve date and time of event end
*
* @since 30.0.0
*
* @return DateTime
*/
public function endDateTime(): DateTime {
return DateTime::createFromInterface($this->baseEventEndDate);
}
/**
* retrieve time zone of event end
*
* @since 30.0.0
*
* @return DateTimeZone
*/
public function endTimeZone(): DateTimeZone {
return $this->baseEventEndTimeZone;
}
/**
* is this an all day event
*
* @since 30.0.0
*
* @return bool
*/
public function entireDay(): bool {
return $this->baseEventStartDateFloating;
}
/**
* is this a recurring event
*
* @since 30.0.0
*
* @return bool
*/
public function recurs(): bool {
return ($this->rruleIterator !== null || $this->rdateIterator !== null);
}
/**
* event recurrence pattern
*
* @since 30.0.0
*
* @return string|null R - Relative or A - Absolute
*/
public function recurringPattern(): string | null {
if ($this->rruleIterator === null && $this->rdateIterator === null) {
return null;
}
if ($this->rruleIterator?->isRelative()) {
return 'R';
}
return 'A';
}
/**
* event recurrence precision
*
* @since 30.0.0
*
* @return string|null daily, weekly, monthly, yearly, fixed
*/
public function recurringPrecision(): string | null {
if ($this->rruleIterator !== null) {
return $this->rruleIterator->precision();
}
if ($this->rdateIterator !== null) {
return 'fixed';
}
return null;
}
/**
* event recurrence interval
*
* @since 30.0.0
*
* @return int|null
*/
public function recurringInterval(): int | null {
return $this->rruleIterator?->interval();
}
/**
* event recurrence conclusion
*
* returns true if RRULE with UNTIL or COUNT (calculated) is used
* returns true RDATE is used
* returns false if RRULE or RDATE are absent, or RRRULE is infinite
*
* @since 30.0.0
*
* @return bool
*/
public function recurringConcludes(): bool {
// retrieve rrule conclusions
if ($this->rruleIterator?->concludesOn() !== null ||
$this->rruleIterator?->concludesAfter() !== null) {
return true;
}
// retrieve rdate conclusions
if ($this->rdateIterator?->concludesAfter() !== null) {
return true;
}
return false;
}
/**
* event recurrence conclusion iterations
*
* returns the COUNT value if RRULE is used
* returns the collection count if RDATE is used
* returns combined count of RRULE COUNT and RDATE if both are used
* returns null if RRULE and RDATE are absent
*
* @since 30.0.0
*
* @return int|null
*/
public function recurringConcludesAfter(): int | null {
// construct count place holder
$count = 0;
// retrieve and add RRULE iterations count
$count += (int) $this->rruleIterator?->concludesAfter();
// retrieve and add RDATE iterations count
$count += (int) $this->rdateIterator?->concludesAfter();
// return count
return !empty($count) ? $count : null;
}
/**
* event recurrence conclusion date
*
* returns the last date of UNTIL or COUNT (calculated) if RRULE is used
* returns the last date in the collection if RDATE is used
* returns the highest date if both RRULE and RDATE are used
* returns null if RRULE and RDATE are absent or RRULE is infinite
*
* @since 30.0.0
*
* @return DateTime|null
*/
public function recurringConcludesOn(): DateTime | null {
if ($this->rruleIterator !== null) {
// retrieve rrule conclusion date
$rrule = $this->rruleIterator->concludes();
// evaluate if rrule conclusion is null
// if this is null that means the recurrence is infinate
if ($rrule === null) {
return null;
}
}
// retrieve rdate conclusion date
if ($this->rdateIterator !== null) {
$rdate = $this->rdateIterator->concludes();
}
// evaluate if both rrule and rdate have date
if (isset($rdate) && isset($rrule)) {
// return the highest date
return (($rdate > $rrule) ? $rdate : $rrule);
} elseif (isset($rrule)) {
return $rrule;
} elseif (isset($rdate)) {
return $rdate;
}
return null;
}
/**
* event recurrence days of the week
*
* returns collection of RRULE BYDAY day(s) ['MO','WE','FR']
* returns blank collection if RRULE is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringDaysOfWeek(): array {
// evaluate if RRULE exists and return day(s) of the week
return $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
}
/**
* event recurrence days of the week (named)
*
* returns collection of RRULE BYDAY day(s) ['Monday','Wednesday','Friday']
* returns blank collection if RRULE is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringDaysOfWeekNamed(): array {
// evaluate if RRULE exists and extract day(s) of the week
$days = $this->rruleIterator !== null ? $this->rruleIterator->daysOfWeek() : [];
// convert numberic month to month name
foreach ($days as $key => $value) {
$days[$key] = $this->dayNamesMap[$value];
}
// return names collection
return $days;
}
/**
* event recurrence days of the month
*
* returns collection of RRULE BYMONTHDAY day(s) [7, 15, 31]
* returns blank collection if RRULE is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringDaysOfMonth(): array {
// evaluate if RRULE exists and return day(s) of the month
return $this->rruleIterator !== null ? $this->rruleIterator->daysOfMonth() : [];
}
/**
* event recurrence days of the year
*
* returns collection of RRULE BYYEARDAY day(s) [57, 205, 365]
* returns blank collection if RRULE is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringDaysOfYear(): array {
// evaluate if RRULE exists and return day(s) of the year
return $this->rruleIterator !== null ? $this->rruleIterator->daysOfYear() : [];
}
/**
* event recurrence weeks of the month
*
* returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
* returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringWeeksOfMonth(): array {
// evaluate if RRULE exists and RRULE is relative return relative position(s)
return $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
}
/**
* event recurrence weeks of the month (named)
*
* returns collection of RRULE SETPOS weeks(s) [1, 3, -1]
* returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringWeeksOfMonthNamed(): array {
// evaluate if RRULE exists and extract relative position(s)
$positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
// convert numberic relative position to relative label
foreach ($positions as $key => $value) {
$positions[$key] = $this->relativePositionNamesMap[$value];
}
// return positions collection
return $positions;
}
/**
* event recurrence weeks of the year
*
* returns collection of RRULE BYWEEKNO weeks(s) [12, 32, 52]
* returns blank collection if RRULE is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringWeeksOfYear(): array {
// evaluate if RRULE exists and return weeks(s) of the year
return $this->rruleIterator !== null ? $this->rruleIterator->weeksOfYear() : [];
}
/**
* event recurrence months of the year
*
* returns collection of RRULE BYMONTH month(s) [3, 7, 12]
* returns blank collection if RRULE is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringMonthsOfYear(): array {
// evaluate if RRULE exists and return month(s) of the year
return $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
}
/**
* event recurrence months of the year (named)
*
* returns collection of RRULE BYMONTH month(s) [3, 7, 12]
* returns blank collection if RRULE is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringMonthsOfYearNamed(): array {
// evaluate if RRULE exists and extract month(s) of the year
$months = $this->rruleIterator !== null ? $this->rruleIterator->monthsOfYear() : [];
// convert numberic month to month name
foreach ($months as $key => $value) {
$months[$key] = $this->monthNamesMap[$value];
}
// return months collection
return $months;
}
/**
* event recurrence relative positions
*
* returns collection of RRULE SETPOS value(s) [1, 5, -3]
* returns blank collection if RRULE is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringRelativePosition(): array {
// evaluate if RRULE exists and return relative position(s)
return $this->rruleIterator !== null ? $this->rruleIterator->relativePosition() : [];
}
/**
* event recurrence relative positions (named)
*
* returns collection of RRULE SETPOS [1, 3, -1]
* returns blank collection if RRULE is absent or SETPOS is absent, RDATE presents or absents has no affect
*
* @since 30.0.0
*
* @return array
*/
public function recurringRelativePositionNamed(): array {
// evaluate if RRULE exists and extract relative position(s)
$positions = $this->rruleIterator?->isRelative() ? $this->rruleIterator->relativePosition() : [];
// convert numberic relative position to relative label
foreach ($positions as $key => $value) {
$positions[$key] = $this->relativePositionNamesMap[$value];
}
// return positions collection
return $positions;
}
/**
* event recurrence date
*
* returns date of currently selected recurrence
*
* @since 30.0.0
*
* @return DateTime
*/
public function recurrenceDate(): DateTime | null {
if ($this->recurrenceCurrentDate !== null) {
return DateTime::createFromInterface($this->recurrenceCurrentDate);
} else {
return null;
}
}
/**
* event recurrence rewind
*
* sets the current recurrence to the first recurrence in the collection
*
* @since 30.0.0
*
* @return void
*/
public function recurrenceRewind(): void {
// rewind and increment rrule
if ($this->rruleIterator !== null) {
$this->rruleIterator->rewind();
}
// rewind and increment rdate
if ($this->rdateIterator !== null) {
$this->rdateIterator->rewind();
}
// rewind and increment exrule
if ($this->eruleIterator !== null) {
$this->eruleIterator->rewind();
}
// rewind and increment exdate
if ($this->edateIterator !== null) {
$this->edateIterator->rewind();
}
// set current date to event start date
$this->recurrenceCurrentDate = clone $this->baseEventStartDate;
}
/**
* event recurrence advance
*
* sets the current recurrence to the next recurrence in the collection
*
* @since 30.0.0
*
* @return void
*/
public function recurrenceAdvance(): void {
// place holders
$nextOccurrenceDate = null;
$nextExceptionDate = null;
$rruleDate = null;
$rdateDate = null;
$eruleDate = null;
$edateDate = null;
// evaludate if rrule is set and advance one interation past current date
if ($this->rruleIterator !== null) {
// forward rrule to the next future date
while ($this->rruleIterator->valid() && $this->rruleIterator->current() <= $this->recurrenceCurrentDate) {
$this->rruleIterator->next();
}
$rruleDate = $this->rruleIterator->current();
}
// evaludate if rdate is set and advance one interation past current date
if ($this->rdateIterator !== null) {
// forward rdate to the next future date
while ($this->rdateIterator->valid() && $this->rdateIterator->current() <= $this->recurrenceCurrentDate) {
$this->rdateIterator->next();
}
$rdateDate = $this->rdateIterator->current();
}
if ($rruleDate !== null && $rdateDate !== null) {
$nextOccurrenceDate = ($rruleDate <= $rdateDate) ? $rruleDate : $rdateDate;
} elseif ($rruleDate !== null) {
$nextOccurrenceDate = $rruleDate;
} elseif ($rdateDate !== null) {
$nextOccurrenceDate = $rdateDate;
}
// evaludate if exrule is set and advance one interation past current date
if ($this->eruleIterator !== null) {
// forward exrule to the next future date
while ($this->eruleIterator->valid() && $this->eruleIterator->current() <= $this->recurrenceCurrentDate) {
$this->eruleIterator->next();
}
$eruleDate = $this->eruleIterator->current();
}
// evaludate if exdate is set and advance one interation past current date
if ($this->edateIterator !== null) {
// forward exdate to the next future date
while ($this->edateIterator->valid() && $this->edateIterator->current() <= $this->recurrenceCurrentDate) {
$this->edateIterator->next();
}
$edateDate = $this->edateIterator->current();
}
// evaludate if exrule and exdate are set and set nextExDate to the first next date
if ($eruleDate !== null && $edateDate !== null) {
$nextExceptionDate = ($eruleDate <= $edateDate) ? $eruleDate : $edateDate;
} elseif ($eruleDate !== null) {
$nextExceptionDate = $eruleDate;
} elseif ($edateDate !== null) {
$nextExceptionDate = $edateDate;
}
// if the next date is part of exrule or exdate find another date
if ($nextOccurrenceDate !== null && $nextExceptionDate !== null && $nextOccurrenceDate == $nextExceptionDate) {
$this->recurrenceCurrentDate = $nextOccurrenceDate;
$this->recurrenceAdvance();
} else {
$this->recurrenceCurrentDate = $nextOccurrenceDate;
}
}
/**
* event recurrence advance
*
* sets the current recurrence to the next recurrence in the collection after the specific date
*
* @since 30.0.0
*
* @param DateTimeInterface $dt date and time to advance
*
* @return void
*/
public function recurrenceAdvanceTo(DateTimeInterface $dt): void {
while ($this->recurrenceCurrentDate !== null && $this->recurrenceCurrentDate < $dt) {
$this->recurrenceAdvance();
}
}
}

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
use DateTime;
class EventReaderRDate extends \Sabre\VObject\Recur\RDateIterator {
public function concludes(): DateTime | null {
return $this->concludesOn();
}
public function concludesAfter(): int | null {
return !empty($this->dates) ? count($this->dates) : null;
}
public function concludesOn(): DateTime | null {
if (count($this->dates) > 0) {
return new DateTime(
$this->dates[array_key_last($this->dates)],
$this->startDate->getTimezone()
);
}
return null;
}
}

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV;
use DateTime;
use DateTimeInterface;
class EventReaderRRule extends \Sabre\VObject\Recur\RRuleIterator {
public function precision(): string {
return $this->frequency;
}
public function interval(): int {
return $this->interval;
}
public function concludes(): DateTime | null {
// evaluate if until value is a date
if ($this->until instanceof DateTimeInterface) {
return DateTime::createFromInterface($this->until);
}
// evaluate if count value is higher than 0
if ($this->count > 0) {
// temporarily store current recurrence date and counter
$currentReccuranceDate = $this->currentDate;
$currentCounter = $this->counter;
// iterate over occurrences until last one (subtract 2 from count for start and end occurrence)
while ($this->counter <= ($this->count - 2)) {
$this->next();
}
// temporarly store last reccurance date
$lastReccuranceDate = $this->currentDate;
// restore current recurrence date and counter
$this->currentDate = $currentReccuranceDate;
$this->counter = $currentCounter;
// return last recurrence date
return DateTime::createFromInterface($lastReccuranceDate);
}
return null;
}
public function concludesAfter(): int | null {
return !empty($this->count) ? $this->count : null;
}
public function concludesOn(): DateTime | null {
return isset($this->until) ? DateTime::createFromInterface($this->until) : null;
}
public function daysOfWeek(): array {
return $this->byDay;
}
public function daysOfMonth(): array {
return $this->byMonthDay;
}
public function daysOfYear(): array {
return $this->byYearDay;
}
public function weeksOfYear(): array {
return $this->byWeekNo;
}
public function monthsOfYear(): array {
return $this->byMonth;
}
public function isRelative(): bool {
return isset($this->bySetPos);
}
public function relativePosition(): array {
return $this->bySetPos;
}
}

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OCA\DAV\CalDAV\Schedule;
use OC\URLGenerator;
use OCA\DAV\CalDAV\EventReader;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
@ -31,6 +33,7 @@ class IMipService {
private ISecureRandom $random;
private L10NFactory $l10nFactory;
private IL10N $l10n;
private ITimeFactory $timeFactory;
/** @var string[] */
private const STRING_DIFF = [
@ -44,7 +47,8 @@ class IMipService {
IConfig $config,
IDBConnection $db,
ISecureRandom $random,
L10NFactory $l10nFactory) {
L10NFactory $l10nFactory,
ITimeFactory $timeFactory) {
$this->urlGenerator = $urlGenerator;
$this->config = $config;
$this->db = $db;
@ -52,6 +56,7 @@ class IMipService {
$this->l10nFactory = $l10nFactory;
$default = $this->l10nFactory->findGenericLanguage();
$this->l10n = $this->l10nFactory->get('dav', $default);
$this->timeFactory = $timeFactory;
}
/**
@ -130,9 +135,13 @@ class IMipService {
* @return array
*/
public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array {
// construct event reader
$eventReaderCurrent = new EventReader($vEvent);
$eventReaderPrevious = !empty($oldVEvent) ? new EventReader($oldVEvent) : null;
$defaultVal = '';
$data = [];
$data['meeting_when'] = $this->generateWhenString($vEvent);
$data['meeting_when'] = $this->generateWhenString($eventReaderCurrent);
foreach(self::STRING_DIFF as $key => $property) {
$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
@ -145,7 +154,7 @@ class IMipService {
}
if(!empty($oldVEvent)) {
$oldMeetingWhen = $this->generateWhenString($oldVEvent);
$oldMeetingWhen = $this->generateWhenString($eventReaderPrevious);
$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
$data['meeting_location_html'] = $this->generateLinkifiedDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
@ -153,107 +162,334 @@ class IMipService {
$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
$data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
$data['meeting_when_html'] =
($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null)
? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when'])
: $data['meeting_when'];
$data['meeting_when_html'] = $oldMeetingWhen !== $data['meeting_when'] ? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when']) : $data['meeting_when'];
}
// generate occuring next string
if ($eventReaderCurrent->recurs()) {
$data['meeting_occurring'] = $this->generateOccurringString($eventReaderCurrent);
}
return $data;
}
/**
* @param IL10N $this->l10n
* @param VEvent $vevent
* @return false|int|string
* genarates a when string based on if a event has an recurrence or not
*
* @since 30.0.0
*
* @param EventReader $er
*
* @return string
*/
public function generateWhenString(VEvent $vevent) {
/** @var Property\ICalendar\DateTime $dtstart */
$dtstart = $vevent->DTSTART;
if (isset($vevent->DTEND)) {
/** @var Property\ICalendar\DateTime $dtend */
$dtend = $vevent->DTEND;
} elseif (isset($vevent->DURATION)) {
$isFloating = $dtstart->isFloating();
$dtend = clone $dtstart;
$endDateTime = $dtend->getDateTime();
$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
$dtend->setDateTime($endDateTime, $isFloating);
} elseif (!$dtstart->hasTime()) {
$isFloating = $dtstart->isFloating();
$dtend = clone $dtstart;
$endDateTime = $dtend->getDateTime();
$endDateTime = $endDateTime->modify('+1 day');
$dtend->setDateTime($endDateTime, $isFloating);
} else {
$dtend = clone $dtstart;
}
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
/** @var \DateTimeImmutable $dtstartDt */
$dtstartDt = $dtstart->getDateTime();
public function generateWhenString(EventReader $er): string {
return match ($er->recurs()) {
true => $this->generateWhenStringRecurring($er),
false => $this->generateWhenStringSingular($er)
};
}
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
/** @var \DateTimeImmutable $dtendDt */
$dtendDt = $dtend->getDateTime();
/**
* genarates a when string for a non recurring event
*
* @since 30.0.0
*
* @param EventReader $er
*
* @return string
*/
public function generateWhenStringSingular(EventReader $er): string {
// calculate time differnce from now to start of event
$occuring = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
// extract start date
$startDate = $this->l10n->l('date', $er->startDateTime(), ['width' => 'full']);
// time of the day
if (!$er->entireDay()) {
$startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
$startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
$endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
// generate localized when string
return match ([($occuring[0] > 1), !empty($endTime)]) {
[false, false] => $this->l10n->t('In a %1$s on %2$s for the entire day', [$occuring[1], $startDate]),
[false, true] => $this->l10n->t('In a %1$s on %2$s between %3$s - %4$s', [$occuring[1], $startDate, $startTime, $endTime]),
[true, false] => $this->l10n->t('In %1$s %2$s on %3$s for the entire day', [$occuring[0], $occuring[1], $startDate]),
[true, true] => $this->l10n->t('In %1$s %2$s on %3$s between %4$s - %5$s', [$occuring[0], $occuring[1], $startDate, $startTime, $endTime]),
default => $this->l10n->t('Could not generate when statement')
};
}
$diff = $dtstartDt->diff($dtendDt);
/**
* genarates a when string based on recurrance precision/frequency
*
* @since 30.0.0
*
* @param EventReader $er
*
* @return string
*/
public function generateWhenStringRecurring(EventReader $er): string {
return match ($er->recurringPrecision()) {
'daily' => $this->generateWhenStringRecurringDaily($er),
'weekly' => $this->generateWhenStringRecurringWeekly($er),
'monthly' => $this->generateWhenStringRecurringMonthly($er),
'yearly' => $this->generateWhenStringRecurringYearly($er),
'fixed' => $this->generateWhenStringRecurringFixed($er),
};
}
$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
/**
* genarates a when string for a daily precision/frequency
*
* @since 30.0.0
*
* @param EventReader $er
*
* @return string
*/
public function generateWhenStringRecurringDaily(EventReader $er): string {
// initialize
$interval = (int) $er->recurringInterval();
$startTime = '';
$endTime = '';
$conclusion = '';
// time of the day
if (!$er->entireDay()) {
$startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
$startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
$endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
// conclusion
if ($er->recurringConcludes()) {
$conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
}
// generate localized when string
return match ([($interval > 1), !empty($startTime), !empty($conclusion)]) {
[false, false, false] => $this->l10n->t('Every Day for the entire day'),
[false, false, true] => $this->l10n->t('Every Day for the entire day until %1$s', [$conclusion]),
[false, true, false] => $this->l10n->t('Every Day between %1$s - %2$s', [$startTime, $endTime]),
[false, true, true] => $this->l10n->t('Every Day between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]),
[true, false, false] => $this->l10n->t('Every %1$d Days for the entire day', [$interval]),
[true, false, true] => $this->l10n->t('Every %1$d Days for the entire day until %2$s', [$interval, $conclusion]),
[true, true, false] => $this->l10n->t('Every %1$d Days between %2$s - %3$s', [$interval, $startTime, $endTime]),
[true, true, true] => $this->l10n->t('Every %1$d Days between %2$s - %3$s until %4$s', [$interval, $startTime, $endTime, $conclusion]),
default => $this->l10n->t('Could not generate event recurrence statement')
};
if ($dtstart instanceof Property\ICalendar\Date) {
// One day event
if ($diff->days === 1) {
return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
}
}
// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
// the email should show 2020-01-01 to 2020-01-04.
$dtendDt->modify('-1 day');
/**
* genarates a when string for a weekly precision/frequency
*
* @since 30.0.0
*
* @param EventReader $er
*
* @return string
*/
public function generateWhenStringRecurringWeekly(EventReader $er): string {
// initialize
$interval = (int) $er->recurringInterval();
$startTime = '';
$endTime = '';
$conclusion = '';
// days of the week
$days = implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
// time of the day
if (!$er->entireDay()) {
$startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
$startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
$endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
// conclusion
if ($er->recurringConcludes()) {
$conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
}
// generate localized when string
return match ([($interval > 1), !empty($startTime), !empty($conclusion)]) {
[false, false, false] => $this->l10n->t('Every Week on %1$s for the entire day', [$days]),
[false, false, true] => $this->l10n->t('Every Week on %1$s for the entire day until %2$s', [$days, $conclusion]),
[false, true, false] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s', [$days, $startTime, $endTime]),
[false, true, true] => $this->l10n->t('Every Week on %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]),
[true, false, false] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day', [$interval, $days]),
[true, false, true] => $this->l10n->t('Every %1$d Weeks on %2$s for the entire day until %3$s', [$interval, $days, $conclusion]),
[true, true, false] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]),
[true, true, true] => $this->l10n->t('Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]),
default => $this->l10n->t('Could not generate event recurrence statement')
};
//event that spans over multiple days
$localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
$localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']);
}
return $localeStart . ' - ' . $localeEnd;
}
/**
* genarates a when string for a monthly precision/frequency
*
* @since 30.0.0
*
* @param EventReader $er
*
* @return string
*/
public function generateWhenStringRecurringMonthly(EventReader $er): string {
// initialize
$interval = (int) $er->recurringInterval();
$startTime = '';
$endTime = '';
$conclusion = '';
// days of month
if ($er->recurringPattern() === 'R') {
$days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' ' .
implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
} else {
$days = implode(', ', $er->recurringDaysOfMonth());
}
// time of the day
if (!$er->entireDay()) {
$startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
$startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
$endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
// conclusion
if ($er->recurringConcludes()) {
$conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
}
// generate localized when string
return match ([($interval > 1), !empty($startTime), !empty($conclusion)]) {
[false, false, false] => $this->l10n->t('Every Month on the %1$s for the entire day', [$days]),
[false, false, true] => $this->l10n->t('Every Month on the %1$s for the entire day until %2$s', [$days, $conclusion]),
[false, true, false] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s', [$days, $startTime, $endTime]),
[false, true, true] => $this->l10n->t('Every Month on the %1$s between %2$s - %3$s until %4$s', [$days, $startTime, $endTime, $conclusion]),
[true, false, false] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day', [$interval, $days]),
[true, false, true] => $this->l10n->t('Every %1$d Months on the %2$s for the entire day until %3$s', [$interval, $days, $conclusion]),
[true, true, false] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s', [$interval, $days, $startTime, $endTime]),
[true, true, true] => $this->l10n->t('Every %1$d Months on the %2$s between %3$s - %4$s until %5$s', [$interval, $days, $startTime, $endTime, $conclusion]),
default => $this->l10n->t('Could not generate event recurrence statement')
};
}
/** @var Property\ICalendar\DateTime $dtstart */
/** @var Property\ICalendar\DateTime $dtend */
$isFloating = $dtstart->isFloating();
$startTimezone = $endTimezone = null;
if (!$isFloating) {
$prop = $dtstart->offsetGet('TZID');
if ($prop instanceof Parameter) {
$startTimezone = $prop->getValue();
}
/**
* genarates a when string for a yearly precision/frequency
*
* @since 30.0.0
*
* @param EventReader $er
*
* @return string
*/
public function generateWhenStringRecurringYearly(EventReader $er): string {
// initialize
$interval = (int) $er->recurringInterval();
$startTime = '';
$endTime = '';
$conclusion = '';
// months of year
$months = implode(', ', array_map(function ($value) { return $this->localizeMonthName($value); }, $er->recurringMonthsOfYearNamed()));
// days of month
if ($er->recurringPattern() === 'R') {
$days = implode(', ', array_map(function ($value) { return $this->localizeRelativePositionName($value); }, $er->recurringRelativePositionNamed())) . ' ' .
implode(', ', array_map(function ($value) { return $this->localizeDayName($value); }, $er->recurringDaysOfWeekNamed()));
} else {
$days = $er->startDateTime()->format('jS');
}
// time of the day
if (!$er->entireDay()) {
$startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
$startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
$endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
// conclusion
if ($er->recurringConcludes()) {
$conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
}
// generate localized when string
return match ([($interval > 1), !empty($startTime), !empty($conclusion)]) {
[false, false, false] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day', [$months, $days]),
[false, false, true] => $this->l10n->t('Every Year in %1$s on the %2$s for the entire day until %3$s', [$months, $days, $conclusion]),
[false, true, false] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s', [$months, $days, $startTime, $endTime]),
[false, true, true] => $this->l10n->t('Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s', [$months, $days, $startTime, $endTime, $conclusion]),
[true, false, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day', [$interval, $months, $days]),
[true, false, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s for the entire day until %4$s', [$interval, $months, $days, $conclusion]),
[true, true, false] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s', [$interval, $months, $days, $startTime, $endTime]),
[true, true, true] => $this->l10n->t('Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s', [$interval, $months, $days, $startTime, $endTime, $conclusion]),
default => $this->l10n->t('Could not generate event recurrence statement')
};
}
$prop = $dtend->offsetGet('TZID');
if ($prop instanceof Parameter) {
$endTimezone = $prop->getValue();
/**
* genarates a when string for a fixed precision/frequency
*
* @since 30.0.0
*
* @param EventReader $er
*
* @return string
*/
public function generateWhenStringRecurringFixed(EventReader $er): string {
// initialize
$startTime = '';
$endTime = '';
$conclusion = '';
// time of the day
if (!$er->entireDay()) {
$startTime = $this->l10n->l('time', $er->startDateTime(), ['width' => 'short']);
$startTime .= $er->startTimeZone() != $er->endTimeZone() ? ' (' . $er->startTimeZone()->getName() . ')' : '';
$endTime = $this->l10n->l('time', $er->endDateTime(), ['width' => 'short']) . ' (' . $er->endTimeZone()->getName() . ')';
}
// conclusion
$conclusion = $this->l10n->l('date', $er->recurringConcludesOn(), ['width' => 'long']);
// generate localized when string
return match (!empty($startTime)) {
false => $this->l10n->t('On specific dates for the entire day until %1$s', [$conclusion]),
true => $this->l10n->t('On specific dates between %1$s - %2$s until %3$s', [$startTime, $endTime, $conclusion]),
};
}
/**
* genarates a occurring next string for a recurring event
*
* @since 30.0.0
*
* @param EventReader $er
*
* @return string
*/
public function generateOccurringString(EventReader $er): string {
// reset to initial occurance
$er->recurrenceRewind();
// forward to current date
$er->recurrenceAdvanceTo($this->timeFactory->getDateTime());
// calculate time differnce from now to start of next event occurance and minimize it
$occuranceIn = $this->minimizeInterval($this->timeFactory->getDateTime()->diff($er->recurrenceDate()));
// store next occurance value
$occurance = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
// forward one occurance
$er->recurrenceAdvance();
// evaluate if occurance is valid
if ($er->recurrenceDate() !== null) {
// store following occurance value
$occurance2 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
// forward one occurance
$er->recurrenceAdvance();
// evaluate if occurance is valid
if ($er->recurrenceDate()) {
// store following occurance value
$occurance3 = $this->l10n->l('date', $er->recurrenceDate(), ['width' => 'long']);
}
}
// generate occurance string
return match ([($occuranceIn[0] > 1), !empty($occurance2), !empty($occurance3)]) {
[false, false, false] => $this->l10n->t('In a %1$s on %2$s', [$occuranceIn[1], $occurance]),
[false, true, false] => $this->l10n->t('In a %1$s on %2$s then on %3$s', [$occuranceIn[1], $occurance, $occurance2]),
[false, true, true] => $this->l10n->t('In a %1$s on %2$s then on %3$s and %4$s', [$occuranceIn[1], $occurance, $occurance2, $occurance3]),
[true, false, false] => $this->l10n->t('In %1$s %2$s on %3$s', [$occuranceIn[0], $occuranceIn[1], $occurance]),
[true, true, false] => $this->l10n->t('In %1$s %2$s on %3$s then on %4$s', [$occuranceIn[0], $occuranceIn[1], $occurance, $occurance2]),
[true, true, true] => $this->l10n->t('In %1$s %2$s on %3$s then on %4$s and %5$s', [$occuranceIn[0], $occuranceIn[1], $occurance, $occurance2, $occurance3]),
default => $this->l10n->t('Could not generate next recurrence statement')
};
$localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
$this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
// always show full date with timezone if timezones are different
if ($startTimezone !== $endTimezone) {
$localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
return $localeStart . ' (' . $startTimezone . ') - ' .
$localeEnd . ' (' . $endTimezone . ')';
}
// show only end time if date is the same
if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) {
$localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']);
} else {
$localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
$this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
}
return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
}
/**
@ -261,12 +497,13 @@ class IMipService {
* @return array
*/
public function buildCancelledBodyData(VEvent $vEvent): array {
// construct event reader
$eventReaderCurrent = new EventReader($vEvent);
$defaultVal = '';
$strikethrough = "<span style='text-decoration: line-through'>%s</span>";
$newMeetingWhen = $this->generateWhenString($vEvent);
$newMeetingWhen = $this->generateWhenString($eventReaderCurrent);
$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');
;
$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
@ -522,7 +759,7 @@ class IMipService {
$data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'),
$this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT);
if ($data['meeting_when'] !== '') {
$template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Date and time:'),
$template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('When:'),
$this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
}
if ($data['meeting_location'] !== '') {
@ -533,6 +770,10 @@ class IMipService {
$template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'),
$this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT);
}
if (isset($data['meeting_occurring'])) {
$template->addBodyListItem($data['meeting_occurring_html'] ?? $data['meeting_occurring'], $this->l10n->t('Occurring:'),
$this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_occurring'], '', IMipPlugin::IMIP_INDENT);
}
$this->addAttendees($template, $vevent);
@ -643,10 +884,104 @@ class IMipService {
return false;
}
$type = $cuType->getValue() ?? 'INDIVIDUAL';
if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM'], true)) {
if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM', 'UNKNOWN'], true)) {
// Don't send emails to things
return true;
}
return false;
}
public function minimizeInterval(\DateInterval $dateInterval): array {
// evaluate if time interval is in the past
if ($dateInterval->invert == 1) {
return [1, 'the past'];
}
// evaluate interval parts and return smallest time period
if ($dateInterval->y > 0) {
$interval = $dateInterval->y;
$scale = ($dateInterval->y > 1) ? 'years' : 'year';
} elseif ($dateInterval->m > 0) {
$interval = $dateInterval->m;
$scale = ($dateInterval->m > 1) ? 'months' : 'month';
} elseif ($dateInterval->d >= 7) {
$interval = (int)($dateInterval->d / 7);
$scale = ((int)($dateInterval->d / 7) > 1) ? 'weeks' : 'week';
} elseif ($dateInterval->d > 0) {
$interval = $dateInterval->d;
$scale = ($dateInterval->d > 1) ? 'days' : 'day';
} elseif ($dateInterval->h > 0) {
$interval = $dateInterval->h;
$scale = ($dateInterval->h > 1) ? 'hours' : 'hour';
} else {
$interval = $dateInterval->i;
$scale = 'minutes';
}
return [$interval, $scale];
}
/**
* Localizes week day names to another language
*
* @param string $value
*
* @return string
*/
public function localizeDayName(string $value): string {
return match ($value) {
'Monday' => $this->l10n->t('Monday'),
'Tuesday' => $this->l10n->t('Tuesday'),
'Wednesday' => $this->l10n->t('Wednesday'),
'Thursday' => $this->l10n->t('Thursday'),
'Friday' => $this->l10n->t('Friday'),
'Saturday' => $this->l10n->t('Saturday'),
'Sunday' => $this->l10n->t('Sunday'),
};
}
/**
* Localizes month names to another language
*
* @param string $value
*
* @return string
*/
public function localizeMonthName(string $value): string {
return match ($value) {
'January' => $this->l10n->t('January'),
'February' => $this->l10n->t('February'),
'March' => $this->l10n->t('March'),
'April' => $this->l10n->t('April'),
'May' => $this->l10n->t('May'),
'June' => $this->l10n->t('June'),
'July' => $this->l10n->t('July'),
'August' => $this->l10n->t('August'),
'September' => $this->l10n->t('September'),
'October' => $this->l10n->t('October'),
'November' => $this->l10n->t('November'),
'December' => $this->l10n->t('December'),
};
}
/**
* Localizes relative position names to another language
*
* @param string $value
*
* @return string
*/
public function localizeRelativePositionName(string $value): string {
return match ($value) {
'First' => $this->l10n->t('First'),
'Second' => $this->l10n->t('Second'),
'Third' => $this->l10n->t('Third'),
'Fourth' => $this->l10n->t('Fourth'),
'Fifty' => $this->l10n->t('Fifty'),
'Last' => $this->l10n->t('Last'),
'Second Last' => $this->l10n->t('Second Last'),
'Third Last' => $this->l10n->t('Third Last'),
'Fourth Last' => $this->l10n->t('Fourth Last'),
'Fifty Last' => $this->l10n->t('Fifty Last'),
};
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save