2014-04-24 15:56:46 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Recurrence computation class for shared use
|
|
|
|
*
|
|
|
|
* Uitility class to compute reccurrence dates from the given rules
|
|
|
|
*
|
|
|
|
* @author Thomas Bruederli <bruederli@kolabsys.com>
|
|
|
|
*
|
|
|
|
* Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
|
|
|
|
*
|
|
|
|
* 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 $next;
|
|
|
|
protected $engine;
|
|
|
|
protected $recurrence;
|
|
|
|
protected $dateonly = false;
|
|
|
|
protected $hour = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default constructor
|
|
|
|
*
|
2022-11-29 15:54:43 +01:00
|
|
|
* @param calendar The calendar plugin instance
|
2014-04-24 15:56:46 +02:00
|
|
|
*/
|
|
|
|
function __construct($lib)
|
|
|
|
{
|
2022-12-01 15:03:56 +01:00
|
|
|
// use Horde classes to compute recurring instances
|
|
|
|
// TODO: replace with something that has less than 6'000 lines of code
|
|
|
|
require_once(__DIR__ . '/Horde_Date_Recurrence.php');
|
2014-04-24 15:56:46 +02:00
|
|
|
|
2022-12-01 15:03:56 +01:00
|
|
|
$this->lib = $lib;
|
2014-04-24 15:56:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize recurrence engine
|
|
|
|
*
|
2022-11-29 15:54:43 +01:00
|
|
|
* @param array The recurrence properties
|
|
|
|
* @param DateTime The recurrence start date
|
2014-04-24 15:56:46 +02:00
|
|
|
*/
|
2014-04-24 19:41:07 +02:00
|
|
|
public function init($recurrence, $start = null)
|
2014-04-24 15:56:46 +02:00
|
|
|
{
|
|
|
|
$this->recurrence = $recurrence;
|
|
|
|
|
|
|
|
$this->engine = new Horde_Date_Recurrence($start);
|
|
|
|
$this->engine->fromRRule20(libcalendaring::to_rrule($recurrence));
|
|
|
|
|
2014-04-24 19:41:07 +02:00
|
|
|
$this->set_start($start);
|
|
|
|
|
2021-02-01 08:30:34 +01:00
|
|
|
if (!empty($recurrence['EXDATE'])) {
|
|
|
|
foreach ((array) $recurrence['EXDATE'] as $exdate) {
|
2022-11-30 12:54:29 +01:00
|
|
|
if ($exdate instanceof DateTimeInterface) {
|
2014-04-24 15:56:46 +02:00
|
|
|
$this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-02-01 08:30:34 +01:00
|
|
|
if (!empty($recurrence['RDATE'])) {
|
|
|
|
foreach ((array) $recurrence['RDATE'] as $rdate) {
|
2022-11-30 12:54:29 +01:00
|
|
|
if ($rdate instanceof DateTimeInterface) {
|
2014-04-24 15:56:46 +02:00
|
|
|
$this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-04-24 19:41:07 +02:00
|
|
|
/**
|
|
|
|
* Setter for (new) recurrence start date
|
|
|
|
*
|
2022-11-29 15:54:43 +01:00
|
|
|
* @param DateTime The recurrence start date
|
2014-04-24 19:41:07 +02:00
|
|
|
*/
|
|
|
|
public function set_start($start)
|
|
|
|
{
|
|
|
|
$this->start = $start;
|
2022-12-01 15:03:56 +01:00
|
|
|
$this->dateonly = !empty($start->_dateonly);
|
2014-04-24 19:41:07 +02:00
|
|
|
$this->next = new Horde_Date($start, $this->lib->timezone->getName());
|
|
|
|
$this->hour = $this->next->hour;
|
|
|
|
$this->engine->setRecurStart($this->next);
|
|
|
|
}
|
|
|
|
|
2014-04-24 15:56:46 +02:00
|
|
|
/**
|
|
|
|
* Get date/time of the next occurence of this event
|
|
|
|
*
|
2022-12-01 15:03:56 +01:00
|
|
|
* @return DateTime|false object or False if recurrence ended
|
2014-04-24 15:56:46 +02:00
|
|
|
*/
|
2014-04-24 19:41:07 +02:00
|
|
|
public function next()
|
2014-04-24 15:56:46 +02:00
|
|
|
{
|
|
|
|
$time = false;
|
|
|
|
$after = clone $this->next;
|
|
|
|
$after->mday = $after->mday + 1;
|
2022-12-01 15:03:56 +01:00
|
|
|
|
2014-04-24 15:56:46 +02:00
|
|
|
if ($this->next && ($next = $this->engine->nextActiveRecurrence($after))) {
|
|
|
|
// avoid endless loops if recurrence computation fails
|
|
|
|
if (!$next->after($this->next)) {
|
|
|
|
return false;
|
|
|
|
}
|
2022-12-01 15:03:56 +01:00
|
|
|
|
2014-04-24 15:56:46 +02:00
|
|
|
// fix time for all-day events
|
|
|
|
if ($this->dateonly) {
|
|
|
|
$next->hour = $this->hour;
|
|
|
|
$next->min = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->next = $next;
|
2022-12-01 15:03:56 +01:00
|
|
|
|
|
|
|
$time = $this->toDateTime($next);
|
2014-04-24 15:56:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $time;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the end date of the occurence of this recurrence cycle
|
|
|
|
*
|
|
|
|
* @return DateTime|bool End datetime of the last occurence or False if recurrence exceeds limit
|
|
|
|
*/
|
|
|
|
public function end()
|
|
|
|
{
|
|
|
|
// recurrence end date is given
|
2022-11-29 15:54:43 +01:00
|
|
|
if ($this->recurrence['UNTIL'] instanceof DateTimeInterface) {
|
2014-04-24 15:56:46 +02:00
|
|
|
return $this->recurrence['UNTIL'];
|
|
|
|
}
|
|
|
|
|
|
|
|
// take the last RDATE entry if set
|
|
|
|
if (is_array($this->recurrence['RDATE']) && !empty($this->recurrence['RDATE'])) {
|
|
|
|
$last = end($this->recurrence['RDATE']);
|
2022-11-29 15:54:43 +01:00
|
|
|
if ($last instanceof DateTimeInterface) {
|
|
|
|
return $last;
|
2014-04-24 15:56:46 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// run through all items till we reach the end
|
|
|
|
if ($this->recurrence['COUNT']) {
|
|
|
|
$last = $this->start;
|
|
|
|
$this->next = new Horde_Date($this->start, $this->lib->timezone->getName());
|
2014-04-24 19:41:07 +02:00
|
|
|
while (($next = $this->next()) && $c < 1000) {
|
2014-04-24 15:56:46 +02:00
|
|
|
$last = $next;
|
|
|
|
$c++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $last;
|
|
|
|
}
|
|
|
|
|
2017-09-12 13:53:34 +02:00
|
|
|
/**
|
|
|
|
* Find date/time of the first occurrence (excluding start date)
|
|
|
|
*/
|
|
|
|
public function first_occurrence()
|
|
|
|
{
|
|
|
|
$start = clone $this->start;
|
|
|
|
$orig_start = clone $this->start;
|
|
|
|
$r = $this->recurrence;
|
2021-02-01 08:30:34 +01:00
|
|
|
$interval = !empty($r['INTERVAL']) ? intval($r['INTERVAL']) : 1;
|
|
|
|
$frequency = isset($this->recurrence['FREQ']) ? $this->recurrence['FREQ'] : null;
|
2017-09-12 13:53:34 +02:00
|
|
|
|
2021-02-01 08:30:34 +01:00
|
|
|
switch ($frequency) {
|
2017-09-12 13:53:34 +02:00
|
|
|
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;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return $start;
|
|
|
|
}
|
|
|
|
|
|
|
|
$r = $this->recurrence;
|
|
|
|
$r['INTERVAL'] = $interval;
|
2021-02-01 08:30:34 +01:00
|
|
|
if (!empty($r['COUNT'])) {
|
2017-09-12 13:53:34 +02:00
|
|
|
// Increase count so we do not stop the loop to early
|
|
|
|
$r['COUNT'] += 100;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create recurrence that starts in the past
|
|
|
|
$recurrence = new self($this->lib);
|
|
|
|
$recurrence->init($r, $start);
|
|
|
|
|
|
|
|
// find the first occurrence
|
|
|
|
$found = false;
|
|
|
|
while ($next = $recurrence->next()) {
|
|
|
|
$start = $next;
|
|
|
|
if ($next >= $orig_start) {
|
|
|
|
$found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$found) {
|
|
|
|
rcube::raise_error(array(
|
|
|
|
'file' => __FILE__,
|
|
|
|
'line' => __LINE__,
|
|
|
|
'message' => sprintf("Failed to find a first occurrence. Start: %s, Recurrence: %s",
|
|
|
|
$orig_start->format(DateTime::ISO8601), json_encode($r)),
|
|
|
|
), true);
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-12-01 15:03:56 +01:00
|
|
|
$start = $this->toDateTime($start);
|
|
|
|
|
|
|
|
return $start;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function toDateTime($date)
|
|
|
|
{
|
|
|
|
if ($date Instanceof Horde_Date) {
|
|
|
|
$date = $date->toDateTime();
|
2017-09-12 13:53:34 +02:00
|
|
|
}
|
|
|
|
|
2022-12-01 15:03:56 +01:00
|
|
|
if ($date instanceof DateTimeInterface) {
|
|
|
|
$date = libcalendaring_datetime::createFromFormat(
|
|
|
|
'Y-m-d\\TH:i:s',
|
|
|
|
$date->format('Y-m-d\\TH:i:s'),
|
|
|
|
$date->getTimezone()
|
|
|
|
);
|
|
|
|
}
|
2017-09-12 13:53:34 +02:00
|
|
|
|
2022-12-01 15:03:56 +01:00
|
|
|
$date->_dateonly = $this->dateonly;
|
|
|
|
|
|
|
|
return $date;
|
2017-09-12 13:53:34 +02:00
|
|
|
}
|
2014-04-24 15:56:46 +02:00
|
|
|
}
|