diff --git a/plugins/libcalendaring/lib/libcalendaring_recurrence.php b/plugins/libcalendaring/lib/libcalendaring_recurrence.php
index 4aca2861..4de96812 100644
--- a/plugins/libcalendaring/lib/libcalendaring_recurrence.php
+++ b/plugins/libcalendaring/lib/libcalendaring_recurrence.php
@@ -1,278 +1,278 @@
*
* 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 .
*/
class libcalendaring_recurrence
{
protected $lib;
protected $start;
protected $engine;
protected $recurrence;
protected $dateonly = false;
protected $event;
protected $duration;
/**
* Default constructor
*
* @param libcalendaring $lib The libcalendaring plugin instance
* @param array $event The event object to operate on
*/
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']);
}
$event['start']->_dateonly = !empty($event['allday']);
$this->init($event['recurrence'], $event['start']);
}
}
/**
* Initialize recurrence engine
*
* @param array The recurrence properties
* @param DateTime The recurrence start date
*/
public function init($recurrence, $start)
{
$this->start = $start;
$this->dateonly = !empty($start->_dateonly);
$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 $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()
{
if ($next_start = $this->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, !empty($this->event['allday']));
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'];
}
- if (!$this->engine->isInfinite()) {
- // run through all items till we reach the end
- try {
- foreach ($this->engine as $end) {
- // do nothing
- }
- }
- catch (Exception $e) {
+ // 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
}
}
- else if (isset($this->event['end']) && $this->event['end'] instanceof DateTimeInterface) {
+ 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['end'];
+ $end = clone $this->event['start'];
$end->add(new DateInterval('P100Y'));
}
-
- return isset($end) ? $this->toDateTime($end, false) : false;
+*/
+ 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;
}
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;
}
}
diff --git a/plugins/libcalendaring/tests/RecurrenceTest.php b/plugins/libcalendaring/tests/RecurrenceTest.php
index cdbb2a8d..d17257a8 100644
--- a/plugins/libcalendaring/tests/RecurrenceTest.php
+++ b/plugins/libcalendaring/tests/RecurrenceTest.php
@@ -1,273 +1,337 @@
*
* Copyright (C) 2022, Apheleia IT AG
*
* 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 .
*/
class RecurrenceTest extends PHPUnit\Framework\TestCase
{
private $plugin;
function setUp(): void
{
$rcube = rcmail::get_instance();
$rcube->plugins->load_plugin('libcalendaring', true, true);
$this->plugin = $rcube->plugins->get_plugin('libcalendaring');
}
/**
- * Test for libcalendaring_recurrence::first_occurrence()
- *
- * @dataProvider data_first_occurrence
+ * Data for test_end()
*/
- function test_first_occurrence($recurrence_data, $start, $expected)
+ function data_end()
{
- $start = new DateTime($start);
- if (!empty($recurrence_data['UNTIL'])) {
- $recurrence_data['UNTIL'] = new DateTime($recurrence_data['UNTIL']);
- }
-
- $recurrence = $this->plugin->get_recurrence();
-
- $recurrence->init($recurrence_data, $start);
- $first = $recurrence->first_occurrence();
+ return [
+ // non-recurring
+ [
+ [
+ 'recurrence' => [],
+ 'start' => new DateTime('2017-08-31 11:00:00')
+ ],
+ '2017-08-31 11:00:00', // expected result
+ ],
+ // daily
+ [
+ [
+ 'recurrence' => ['FREQ' => 'DAILY', 'INTERVAL' => '1', 'COUNT' => 2],
+ 'start' => new DateTime('2017-08-31 11:00:00')
+ ],
+ '2017-09-01 11:00:00',
+ ],
+ // weekly
+ [
+ [
+ 'recurrence' => ['FREQ' => 'WEEKLY', 'COUNT' => 3],
+ 'start' => new DateTime('2017-08-31 11:00:00'), // Thursday
+ ],
+ '2017-09-14 11:00:00',
+ ],
+ // UNTIL
+ [
+ [
+ 'recurrence' => ['FREQ' => 'WEEKLY', 'COUNT' => 3, 'UNTIL' => new DateTime('2017-09-07 11:00:00')],
+ 'start' => new DateTime('2017-08-31 11:00:00'), // Thursday
+ ],
+ '2017-09-07 11:00:00',
+ ],
+ // Infinite recurrence, no count, no until
+ [
+ [
+ 'recurrence' => ['FREQ' => 'WEEKLY', 'INTERVAL' => '1'],
+ 'start' => new DateTime('2017-08-31 11:00:00'), // Thursday
+ ],
+ '2084-09-21 11:00:00',
+ ],
- $this->assertEquals($expected, $first ? $first->format('Y-m-d H:i:s') : '');
+ // TODO: Test an event with EXDATE/RDATEs
+ ];
}
-
/**
* Data for test_first_occurrence()
*/
function data_first_occurrence()
{
// TODO: BYYEARDAY, BYWEEKNO, BYSETPOS, WKST
return array(
// non-recurring
array(
array(), // recurrence data
'2017-08-31 11:00:00', // start date
'2017-08-31 11:00:00', // expected result
),
// daily
array(
array('FREQ' => 'DAILY', 'INTERVAL' => '1'), // recurrence data
'2017-08-31 11:00:00', // start date
'2017-08-31 11:00:00', // expected result
),
// TODO: this one is not supported by the Calendar UI
/*
array(
array('FREQ' => 'DAILY', 'INTERVAL' => '1', 'BYMONTH' => 1),
'2017-08-31 11:00:00',
'2018-01-01 11:00:00',
),
*/
// weekly
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1'),
'2017-08-31 11:00:00', // Thursday
'2017-08-31 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE'),
'2017-08-31 11:00:00', // Thursday
'2017-09-06 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'TH'),
'2017-08-31 11:00:00', // Thursday
'2017-08-31 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'FR'),
'2017-08-31 11:00:00', // Thursday
'2017-09-01 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '2'),
'2017-08-31 11:00:00', // Thursday
'2017-08-31 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '3', 'BYDAY' => 'WE'),
'2017-08-31 11:00:00', // Thursday
'2017-09-20 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE', 'COUNT' => 1),
'2017-08-31 11:00:00', // Thursday
'2017-09-06 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE', 'UNTIL' => '2017-09-01'),
'2017-08-31 11:00:00', // Thursday
'',
),
// monthly
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1'),
'2017-09-08 11:00:00',
'2017-09-08 11:00:00',
),
/*
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYMONTHDAY' => '8,9'),
'2017-08-31 11:00:00',
'2017-09-08 11:00:00',
),
*/
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYMONTHDAY' => '8,9'),
'2017-09-08 11:00:00',
'2017-09-08 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYDAY' => '1WE'),
'2017-08-16 11:00:00',
'2017-09-06 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYDAY' => '-1WE'),
'2017-08-16 11:00:00',
'2017-08-30 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '2'),
'2017-09-08 11:00:00',
'2017-09-08 11:00:00',
),
/*
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '2', 'BYMONTHDAY' => '8'),
'2017-08-31 11:00:00',
'2017-09-08 11:00:00', // ??????
),
*/
// yearly
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1'),
'2017-08-16 12:00:00',
'2017-08-16 12:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '8'),
'2017-08-16 12:00:00',
'2017-08-16 12:00:00',
),
/*
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYDAY' => '-1MO'),
'2017-08-16 11:00:00',
'2017-12-25 11:00:00',
),
*/
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '8', 'BYDAY' => '-1MO'),
'2017-08-16 11:00:00',
'2017-08-28 11:00:00',
),
/*
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '1', 'BYDAY' => '1MO'),
'2017-08-16 11:00:00',
'2018-01-01 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '1,9', 'BYDAY' => '1MO'),
'2017-08-16 11:00:00',
'2017-09-04 11:00:00',
),
*/
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '2'),
'2017-08-16 11:00:00',
'2017-08-16 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '2', 'BYMONTH' => '8'),
'2017-08-16 11:00:00',
'2017-08-16 11:00:00',
),
/*
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '2', 'BYDAY' => '-1MO'),
'2017-08-16 11:00:00',
'2017-12-25 11:00:00',
),
*/
// on dates (FIXME: do we really expect the first occurrence to be on the start date?)
array(
array('RDATE' => array(new DateTime('2017-08-10 11:00:00 Europe/Warsaw'))),
'2017-08-01 11:00:00',
'2017-08-01 11:00:00',
),
);
}
+ /**
+ * Test for libcalendaring_recurrence::end()
+ *
+ * @dataProvider data_end
+ */
+ function test_end($event, $expected)
+ {
+ $recurrence = new libcalendaring_recurrence($this->plugin, $event);
+
+ $end = $recurrence->end();
+
+ $this->assertSame($expected, $end ? $end->format('Y-m-d H:i:s') : $end);
+ }
+
+ /**
+ * Test for libcalendaring_recurrence::first_occurrence()
+ *
+ * @dataProvider data_first_occurrence
+ */
+ function test_first_occurrence($recurrence_data, $start, $expected)
+ {
+ $start = new DateTime($start);
+ if (!empty($recurrence_data['UNTIL'])) {
+ $recurrence_data['UNTIL'] = new DateTime($recurrence_data['UNTIL']);
+ }
+
+ $recurrence = $this->plugin->get_recurrence();
+
+ $recurrence->init($recurrence_data, $start);
+ $first = $recurrence->first_occurrence();
+
+ $this->assertEquals($expected, $first ? $first->format('Y-m-d H:i:s') : '');
+ }
+
/**
* Test for libcalendaring_recurrence::first_occurrence() for all-day events
*
* @dataProvider data_first_occurrence
*/
function test_first_occurrence_allday($recurrence_data, $start, $expected)
{
$start = new libcalendaring_datetime($start);
$start->_dateonly = true;
if (!empty($recurrence_data['UNTIL'])) {
$recurrence_data['UNTIL'] = new DateTime($recurrence_data['UNTIL']);
}
$recurrence = $this->plugin->get_recurrence();
$recurrence->init($recurrence_data, $start);
$first = $recurrence->first_occurrence();
$this->assertEquals($expected, $first ? $first->format('Y-m-d H:i:s') : '');
if ($expected) {
$this->assertTrue($first->_dateonly);
}
}
/**
* Test for libcalendaring_recurrence::next_instance()
*/
function test_next_instance()
{
date_default_timezone_set('America/New_York');
$start = new libcalendaring_datetime('2017-08-31 11:00:00', new DateTimeZone('Europe/Berlin'));
$event = [
'start' => $start,
'recurrence' => ['FREQ' => 'WEEKLY', 'INTERVAL' => '1'],
'allday' => true,
];
$recurrence = new libcalendaring_recurrence($this->plugin, $event);
$next = $recurrence->next_instance();
$this->assertEquals($start->format('2017-09-07 H:i:s'), $next['start']->format('Y-m-d H:i:s'), 'Same time');
$this->assertEquals($start->getTimezone()->getName(), $next['start']->getTimezone()->getName(), 'Same timezone');
$this->assertTrue($next['start']->_dateonly, '_dateonly flag');
}
}
diff --git a/plugins/libkolab/lib/kolab_date_recurrence.php b/plugins/libkolab/lib/kolab_date_recurrence.php
index f2110b03..2aaf5888 100644
--- a/plugins/libkolab/lib/kolab_date_recurrence.php
+++ b/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -1,241 +1,242 @@
*
* Copyright (C) 2012-2016, Kolab Systems AG
*
* 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 .
*/
class kolab_date_recurrence
{
private /* EventCal */ $engine;
private /* kolab_format_xcal */ $object;
private /* DateTime */ $start;
private /* DateTime */ $next;
private /* cDateTime */ $cnext;
private /* DateInterval */ $duration;
private /* bool */ $allday;
/**
* Default constructor
*
* @param kolab_format_xcal The Kolab object to operate on
*/
function __construct($object)
{
$data = $object->to_array();
$this->object = $object;
$this->engine = $object->to_libcal();
$this->start = $this->next = $data['start'];
$this->allday = !empty($data['allday']);
$this->cnext = kolab_format::get_datetime($this->next);
if (is_object($data['start']) && is_object($data['end'])) {
$this->duration = $data['start']->diff($data['end']);
}
else {
// Prevent from errors when end date is not set (#5307) RFC5545 3.6.1
$seconds = !empty($data['end']) ? ($data['end'] - $data['start']) : 0;
$this->duration = new DateInterval('PT' . $seconds . 'S');
}
}
/**
* Get date/time of the next occurence of this event
*
* @param bool Return a Unix timestamp instead of a DateTime object
*
* @return DateTime|int|false Object/unix timestamp or False if recurrence ended
*/
public function next_start($timestamp = false)
{
$time = false;
if ($this->engine && $this->next) {
if (($cnext = new cDateTime($this->engine->getNextOccurence($this->cnext))) && $cnext->isValid()) {
$next = kolab_format::php_datetime($cnext, $this->start->getTimezone());
$time = $timestamp ? $next->format('U') : $next;
if ($this->allday) {
// it looks that for allday events the occurrence time
// is reset to 00:00:00, this is causing various issues
$next->setTime($this->start->format('G'), $this->start->format('i'), $this->start->format('s'));
$next->_dateonly = true;
}
$this->cnext = $cnext;
$this->next = $next;
}
}
return $time;
}
/**
* Get the next recurring instance of this event
*
* @return mixed Array with event properties or False if recurrence ended
*/
public function next_instance()
{
if ($next_start = $this->next_start()) {
$next_end = clone $next_start;
$next_end->add($this->duration);
$next = $this->object->to_array();
$recurrence_id_format = libkolab::recurrence_id_format($next);
$next['start'] = $next_start;
$next['end'] = $next_end;
$next['recurrence_date'] = clone $next_start;
$next['_instance'] = $next_start->format($recurrence_id_format);
unset($next['_formatobj']);
return $next;
}
return false;
}
/**
* Get the end date of the last occurence of this recurrence cycle
*
* @return DateTime|bool End datetime of the last event or False if recurrence exceeds limit
*/
public function end()
{
$event = $this->object->to_array();
// recurrence end date is given
- if ($event['recurrence']['UNTIL'] instanceof DateTimeInterface) {
+ if (isset($event['recurrence']['UNTIL']) && $event['recurrence']['UNTIL'] instanceof DateTimeInterface) {
return $event['recurrence']['UNTIL'];
}
// let libkolab do the work
if ($this->engine && ($cend = $this->engine->getLastOccurrence())
&& ($end_dt = kolab_format::php_datetime(new cDateTime($cend)))
) {
return $end_dt;
}
- // determine a reasonable end date if none given
- if (!$event['recurrence']['COUNT'] && $event['end'] instanceof DateTimeInterface) {
- $end_dt = clone $event['end'];
- $end_dt->add(new DateInterval('P100Y'));
-
- return $end_dt;
+ // determine a reasonable end date for an infinite recurrence
+ if (empty($event['recurrence']['COUNT'])) {
+ if (!empty($event['start']) && $event['start'] instanceof DateTimeInterface) {
+ $start_dt = clone $event['start'];
+ $start_dt->add(new DateInterval('P100Y'));
+ return $start_dt;
+ }
}
return false;
}
/**
* Find date/time of the first occurrence
*
* @return DateTime|null First occurrence
*/
public function first_occurrence()
{
$event = $this->object->to_array();
$start = clone $this->start;
$orig_start = clone $this->start;
$interval = intval($event['recurrence']['INTERVAL'] ?: 1);
switch ($event['recurrence']['FREQ']) {
case 'WEEKLY':
if (empty($event['recurrence']['BYDAY'])) {
return $orig_start;
}
$start->sub(new DateInterval("P{$interval}W"));
break;
case 'MONTHLY':
if (empty($event['recurrence']['BYDAY']) && empty($event['recurrence']['BYMONTHDAY'])) {
return $orig_start;
}
$start->sub(new DateInterval("P{$interval}M"));
break;
case 'YEARLY':
if (empty($event['recurrence']['BYDAY']) && empty($event['recurrence']['BYMONTH'])) {
return $orig_start;
}
$start->sub(new DateInterval("P{$interval}Y"));
break;
case 'DAILY':
if (!empty($event['recurrence']['BYMONTH'])) {
break;
}
default:
return $orig_start;
}
$event['start'] = $start;
$event['recurrence']['INTERVAL'] = $interval;
if ($event['recurrence']['COUNT']) {
// Increase count so we do not stop the loop to early
$event['recurrence']['COUNT'] += 100;
}
// Create recurrence that starts in the past
$object_type = $this->object instanceof kolab_format_task ? 'task' : 'event';
$object = kolab_format::factory($object_type, 3.0);
$object->set($event);
$recurrence = new self($object);
$orig_date = $orig_start->format('Y-m-d');
$found = false;
// find the first occurrence
while ($next = $recurrence->next_start()) {
$start = $next;
if ($next->format('Y-m-d') >= $orig_date) {
$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($event['recurrence'])),
), true);
return null;
}
if ($this->allday) {
$start->_dateonly = true;
}
return $start;
}
}
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index 6c5e7a7a..5b0ecbbe 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -1,388 +1,389 @@
*
* Copyright (C) 2012-2015, Kolab Systems AG
*
* 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 .
*/
class libkolab extends rcube_plugin
{
static $http_requests = array();
static $bonnie_api = false;
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
// load local config
$this->load_config();
+ $this->require_plugin('libcalendaring');
// extend include path to load bundled lib classes
$include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
set_include_path($include_path);
$this->add_hook('storage_init', array($this, 'storage_init'));
$this->add_hook('storage_connect', array($this, 'storage_connect'));
$this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders'));
// For Chwala
$this->add_hook('folder_mod', array('kolab_storage', 'folder_mod'));
$rcmail = rcube::get_instance();
try {
kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
rcube::raise_error($e, true);
kolab_format::$timezone = new DateTimeZone('GMT');
}
$this->add_texts('localization/', false);
if (!empty($rcmail->output->type) && $rcmail->output->type == 'html') {
$rcmail->output->add_handler('libkolab.folder_search_form', array($this, 'folder_search_form'));
$this->include_stylesheet($this->local_skin_path() . '/libkolab.css');
}
// embed scripts and templates for email message audit trail
if ($rcmail->task == 'mail' && self::get_bonnie_api()) {
if ($rcmail->output->type == 'html') {
$this->add_hook('render_page', array($this, 'bonnie_render_page'));
$this->include_script('libkolab.js');
// add 'Show history' item to message menu
$this->api->add_content(html::tag('li', array('role' => 'menuitem'),
$this->api->output->button(array(
'command' => 'kolab-mail-history',
'label' => 'libkolab.showhistory',
'type' => 'link',
'classact' => 'icon history active',
'class' => 'icon history disabled',
'innerclass' => 'icon history',
))),
'messagemenu');
}
$this->register_action('plugin.message-changelog', array($this, 'message_changelog'));
}
}
/**
* Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
*/
function storage_init($p)
{
$kolab_headers = 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION MESSAGE-ID';
if (!empty($p['fetch_headers'])) {
$p['fetch_headers'] .= ' ' . $kolab_headers;
}
else {
$p['fetch_headers'] = $kolab_headers;
}
return $p;
}
/**
* Hook into IMAP connection to replace client identity
*/
function storage_connect($p)
{
$client_name = 'Roundcube/Kolab';
if (empty($p['ident'])) {
$p['ident'] = array(
'name' => $client_name,
'version' => RCUBE_VERSION,
/*
'php' => PHP_VERSION,
'os' => PHP_OS,
'command' => $_SERVER['REQUEST_URI'],
*/
);
}
else {
$p['ident']['name'] = $client_name;
}
return $p;
}
/**
* Getter for a singleton instance of the Bonnie API
*
* @return mixed kolab_bonnie_api instance if configured, false otherwise
*/
public static function get_bonnie_api()
{
// get configuration for the Bonnie API
if (!self::$bonnie_api && ($bonnie_config = rcube::get_instance()->config->get('kolab_bonnie_api', false))) {
self::$bonnie_api = new kolab_bonnie_api($bonnie_config);
}
return self::$bonnie_api;
}
/**
* Hook to append the message history dialog template to the mail view
*/
function bonnie_render_page($p)
{
if (($p['template'] === 'mail' || $p['template'] === 'message') && !$p['kolab-audittrail']) {
// append a template for the audit trail dialog
$this->api->output->add_footer(
html::div(array('id' => 'mailmessagehistory', 'class' => 'uidialog', 'aria-hidden' => 'true', 'style' => 'display:none'),
self::object_changelog_table(array('class' => 'records-table changelog-table'))
)
);
$this->api->output->set_env('kolab_audit_trail', true);
$p['kolab-audittrail'] = true;
}
return $p;
}
/**
* Handler for message audit trail changelog requests
*/
public function message_changelog()
{
if (!self::$bonnie_api) {
return false;
}
$rcmail = rcube::get_instance();
$msguid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST, true);
$mailbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
$result = $msguid && $mailbox ? self::$bonnie_api->changelog('mail', null, $mailbox, $msguid) : null;
if (is_array($result)) {
if (is_array($result['changes'])) {
$dtformat = $rcmail->config->get('date_format') . ' ' . $rcmail->config->get('time_format');
array_walk($result['changes'], function(&$change) use ($dtformat, $rcmail) {
if ($change['date']) {
$dt = rcube_utils::anytodatetime($change['date']);
if ($dt instanceof DateTimeInterface) {
$change['date'] = $rcmail->format_date($dt, $dtformat);
}
}
});
}
$this->api->output->command('plugin.message_render_changelog', $result['changes']);
}
else {
$this->api->output->command('plugin.message_render_changelog', false);
}
$this->api->output->send();
}
/**
* Wrapper function to load and initalize the HTTP_Request2 Object
*
* @param string|Net_Url2 Request URL
* @param string Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
* @param array Configuration for this Request instance, that will be merged
* with default configuration
*
* @return HTTP_Request2 Request object
*/
public static function http_request($url = '', $method = 'GET', $config = array())
{
$rcube = rcube::get_instance();
$http_config = (array) $rcube->config->get('kolab_http_request');
// deprecated configuration options
if (empty($http_config)) {
foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
$value = $rcube->config->get('kolab_' . $option, true);
if (is_bool($value)) {
$http_config[$option] = $value;
}
}
}
if (!empty($config)) {
$http_config = array_merge($http_config, $config);
}
// force CURL adapter, this allows to handle correctly
// compressed responses with SplObserver registered (kolab_files) (#4507)
$http_config['adapter'] = 'HTTP_Request2_Adapter_Curl';
$key = md5(serialize($http_config));
if (!($request = self::$http_requests[$key])) {
// load HTTP_Request2 (support both composer-installed and system-installed package)
if (!class_exists('HTTP_Request2')) {
require_once 'HTTP/Request2.php';
}
try {
$request = new HTTP_Request2();
$request->setConfig($http_config);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
// proxy User-Agent string
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
self::$http_requests[$key] = $request;
}
// cleanup
try {
$request->setBody('');
$request->setUrl($url);
$request->setMethod($method);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
return $request;
}
/**
* Table oultine for object changelog display
*/
public static function object_changelog_table($attrib = array())
{
$rcube = rcube::get_instance();
$attrib += array('domain' => 'libkolab');
$table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0));
$table->add_header('diff', '');
$table->add_header('revision', $rcube->gettext('revision', $attrib['domain']));
$table->add_header('date', $rcube->gettext('date', $attrib['domain']));
$table->add_header('user', $rcube->gettext('user', $attrib['domain']));
$table->add_header('operation', $rcube->gettext('operation', $attrib['domain']));
$table->add_header('actions', ' ');
$rcube->output->add_label(
'libkolab.showrevision',
'libkolab.actionreceive',
'libkolab.actionappend',
'libkolab.actionmove',
'libkolab.actiondelete',
'libkolab.actionread',
'libkolab.actionflagset',
'libkolab.actionflagclear',
'libkolab.objectchangelog',
'libkolab.objectchangelognotavailable',
'close'
);
return $table->show($attrib);
}
/**
* Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
*/
public static function html_diff($from, $to, $is_html = null)
{
// auto-detect text/html format
if ($is_html === null) {
$from_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $from, $m) && strpos($from, ''.$m[1].'>') > 0);
$to_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $to, $m) && strpos($to, ''.$m[1].'>') > 0);
$is_html = $from_html || $to_html;
// ensure both parts are of the same format
if ($is_html && !$from_html) {
$converter = new rcube_text2html($from, false, array('wrap' => true));
$from = $converter->get_html();
}
if ($is_html && !$to_html) {
$converter = new rcube_text2html($to, false, array('wrap' => true));
$to = $converter->get_html();
}
}
// compute diff from HTML
if ($is_html) {
include_once __dir__ . '/vendor/Caxy/HtmlDiff/Match.php';
include_once __dir__ . '/vendor/Caxy/HtmlDiff/Operation.php';
include_once __dir__ . '/vendor/Caxy/HtmlDiff/HtmlDiff.php';
// replace data: urls with a transparent image to avoid memory problems
$from = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $from);
$to = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $to);
$diff = new Caxy\HtmlDiff\HtmlDiff($from, $to);
$diffhtml = $diff->build();
// remove empty inserts (from tables)
return preg_replace('!\s*!Uims', '', $diffhtml);
}
else {
include_once __dir__ . '/vendor/finediff.php';
$diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
return $diff->renderDiffToHTML();
}
}
/**
* Return a date() format string to render identifiers for recurrence instances
*
* @param array Hash array with event properties
* @return string Format string
*/
public static function recurrence_id_format($event)
{
return $event['allday'] ? 'Ymd' : 'Ymd\THis';
}
/**
* Returns HTML code for folder search widget
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
public function folder_search_form($attrib)
{
$rcmail = rcube::get_instance();
$attrib += array(
'gui-object' => false,
'wrapper' => true,
'form-name' => 'foldersearchform',
'command' => 'non-extsing-command',
'reset-command' => 'non-existing-command',
);
if ($attrib['label-domain'] && !strpos($attrib['buttontitle'], '.')) {
$attrib['buttontitle'] = $attrib['label-domain'] . '.' . $attrib['buttontitle'];
}
if ($attrib['buttontitle']) {
$attrib['placeholder'] = $rcmail->gettext($attrib['buttontitle']);
}
return $rcmail->output->search_form($attrib);
}
}
diff --git a/plugins/libkolab/tests/KolabDateRecurrenceTest.php b/plugins/libkolab/tests/KolabDateRecurrenceTest.php
index 3c51aa19..c2ba0208 100644
--- a/plugins/libkolab/tests/KolabDateRecurrenceTest.php
+++ b/plugins/libkolab/tests/KolabDateRecurrenceTest.php
@@ -1,270 +1,342 @@
*
* Copyright (C) 2017, Kolab Systems AG
*
* 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 .
*/
class KolabDateRecurrenceTest extends PHPUnit\Framework\TestCase
{
function setUp(): void
{
$rcube = rcmail::get_instance();
$rcube->plugins->load_plugin('libkolab', true, true);
+ $rcube->plugins->load_plugin('libcalendaring', true, true);
}
/**
- * kolab_date_recurrence::first_occurrence()
- *
- * @dataProvider data_first_occurrence
+ * Data for test_end()
*/
- function test_first_occurrence($recurrence_data, $start, $expected)
+ function data_end()
{
- if (!kolab_format::supports(3)) {
- $this->markTestSkipped('No Kolab support');
- }
-
- $start = new DateTime($start);
- if (!empty($recurrence_data['UNTIL'])) {
- $recurrence_data['UNTIL'] = new DateTime($recurrence_data['UNTIL']);
- }
-
- $event = array('start' => $start, 'recurrence' => $recurrence_data);
- $object = kolab_format::factory('event', 3.0);
- $object->set($event);
-
- $recurrence = new kolab_date_recurrence($object);
- $first = $recurrence->first_occurrence();
+ return [
+ // non-recurring
+ [
+ [
+ 'recurrence' => [],
+ 'start' => new DateTime('2017-08-31 11:00:00')
+ ],
+ '2117-08-31 11:00:00', // expected result
+ ],
+ // daily
+ [
+ [
+ 'recurrence' => ['FREQ' => 'DAILY', 'INTERVAL' => '1', 'COUNT' => 2],
+ 'start' => new DateTime('2017-08-31 11:00:00')
+ ],
+ '2017-09-01 11:00:00',
+ ],
+ // weekly
+ [
+ [
+ 'recurrence' => ['FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'COUNT' => 3],
+ 'start' => new DateTime('2017-08-31 11:00:00'), // Thursday
+ ],
+ '2017-09-14 11:00:00',
+ ],
+ // UNTIL
+ [
+ [
+ 'recurrence' => ['FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'COUNT' => 3, 'UNTIL' => new DateTime('2017-09-07 11:00:00')],
+ 'start' => new DateTime('2017-08-31 11:00:00'), // Thursday
+ ],
+ '2017-09-07 11:00:00',
+ ],
+ // Infinite recurrence, no count, no until
+ [
+ [
+ 'recurrence' => ['FREQ' => 'WEEKLY', 'INTERVAL' => '1'],
+ 'start' => new DateTime('2017-08-31 11:00:00'), // Thursday
+ ],
+ '2117-08-31 11:00:00',
+ ],
- $this->assertEquals($expected, $first ? $first->format('Y-m-d H:i:s') : '');
+ // TODO: Test an event with EXDATE/RDATE
+ ];
}
/**
* Data for test_first_occurrence()
*/
function data_first_occurrence()
{
// TODO: BYYEARDAY, BYWEEKNO, BYSETPOS, WKST
return array(
// non-recurring
array(
array(), // recurrence data
'2017-08-31 11:00:00', // start date
'2017-08-31 11:00:00', // expected result
),
// daily
array(
array('FREQ' => 'DAILY', 'INTERVAL' => '1'), // recurrence data
'2017-08-31 11:00:00', // start date
'2017-08-31 11:00:00', // expected result
),
// TODO: this one is not supported by the Calendar UI
array(
array('FREQ' => 'DAILY', 'INTERVAL' => '1', 'BYMONTH' => 1),
'2017-08-31 11:00:00',
'2018-01-01 11:00:00',
),
// weekly
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1'),
'2017-08-31 11:00:00', // Thursday
'2017-08-31 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE'),
'2017-08-31 11:00:00', // Thursday
'2017-09-06 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'TH'),
'2017-08-31 11:00:00', // Thursday
'2017-08-31 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'FR'),
'2017-08-31 11:00:00', // Thursday
'2017-09-01 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '2'),
'2017-08-31 11:00:00', // Thursday
'2017-08-31 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '3', 'BYDAY' => 'WE'),
'2017-08-31 11:00:00', // Thursday
'2017-09-20 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE', 'COUNT' => 1),
'2017-08-31 11:00:00', // Thursday
'2017-09-06 11:00:00',
),
array(
array('FREQ' => 'WEEKLY', 'INTERVAL' => '1', 'BYDAY' => 'WE', 'UNTIL' => '2017-09-01'),
'2017-08-31 11:00:00', // Thursday
'',
),
// monthly
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1'),
'2017-09-08 11:00:00',
'2017-09-08 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYMONTHDAY' => '8,9'),
'2017-08-31 11:00:00',
'2017-09-08 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYMONTHDAY' => '8,9'),
'2017-09-08 11:00:00',
'2017-09-08 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYDAY' => '1WE'),
'2017-08-16 11:00:00',
'2017-09-06 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '1', 'BYDAY' => '-1WE'),
'2017-08-16 11:00:00',
'2017-08-30 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '2'),
'2017-09-08 11:00:00',
'2017-09-08 11:00:00',
),
array(
array('FREQ' => 'MONTHLY', 'INTERVAL' => '2', 'BYMONTHDAY' => '8'),
'2017-08-31 11:00:00',
'2017-09-08 11:00:00', // ??????
),
// yearly
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1'),
'2017-08-16 12:00:00',
'2017-08-16 12:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '8'),
'2017-08-16 12:00:00',
'2017-08-16 12:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYDAY' => '-1MO'),
'2017-08-16 11:00:00',
'2017-12-25 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '8', 'BYDAY' => '-1MO'),
'2017-08-16 11:00:00',
'2017-08-28 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '1', 'BYDAY' => '1MO'),
'2017-08-16 11:00:00',
'2018-01-01 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '1', 'BYMONTH' => '1,9', 'BYDAY' => '1MO'),
'2017-08-16 11:00:00',
'2017-09-04 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '2'),
'2017-08-16 11:00:00',
'2017-08-16 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '2', 'BYMONTH' => '8'),
'2017-08-16 11:00:00',
'2017-08-16 11:00:00',
),
array(
array('FREQ' => 'YEARLY', 'INTERVAL' => '2', 'BYDAY' => '-1MO'),
'2017-08-16 11:00:00',
'2017-12-25 11:00:00',
),
// on dates (FIXME: do we really expect the first occurrence to be on the start date?)
array(
array('RDATE' => array (new DateTime('2017-08-10 11:00:00 Europe/Warsaw'))),
'2017-08-01 11:00:00',
'2017-08-01 11:00:00',
),
);
}
+ /**
+ * kolab_date_recurrence::end()
+ *
+ * @dataProvider data_end
+ */
+ function test_end($event, $expected)
+ {
+ if (!kolab_format::supports(3)) {
+ $this->markTestSkipped('No Kolab support');
+ }
+
+ $object = kolab_format::factory('event', 3.0);
+ $object->set($event);
+
+ $recurrence = new kolab_date_recurrence($object);
+ $end = $recurrence->end();
+
+ $this->assertSame($expected, $end ? $end->format('Y-m-d H:i:s') : $end);
+ }
+
+ /**
+ * kolab_date_recurrence::first_occurrence()
+ *
+ * @dataProvider data_first_occurrence
+ */
+ function test_first_occurrence($recurrence_data, $start, $expected)
+ {
+ if (!kolab_format::supports(3)) {
+ $this->markTestSkipped('No Kolab support');
+ }
+
+ $start = new DateTime($start);
+ if (!empty($recurrence_data['UNTIL'])) {
+ $recurrence_data['UNTIL'] = new DateTime($recurrence_data['UNTIL']);
+ }
+
+ $event = array('start' => $start, 'recurrence' => $recurrence_data);
+ $object = kolab_format::factory('event', 3.0);
+ $object->set($event);
+
+ $recurrence = new kolab_date_recurrence($object);
+ $first = $recurrence->first_occurrence();
+
+ $this->assertEquals($expected, $first ? $first->format('Y-m-d H:i:s') : '');
+ }
+
/**
* kolab_date_recurrence::first_occurrence() for all-day events
*
* @dataProvider data_first_occurrence
*/
function test_first_occurrence_allday($recurrence_data, $start, $expected)
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
$start = new DateTime($start);
if (!empty($recurrence_data['UNTIL'])) {
$recurrence_data['UNTIL'] = new DateTime($recurrence_data['UNTIL']);
}
$event = array('start' => $start, 'recurrence' => $recurrence_data, 'allday' => true);
$object = kolab_format::factory('event', 3.0);
$object->set($event);
$recurrence = new kolab_date_recurrence($object);
$first = $recurrence->first_occurrence();
$this->assertEquals($expected, $first ? $first->format('Y-m-d H:i:s') : '');
}
/**
* kolab_date_recurrence::next_instance()
*/
function test_next_instance()
{
if (!kolab_format::supports(3)) {
$this->markTestSkipped('No Kolab support');
}
date_default_timezone_set('America/New_York');
$start = new DateTime('2017-08-31 11:00:00', new DateTimeZone('Europe/Berlin'));
$event = [
'start' => $start,
'recurrence' => ['FREQ' => 'WEEKLY', 'INTERVAL' => '1'],
'allday' => true,
];
$object = kolab_format::factory('event', 3.0);
$object->set($event);
$recurrence = new kolab_date_recurrence($object);
$next = $recurrence->next_instance();
$this->assertEquals($start->format('2017-09-07 H:i:s'), $next['start']->format('Y-m-d H:i:s'), 'Same time');
$this->assertEquals($start->getTimezone()->getName(), $next['start']->getTimezone()->getName(), 'Same timezone');
$this->assertSame(true, $next['start']->_dateonly, '_dateonly flag');
}
}