From 251d4e367ab3dc850da46f3eaf6f182bf8d3ab68 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Thu, 1 Dec 2022 15:03:56 +0100 Subject: [PATCH] Fixes in libcalendaring_recurrence.php --- .../lib/Horde_Date_Recurrence.php | 7 +- .../lib/libcalendaring_recurrence.php | 42 ++- .../libcalendaring/tests/RecurrenceTest.php | 272 ++++++++++++++++++ ...LibvcalendarTest.php => VcalendarTest.php} | 2 +- 4 files changed, 308 insertions(+), 15 deletions(-) create mode 100644 plugins/libcalendaring/tests/RecurrenceTest.php rename plugins/libcalendaring/tests/{LibvcalendarTest.php => VcalendarTest.php} (99%) diff --git a/plugins/libcalendaring/lib/Horde_Date_Recurrence.php b/plugins/libcalendaring/lib/Horde_Date_Recurrence.php index 9247257f..4dfdfaf6 100644 --- a/plugins/libcalendaring/lib/Horde_Date_Recurrence.php +++ b/plugins/libcalendaring/lib/Horde_Date_Recurrence.php @@ -1163,8 +1163,11 @@ class Horde_Date_Recurrence $rdata = array(); $parts = explode(';', $rrule); foreach ($parts as $part) { - list($key, $value) = explode('=', $part, 2); - $rdata[strtoupper($key)] = $value; + $value = null; + if (strpos($part, '=')) { + list($part, $value) = explode('=', $part, 2); + } + $rdata[strtoupper($part)] = $value; } if (isset($rdata['FREQ'])) { diff --git a/plugins/libcalendaring/lib/libcalendaring_recurrence.php b/plugins/libcalendaring/lib/libcalendaring_recurrence.php index a96f33eb..b62ba42f 100644 --- a/plugins/libcalendaring/lib/libcalendaring_recurrence.php +++ b/plugins/libcalendaring/lib/libcalendaring_recurrence.php @@ -39,11 +39,11 @@ class libcalendaring_recurrence */ function __construct($lib) { - // 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'); + // 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'); - $this->lib = $lib; + $this->lib = $lib; } /** @@ -85,7 +85,7 @@ class libcalendaring_recurrence public function set_start($start) { $this->start = $start; - $this->dateonly = $start->_dateonly; + $this->dateonly = !empty($start->_dateonly); $this->next = new Horde_Date($start, $this->lib->timezone->getName()); $this->hour = $this->next->hour; $this->engine->setRecurStart($this->next); @@ -94,26 +94,29 @@ class libcalendaring_recurrence /** * Get date/time of the next occurence of this event * - * @return DateTime|int|false object or False if recurrence ended + * @return DateTime|false object or False if recurrence ended */ public function next() { $time = false; $after = clone $this->next; $after->mday = $after->mday + 1; + if ($this->next && ($next = $this->engine->nextActiveRecurrence($after))) { // avoid endless loops if recurrence computation fails if (!$next->after($this->next)) { return false; } + // fix time for all-day events if ($this->dateonly) { $next->hour = $this->hour; $next->min = 0; } - $time = $next->toDateTime(); $this->next = $next; + + $time = $this->toDateTime($next); } return $time; @@ -224,12 +227,27 @@ class libcalendaring_recurrence return null; } - if ($start Instanceof Horde_Date) { - $start = $start->toDateTime(); - } - - $start->_dateonly = $this->dateonly; + $start = $this->toDateTime($start); return $start; } + + private function toDateTime($date) + { + if ($date Instanceof Horde_Date) { + $date = $date->toDateTime(); + } + + if ($date instanceof DateTimeInterface) { + $date = libcalendaring_datetime::createFromFormat( + 'Y-m-d\\TH:i:s', + $date->format('Y-m-d\\TH:i:s'), + $date->getTimezone() + ); + } + + $date->_dateonly = $this->dateonly; + + return $date; + } } diff --git a/plugins/libcalendaring/tests/RecurrenceTest.php b/plugins/libcalendaring/tests/RecurrenceTest.php new file mode 100644 index 00000000..7d191474 --- /dev/null +++ b/plugins/libcalendaring/tests/RecurrenceTest.php @@ -0,0 +1,272 @@ + + * + * 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 + */ + 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') : ''); + } + + /** + * 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::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() + */ + 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')); + $start->_dateonly = true; + + $recurrence = $this->plugin->get_recurrence(); + + $recurrence->init(['FREQ' => 'WEEKLY', 'INTERVAL' => '1'], $start); + + $next = $recurrence->next(); + + $this->assertEquals($start->format('2017-09-07 H:i:s'), $next->format('Y-m-d H:i:s'), 'Same time'); + $this->assertEquals($start->getTimezone()->getName(), $next->getTimezone()->getName(), 'Same timezone'); + $this->assertTrue($next->_dateonly, '_dateonly flag'); + } +} diff --git a/plugins/libcalendaring/tests/LibvcalendarTest.php b/plugins/libcalendaring/tests/VcalendarTest.php similarity index 99% rename from plugins/libcalendaring/tests/LibvcalendarTest.php rename to plugins/libcalendaring/tests/VcalendarTest.php index b687d2b5..232f673b 100644 --- a/plugins/libcalendaring/tests/LibvcalendarTest.php +++ b/plugins/libcalendaring/tests/VcalendarTest.php @@ -21,7 +21,7 @@ * along with this program. If not, see . */ -class LibvcalendarTest extends PHPUnit\Framework\TestCase +class VcalendarTest extends PHPUnit\Framework\TestCase { private $attachment_data;