roundcubemail-plugins-kolab/plugins/libcalendaring/lib/libcalendaring_recurrence.php
Aleksander Machniak 2103cc317d PHPDoc fixes
2024-01-26 14:38:48 +01:00

295 lines
9.4 KiB
PHP

<?php
use Sabre\VObject\Recur\EventIterator;
/**
* Recurrence computation class for shared use.
*
* Uitility class to compute recurrence dates from the given rules.
*
* @author Aleksander Machniak <machniak@apheleia-it.ch
*
* Copyright (C) 2012-2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* 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/>.
*/
class libcalendaring_recurrence
{
protected $lib;
protected $start;
protected $engine;
protected $recurrence;
protected $dateonly = false;
protected $event;
protected $duration;
protected $isStart = true;
/**
* Default constructor
*
* @param libcalendaring $lib The libcalendaring plugin instance
* @param array $event The event object to operate on
*/
public function __construct($lib, $event = null)
{
$this->lib = $lib;
$this->event = $event;
if (!empty($event)) {
if (!empty($event['start']) && is_object($event['start'])
&& !empty($event['end']) && is_object($event['end'])
) {
$this->duration = $event['start']->diff($event['end']);
}
$this->init($event['recurrence'], $event['start']);
}
}
/**
* Initialize recurrence engine
*
* @param array $recurrence The recurrence properties
* @param DateTime $start The recurrence start date
*/
public function init($recurrence, $start)
{
$this->start = $start;
$this->dateonly = !empty($start->_dateonly) || !empty($this->event['allday']);
$this->recurrence = $recurrence;
$event = [
'uid' => '1',
'allday' => $this->dateonly,
'recurrence' => $recurrence,
'start' => $start,
// TODO: END/DURATION ???
// TODO: moved occurrences ???
];
$vcalendar = new libcalendaring_vcalendar($this->lib->timezone);
$ve = $vcalendar->toSabreComponent($event);
$this->engine = new EventIterator($ve, null, $this->lib->timezone);
}
/**
* Get date/time of the next occurence of this event, and push the iterator.
*
* @return DateTime|false object or False if recurrence ended
*/
public function next_start()
{
try {
$this->engine->next();
$current = $this->engine->getDtStart();
} catch (Exception $e) {
// do nothing
}
return !empty($current) ? $this->toDateTime($current) : false;
}
/**
* Get the next recurring instance of this event
*
* @return array|false Array with event properties or False if recurrence ended
*/
public function next_instance()
{
// Here's the workaround for an issue for an event with its start date excluded
// E.g. A daily event starting on 10th which is one of EXDATE dates
// should return 11th as next_instance() when called for the first time.
// Looks like Sabre is setting internal "current date" to 11th on such an object
// initialization, therefore calling next() would move it to 12th.
if ($this->isStart && ($next_start = $this->engine->getDtStart())
&& $next_start->format('Ymd') != $this->start->format('Ymd')
) {
$next_start = $this->toDateTime($next_start);
} else {
$next_start = $this->next_start();
}
$this->isStart = false;
if ($next_start) {
$next = $this->event;
$next['start'] = $next_start;
if ($this->duration) {
$next['end'] = clone $next_start;
$next['end']->add($this->duration);
}
$next['recurrence_date'] = clone $next_start;
$next['_instance'] = libcalendaring::recurrence_instance_identifier($next);
unset($next['_formatobj']);
return $next;
}
return false;
}
/**
* Get the date of the end of the last occurrence of this recurrence cycle
*
* @return DateTime|false End datetime of the last occurrence or False if there's no end date
*/
public function end()
{
// recurrence end date is given
if (isset($this->recurrence['UNTIL']) && $this->recurrence['UNTIL'] instanceof DateTimeInterface) {
return $this->recurrence['UNTIL'];
}
// Run through all items till we reach the end, or limit of iterations
// Note: Sabre has a limits of iteration in VObject\Settings, so it is not an infinite loop
try {
foreach ($this->engine as $end) {
// do nothing
}
} catch (Exception $e) {
// do nothing
}
/*
if (empty($end) && isset($this->event['start']) && $this->event['start'] instanceof DateTimeInterface) {
// determine a reasonable end date if none given
$end = clone $this->event['start'];
$end->add(new DateInterval('P100Y'));
}
*/
return isset($end) ? $this->toDateTime($end) : false;
}
/**
* Find date/time of the first occurrence (excluding start date)
*
* @return DateTime|null First occurrence
*/
public function first_occurrence()
{
$start = clone $this->start;
$interval = $this->recurrence['INTERVAL'] ?? 1;
$freq = $this->recurrence['FREQ'] ?? null;
switch ($freq) {
case 'WEEKLY':
if (empty($this->recurrence['BYDAY'])) {
return $start;
}
$start->sub(new DateInterval("P{$interval}W"));
break;
case 'MONTHLY':
if (empty($this->recurrence['BYDAY']) && empty($this->recurrence['BYMONTHDAY'])) {
return $start;
}
$start->sub(new DateInterval("P{$interval}M"));
break;
case 'YEARLY':
if (empty($this->recurrence['BYDAY']) && empty($this->recurrence['BYMONTH'])) {
return $start;
}
$start->sub(new DateInterval("P{$interval}Y"));
break;
case 'DAILY':
if (!empty($this->recurrence['BYMONTH'])) {
break;
}
// no break
default:
return $start;
}
$recurrence = $this->recurrence;
if (!empty($recurrence['COUNT'])) {
// Increase count so we do not stop the loop to early
$recurrence['COUNT'] += 100;
}
// Create recurrence that starts in the past
$self = new self($this->lib);
$self->init($recurrence, $start);
// TODO: This method does not work the same way as the kolab_date_recurrence based on
// kolabcalendaring. I.e. if an event start date does not match the recurrence rule
// it will be returned, kolab_date_recurrence will return the next occurrence in such a case
// which is the intended result of this function.
// See some commented out test cases in tests/RecurrenceTest.php
// find the first occurrence
$found = false;
while ($next = $self->next_start()) {
$start = $next;
if ($next >= $this->start) {
$found = true;
break;
}
}
if (!$found) {
rcube::raise_error(
[
'file' => __FILE__, 'line' => __LINE__,
'message' => sprintf(
"Failed to find a first occurrence. Start: %s, Recurrence: %s",
$this->start->format(DateTime::ISO8601),
json_encode($recurrence)
),
],
true
);
return null;
}
return $this->toDateTime($start);
}
/**
* Convert any DateTime into libcalendaring_datetime
*/
protected function toDateTime($date, $useStart = true)
{
if ($date instanceof DateTimeInterface) {
$date = libcalendaring_datetime::createFromFormat(
'Y-m-d\\TH:i:s',
$date->format('Y-m-d\\TH:i:s'),
// Sabre will loose timezone on all-day events, use the event start's timezone
$this->start->getTimezone()
);
}
$date->_dateonly = $this->dateonly;
if ($useStart && $this->dateonly) {
// Sabre sets time to 00:00:00 for all-day events,
// let's copy the time from the event's start
$date->setTime((int) $this->start->format('H'), (int) $this->start->format('i'), (int) $this->start->format('s'));
}
return $date;
}
}