Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2534257
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
106 KB
Referenced Files
None
Subscribers
None
View Options
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
@@ -1,1744 +1,1747 @@
<?php
/**
* This is a modified copy of Horde/Date/Recurrence.php (2015-01-05)
* Pull the latest version of this file from the PEAR channel of the Horde
* project at http://pear.horde.org by installing the Horde_Date package.
*/
if (!class_exists('Horde_Date')) {
require_once(__DIR__ . '/Horde_Date.php');
}
// minimal required implementation of Horde_Date_Translation to avoid a huge dependency nightmare
class Horde_Date_Translation
{
function t($arg) { return $arg; }
function ngettext($sing, $plur, $num) { return ($num > 1 ? $plur : $sing); }
}
/**
* This file contains the Horde_Date_Recurrence class and according constants.
*
* Copyright 2007-2015 Horde LLC (http://www.horde.org/)
*
* See the enclosed file COPYING for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @package Date
*/
/**
* The Horde_Date_Recurrence class implements algorithms for calculating
* recurrences of events, including several recurrence types, intervals,
* exceptions, and conversion from and to vCalendar and iCalendar recurrence
* rules.
*
* All methods expecting dates as parameters accept all values that the
* Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date
* object, an ISO time string or a hash.
*
* @author Jan Schneider <jan@horde.org>
* @category Horde
* @package Date
*/
class Horde_Date_Recurrence
{
/** No Recurrence **/
const RECUR_NONE = 0;
/** Recurs daily. */
const RECUR_DAILY = 1;
/** Recurs weekly. */
const RECUR_WEEKLY = 2;
/** Recurs monthly on the same date. */
const RECUR_MONTHLY_DATE = 3;
/** Recurs monthly on the same week day. */
const RECUR_MONTHLY_WEEKDAY = 4;
/** Recurs yearly on the same date. */
const RECUR_YEARLY_DATE = 5;
/** Recurs yearly on the same day of the year. */
const RECUR_YEARLY_DAY = 6;
/** Recurs yearly on the same week day. */
const RECUR_YEARLY_WEEKDAY = 7;
/**
* The start time of the event.
*
* @var Horde_Date
*/
public $start;
/**
* The end date of the recurrence interval.
*
* @var Horde_Date
*/
public $recurEnd = null;
/**
* The number of recurrences.
*
* @var integer
*/
public $recurCount = null;
/**
* The type of recurrence this event follows. RECUR_* constant.
*
* @var integer
*/
public $recurType = self::RECUR_NONE;
/**
* The length of time between recurrences. The time unit depends on the
* recurrence type.
*
* @var integer
*/
public $recurInterval = 1;
/**
* Any additional recurrence data.
*
* @var integer
*/
public $recurData = null;
/**
* BYDAY recurrence number
*
* @var integer
*/
public $recurNthDay = null;
/**
* BYMONTH recurrence data
*
* @var array
*/
public $recurMonths = array();
/**
* RDATE recurrence values
*
* @var array
*/
public $rdates = array();
/**
* All the exceptions from recurrence for this event.
*
* @var array
*/
public $exceptions = array();
/**
* All the dates this recurrence has been marked as completed.
*
* @var array
*/
public $completions = array();
/**
* Constructor.
*
* @param Horde_Date $start Start of the recurring event.
*/
public function __construct($start)
{
$this->start = new Horde_Date($start);
}
/**
* Resets the class properties.
*/
public function reset()
{
$this->recurEnd = null;
$this->recurCount = null;
$this->recurType = self::RECUR_NONE;
$this->recurInterval = 1;
$this->recurData = null;
$this->exceptions = array();
$this->completions = array();
}
/**
* Checks if this event recurs on a given day of the week.
*
* @param integer $dayMask A mask consisting of Horde_Date::MASK_*
* constants specifying the day(s) to check.
*
* @return boolean True if this event recurs on the given day(s).
*/
public function recurOnDay($dayMask)
{
return ($this->recurData & $dayMask);
}
/**
* Specifies the days this event recurs on.
*
* @param integer $dayMask A mask consisting of Horde_Date::MASK_*
* constants specifying the day(s) to recur on.
*/
public function setRecurOnDay($dayMask)
{
$this->recurData = $dayMask;
}
/**
*
* @param integer $nthDay The nth weekday of month to repeat events on
*/
public function setRecurNthWeekday($nth)
{
$this->recurNthDay = (int)$nth;
}
/**
*
* @return integer The nth weekday of month to repeat events.
*/
public function getRecurNthWeekday()
{
return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7);
}
/**
* Specifies the months for yearly (weekday) recurrence
*
* @param array $months List of months (integers) this event recurs on.
*/
function setRecurByMonth($months)
{
$this->recurMonths = (array)$months;
}
/**
* Returns a list of months this yearly event recurs on
*
* @return array List of months (integers) this event recurs on.
*/
function getRecurByMonth()
{
return $this->recurMonths;
}
/**
* Returns the days this event recurs on.
*
* @return integer A mask consisting of Horde_Date::MASK_* constants
* specifying the day(s) this event recurs on.
*/
public function getRecurOnDays()
{
return $this->recurData;
}
/**
* Returns whether this event has a specific recurrence type.
*
* @param integer $recurrence RECUR_* constant of the
* recurrence type to check for.
*
* @return boolean True if the event has the specified recurrence type.
*/
public function hasRecurType($recurrence)
{
return ($recurrence == $this->recurType);
}
/**
* Sets a recurrence type for this event.
*
* @param integer $recurrence A RECUR_* constant.
*/
public function setRecurType($recurrence)
{
$this->recurType = $recurrence;
}
/**
* Returns recurrence type of this event.
*
* @return integer A RECUR_* constant.
*/
public function getRecurType()
{
return $this->recurType;
}
/**
* Returns a description of this event's recurring type.
*
* @return string Human readable recurring type.
*/
public function getRecurName()
{
switch ($this->getRecurType()) {
case self::RECUR_NONE:
return Horde_Date_Translation::t("No recurrence");
case self::RECUR_DAILY:
return Horde_Date_Translation::t("Daily");
case self::RECUR_WEEKLY:
return Horde_Date_Translation::t("Weekly");
case self::RECUR_MONTHLY_DATE:
case self::RECUR_MONTHLY_WEEKDAY:
return Horde_Date_Translation::t("Monthly");
case self::RECUR_YEARLY_DATE:
case self::RECUR_YEARLY_DAY:
case self::RECUR_YEARLY_WEEKDAY:
return Horde_Date_Translation::t("Yearly");
}
}
/**
* Sets the length of time between recurrences of this event.
*
* @param integer $interval The time between recurrences.
*/
public function setRecurInterval($interval)
{
if ($interval > 0) {
$this->recurInterval = $interval;
}
}
/**
* Retrieves the length of time between recurrences of this event.
*
* @return integer The number of seconds between recurrences.
*/
public function getRecurInterval()
{
return $this->recurInterval;
}
/**
* Sets the number of recurrences of this event.
*
* @param integer $count The number of recurrences.
*/
public function setRecurCount($count)
{
if ($count > 0) {
$this->recurCount = (int)$count;
// Recurrence counts and end dates are mutually exclusive.
$this->recurEnd = null;
} else {
$this->recurCount = null;
}
}
/**
* Retrieves the number of recurrences of this event.
*
* @return integer The number recurrences.
*/
public function getRecurCount()
{
return $this->recurCount;
}
/**
* Returns whether this event has a recurrence with a fixed count.
*
* @return boolean True if this recurrence has a fixed count.
*/
public function hasRecurCount()
{
return isset($this->recurCount);
}
/**
* Sets the start date of the recurrence interval.
*
* @param Horde_Date $start The recurrence start.
*/
public function setRecurStart($start)
{
$this->start = clone $start;
}
/**
* Retrieves the start date of the recurrence interval.
*
* @return Horde_Date The recurrence start.
*/
public function getRecurStart()
{
return $this->start;
}
/**
* Sets the end date of the recurrence interval.
*
* @param Horde_Date $end The recurrence end.
*/
public function setRecurEnd($end)
{
if (!empty($end)) {
// Recurrence counts and end dates are mutually exclusive.
$this->recurCount = null;
$this->recurEnd = clone $end;
} else {
$this->recurEnd = $end;
}
}
/**
* Retrieves the end date of the recurrence interval.
*
* @return Horde_Date The recurrence end.
*/
public function getRecurEnd()
{
return $this->recurEnd;
}
/**
* Returns whether this event has a recurrence end.
*
* @return boolean True if this recurrence ends.
*/
public function hasRecurEnd()
{
return isset($this->recurEnd) && isset($this->recurEnd->year) &&
$this->recurEnd->year != 9999;
}
/**
* Finds the next recurrence of this event that's after $afterDate.
*
* @param Horde_Date|string $after Return events after this date.
*
* @return Horde_Date|boolean The date of the next recurrence or false
* if the event does not recur after
* $afterDate.
*/
public function nextRecurrence($after)
{
if (!($after instanceof Horde_Date)) {
$after = new Horde_Date($after);
} else {
$after = clone($after);
}
// Make sure $after and $this->start are in the same TZ
$after->setTimezone($this->start->timezone);
if ($this->start->compareDateTime($after) >= 0) {
return clone $this->start;
}
if ($this->recurInterval == 0 && empty($this->rdates)) {
return false;
}
switch ($this->getRecurType()) {
case self::RECUR_DAILY:
$diff = $this->start->diff($after);
$recur = ceil($diff / $this->recurInterval);
if ($this->recurCount && $recur >= $this->recurCount) {
return false;
}
$recur *= $this->recurInterval;
$next = $this->start->add(array('day' => $recur));
if ((!$this->hasRecurEnd() ||
$next->compareDateTime($this->recurEnd) <= 0) &&
$next->compareDateTime($after) >= 0) {
return $next;
}
break;
case self::RECUR_WEEKLY:
if (empty($this->recurData)) {
return false;
}
$start_week = Horde_Date_Utils::firstDayOfWeek($this->start->format('W'),
$this->start->year);
$start_week->timezone = $this->start->timezone;
$start_week->hour = $this->start->hour;
$start_week->min = $this->start->min;
$start_week->sec = $this->start->sec;
// Make sure we are not at the ISO-8601 first week of year while
// still in month 12...OR in the ISO-8601 last week of year while
// in month 1 and adjust the year accordingly.
$week = $after->format('W');
if ($week == 1 && $after->month == 12) {
$theYear = $after->year + 1;
} elseif ($week >= 52 && $after->month == 1) {
$theYear = $after->year - 1;
} else {
$theYear = $after->year;
}
$after_week = Horde_Date_Utils::firstDayOfWeek($week, $theYear);
$after_week->timezone = $this->start->timezone;
$after_week_end = clone $after_week;
$after_week_end->mday += 7;
$diff = $start_week->diff($after_week);
$interval = $this->recurInterval * 7;
$repeats = floor($diff / $interval);
if ($diff % $interval < 7) {
$recur = $diff;
} else {
/**
* If the after_week is not in the first week interval the
* search needs to skip ahead a complete interval. The way it is
* calculated here means that an event that occurs every second
* week on Monday and Wednesday with the event actually starting
* on Tuesday or Wednesday will only have one incidence in the
* first week.
*/
$recur = $interval * ($repeats + 1);
}
if ($this->hasRecurCount()) {
$recurrences = 0;
/**
* Correct the number of recurrences by the number of events
* that lay between the start of the start week and the
* recurrence start.
*/
$next = clone $start_week;
while ($next->compareDateTime($this->start) < 0) {
if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
$recurrences--;
}
++$next->mday;
}
if ($repeats > 0) {
$weekdays = $this->recurData;
$total_recurrences_per_week = 0;
while ($weekdays > 0) {
if ($weekdays % 2) {
$total_recurrences_per_week++;
}
$weekdays = ($weekdays - ($weekdays % 2)) / 2;
}
$recurrences += $total_recurrences_per_week * $repeats;
}
}
$next = clone $start_week;
$next->mday += $recur;
while ($next->compareDateTime($after) < 0 &&
$next->compareDateTime($after_week_end) < 0) {
if ($this->hasRecurCount()
&& $next->compareDateTime($after) < 0
&& $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
$recurrences++;
}
++$next->mday;
}
if ($this->hasRecurCount() &&
$recurrences >= $this->recurCount) {
return false;
}
if (!$this->hasRecurEnd() ||
$next->compareDateTime($this->recurEnd) <= 0) {
if ($next->compareDateTime($after_week_end) >= 0) {
return $this->nextRecurrence($after_week_end);
}
while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) &&
$next->compareDateTime($after_week_end) < 0) {
++$next->mday;
}
if (!$this->hasRecurEnd() ||
$next->compareDateTime($this->recurEnd) <= 0) {
if ($next->compareDateTime($after_week_end) >= 0) {
return $this->nextRecurrence($after_week_end);
} else {
return $next;
}
}
}
break;
case self::RECUR_MONTHLY_DATE:
$start = clone $this->start;
if ($after->compareDateTime($start) < 0) {
$after = clone $start;
} else {
$after = clone $after;
}
// If we're starting past this month's recurrence of the event,
// look in the next month on the day the event recurs.
if ($after->mday > $start->mday) {
++$after->month;
$after->mday = $start->mday;
}
// Adjust $start to be the first match.
$offset = ($after->month - $start->month) + ($after->year - $start->year) * 12;
$offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
if ($this->recurCount &&
($offset / $this->recurInterval) >= $this->recurCount) {
return false;
}
$start->month += $offset;
$count = $offset / $this->recurInterval;
do {
if ($this->recurCount &&
$count++ >= $this->recurCount) {
return false;
}
// Bail if we've gone past the end of recurrence.
if ($this->hasRecurEnd() &&
$this->recurEnd->compareDateTime($start) < 0) {
return false;
}
if ($start->isValid()) {
return $start;
}
// If the interval is 12, and the date isn't valid, then we
// need to see if February 29th is an option. If not, then the
// event will _never_ recur, and we need to stop checking to
// avoid an infinite loop.
if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) {
return false;
}
// Add the recurrence interval.
$start->month += $this->recurInterval;
} while (true);
break;
case self::RECUR_MONTHLY_WEEKDAY:
// Start with the start date of the event.
$estart = clone $this->start;
// What day of the week, and week of the month, do we recur on?
if (isset($this->recurNthDay)) {
$nth = $this->recurNthDay;
$weekday = log($this->recurData, 2);
} else {
$nth = ceil($this->start->mday / 7);
$weekday = $estart->dayOfWeek();
}
// Adjust $estart to be the first candidate.
$offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
$offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
// Adjust our working date until it's after $after.
$estart->month += $offset - $this->recurInterval;
$count = $offset / $this->recurInterval;
do {
if ($this->recurCount &&
$count++ >= $this->recurCount) {
return false;
}
$estart->month += $this->recurInterval;
$next = clone $estart;
$next->setNthWeekday($weekday, $nth);
if ($next->month != $estart->month) {
// We're already in the next month.
continue;
}
if ($next->compareDateTime($after) < 0) {
// We haven't made it past $after yet, try again.
continue;
}
if ($this->hasRecurEnd() &&
$next->compareDateTime($this->recurEnd) > 0) {
// We've gone past the end of recurrence; we can give up
// now.
return false;
}
// We have a candidate to return.
break;
} while (true);
return $next;
case self::RECUR_YEARLY_DATE:
// Start with the start date of the event.
$estart = clone $this->start;
$after = clone $after;
if ($after->month > $estart->month ||
($after->month == $estart->month && $after->mday > $estart->mday)) {
++$after->year;
$after->month = $estart->month;
$after->mday = $estart->mday;
}
// Seperate case here for February 29th
if ($estart->month == 2 && $estart->mday == 29) {
while (!Horde_Date_Utils::isLeapYear($after->year)) {
++$after->year;
}
}
// Adjust $estart to be the first candidate.
$offset = $after->year - $estart->year;
if ($offset > 0) {
$offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
$estart->year += $offset;
}
// We've gone past the end of recurrence; give up.
if ($this->recurCount &&
$offset >= $this->recurCount) {
return false;
}
if ($this->hasRecurEnd() &&
$this->recurEnd->compareDateTime($estart) < 0) {
return false;
}
return $estart;
case self::RECUR_YEARLY_DAY:
// Check count first.
$dayofyear = $this->start->dayOfYear();
$count = ($after->year - $this->start->year) / $this->recurInterval + 1;
if ($this->recurCount &&
($count > $this->recurCount ||
($count == $this->recurCount &&
$after->dayOfYear() > $dayofyear))) {
return false;
}
// Start with a rough interval.
$estart = clone $this->start;
$estart->year += floor($count - 1) * $this->recurInterval;
// Now add the difference to the required day of year.
$estart->mday += $dayofyear - $estart->dayOfYear();
// Add an interval if the estimation was wrong.
if ($estart->compareDate($after) < 0) {
$estart->year += $this->recurInterval;
$estart->mday += $dayofyear - $estart->dayOfYear();
}
// We've gone past the end of recurrence; give up.
if ($this->hasRecurEnd() &&
$this->recurEnd->compareDateTime($estart) < 0) {
return false;
}
return $estart;
case self::RECUR_YEARLY_WEEKDAY:
// Start with the start date of the event.
$estart = clone $this->start;
// What day of the week, and week of the month, do we recur on?
if (isset($this->recurNthDay)) {
$nth = $this->recurNthDay;
$weekday = log($this->recurData, 2);
} else {
$nth = ceil($this->start->mday / 7);
$weekday = $estart->dayOfWeek();
}
// Adjust $estart to be the first candidate.
$offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
// Adjust our working date until it's after $after.
$estart->year += $offset - $this->recurInterval;
$count = $offset / $this->recurInterval;
do {
if ($this->recurCount &&
$count++ >= $this->recurCount) {
return false;
}
$estart->year += $this->recurInterval;
$next = clone $estart;
$next->setNthWeekday($weekday, $nth);
if ($next->compareDateTime($after) < 0) {
// We haven't made it past $after yet, try again.
continue;
}
if ($this->hasRecurEnd() &&
$next->compareDateTime($this->recurEnd) > 0) {
// We've gone past the end of recurrence; we can give up
// now.
return false;
}
// We have a candidate to return.
break;
} while (true);
return $next;
}
// fall-back to RDATE properties
if (!empty($this->rdates)) {
$next = clone $this->start;
foreach ($this->rdates as $rdate) {
$next->year = $rdate->year;
$next->month = $rdate->month;
$next->mday = $rdate->mday;
if ($next->compareDateTime($after) >= 0) {
return $next;
}
}
}
// We didn't find anything, the recurType was bad, or something else
// went wrong - return false.
return false;
}
/**
* Returns whether this event has any date that matches the recurrence
* rules and is not an exception.
*
* @return boolean True if an active recurrence exists.
*/
public function hasActiveRecurrence()
{
if (!$this->hasRecurEnd()) {
return true;
}
$next = $this->nextRecurrence(new Horde_Date($this->start));
while (is_object($next)) {
if (!$this->hasException($next->year, $next->month, $next->mday) &&
!$this->hasCompletion($next->year, $next->month, $next->mday)) {
return true;
}
$next = $this->nextRecurrence($next->add(array('day' => 1)));
}
return false;
}
/**
* Returns the next active recurrence.
*
* @param Horde_Date $afterDate Return events after this date.
*
* @return Horde_Date|boolean The date of the next active
* recurrence or false if the event
* has no active recurrence after
* $afterDate.
*/
public function nextActiveRecurrence($afterDate)
{
$next = $this->nextRecurrence($afterDate);
while (is_object($next)) {
if (!$this->hasException($next->year, $next->month, $next->mday) &&
!$this->hasCompletion($next->year, $next->month, $next->mday)) {
return $next;
}
$next->mday++;
$next = $this->nextRecurrence($next);
}
return false;
}
/**
* Adds an absolute recurrence date.
*
* @param integer $year The year of the instance.
* @param integer $month The month of the instance.
* @param integer $mday The day of the month of the instance.
*/
public function addRDate($year, $month, $mday)
{
$this->rdates[] = new Horde_Date($year, $month, $mday);
}
/**
* Adds an exception to a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the exception.
*/
public function addException($year, $month, $mday)
{
$key = sprintf('%04d%02d%02d', $year, $month, $mday);
if (array_search($key, $this->exceptions) === false) {
$this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
}
}
/**
* Deletes an exception from a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the exception.
*/
public function deleteException($year, $month, $mday)
{
$key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions);
if ($key !== false) {
unset($this->exceptions[$key]);
}
}
/**
* Checks if an exception exists for a given reccurence of an event.
*
* @param integer $year The year of the reucrance.
* @param integer $month The month of the reucrance.
* @param integer $mday The day of the month of the reucrance.
*
* @return boolean True if an exception exists for the given date.
*/
public function hasException($year, $month, $mday)
{
return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
$this->getExceptions());
}
/**
* Retrieves all the exceptions for this event.
*
* @return array Array containing the dates of all the exceptions in
* YYYYMMDD form.
*/
public function getExceptions()
{
return $this->exceptions;
}
/**
* Adds a completion to a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the completion.
*/
public function addCompletion($year, $month, $mday)
{
$this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
}
/**
* Deletes a completion from a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the completion.
*/
public function deleteCompletion($year, $month, $mday)
{
$key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions);
if ($key !== false) {
unset($this->completions[$key]);
}
}
/**
* Checks if a completion exists for a given reccurence of an event.
*
* @param integer $year The year of the reucrance.
* @param integer $month The month of the recurrance.
* @param integer $mday The day of the month of the recurrance.
*
* @return boolean True if a completion exists for the given date.
*/
public function hasCompletion($year, $month, $mday)
{
return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
$this->getCompletions());
}
/**
* Retrieves all the completions for this event.
*
* @return array Array containing the dates of all the completions in
* YYYYMMDD form.
*/
public function getCompletions()
{
return $this->completions;
}
/**
* Parses a vCalendar 1.0 recurrence rule.
*
* @link http://www.imc.org/pdi/vcal-10.txt
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param string $rrule A vCalendar 1.0 conform RRULE value.
*/
public function fromRRule10($rrule)
{
$this->reset();
if (!$rrule) {
return;
}
if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) {
// No recurrence data - event does not recur.
$this->setRecurType(self::RECUR_NONE);
}
// Always default the recurInterval to 1.
$this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1);
$remainder = trim($matches[3]);
switch ($matches[1]) {
case 'D':
$this->setRecurType(self::RECUR_DAILY);
break;
case 'W':
$this->setRecurType(self::RECUR_WEEKLY);
if (!empty($remainder)) {
$mask = 0;
while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
$day = trim($matches[0]);
$remainder = substr($remainder, strlen($matches[0]));
$mask |= $maskdays[$day];
}
$this->setRecurOnDay($mask);
} else {
// Recur on the day of the week of the original recurrence.
$maskdays = array(
Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY,
);
$this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
}
break;
case 'MP':
$this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
break;
case 'MD':
$this->setRecurType(self::RECUR_MONTHLY_DATE);
break;
case 'YM':
$this->setRecurType(self::RECUR_YEARLY_DATE);
break;
case 'YD':
$this->setRecurType(self::RECUR_YEARLY_DAY);
break;
}
// We don't support modifiers at the moment, strip them.
while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) {
$remainder = substr($remainder, 1);
}
if (!empty($remainder)) {
if (strpos($remainder, '#') === 0) {
$this->setRecurCount(substr($remainder, 1));
} else {
list($year, $month, $mday, $hour, $min, $sec, $tz) =
sscanf($remainder, '%04d%02d%02dT%02d%02d%02d%s');
$this->setRecurEnd(new Horde_Date(array('year' => $year,
'month' => $month,
'mday' => $mday,
'hour' => $hour,
'min' => $min,
'sec' => $sec),
$tz == 'Z' ? 'UTC' : $this->start->timezone));
}
}
}
/**
* Creates a vCalendar 1.0 recurrence rule.
*
* @link http://www.imc.org/pdi/vcal-10.txt
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param Horde_Icalendar $calendar A Horde_Icalendar object instance.
*
* @return string A vCalendar 1.0 conform RRULE value.
*/
public function toRRule10($calendar)
{
switch ($this->recurType) {
case self::RECUR_NONE:
return '';
case self::RECUR_DAILY:
$rrule = 'D' . $this->recurInterval;
break;
case self::RECUR_WEEKLY:
$rrule = 'W' . $this->recurInterval;
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
for ($i = 0; $i <= 7; ++$i) {
if ($this->recurOnDay(pow(2, $i))) {
$rrule .= ' ' . $vcaldays[$i];
}
}
break;
case self::RECUR_MONTHLY_DATE:
$rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday);
break;
case self::RECUR_MONTHLY_WEEKDAY:
$nth_weekday = (int)($this->start->mday / 7);
if (($this->start->mday % 7) > 0) {
$nth_weekday++;
}
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()];
break;
case self::RECUR_YEARLY_DATE:
$rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month);
break;
case self::RECUR_YEARLY_DAY:
$rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear();
break;
default:
return '';
}
if ($this->hasRecurEnd()) {
$recurEnd = clone $this->recurEnd;
return $rrule . ' ' . $calendar->_exportDateTime($recurEnd);
}
return $rrule . ' #' . (int)$this->getRecurCount();
}
/**
* Parses an iCalendar 2.0 recurrence rule.
*
* @link http://rfc.net/rfc2445.html#s4.3.10
* @link http://rfc.net/rfc2445.html#s4.8.5
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param string $rrule An iCalendar 2.0 conform RRULE value.
*/
public function fromRRule20($rrule)
{
$this->reset();
// Parse the recurrence rule into keys and values.
$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'])) {
// Always default the recurInterval to 1.
$this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
$maskdays = array(
'SU' => Horde_Date::MASK_SUNDAY,
'MO' => Horde_Date::MASK_MONDAY,
'TU' => Horde_Date::MASK_TUESDAY,
'WE' => Horde_Date::MASK_WEDNESDAY,
'TH' => Horde_Date::MASK_THURSDAY,
'FR' => Horde_Date::MASK_FRIDAY,
'SA' => Horde_Date::MASK_SATURDAY,
);
switch (strtoupper($rdata['FREQ'])) {
case 'DAILY':
$this->setRecurType(self::RECUR_DAILY);
break;
case 'WEEKLY':
$this->setRecurType(self::RECUR_WEEKLY);
if (isset($rdata['BYDAY'])) {
$days = explode(',', $rdata['BYDAY']);
$mask = 0;
foreach ($days as $day) {
$mask |= $maskdays[$day];
}
$this->setRecurOnDay($mask);
} else {
// Recur on the day of the week of the original
// recurrence.
$maskdays = array(
Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY);
$this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
}
break;
case 'MONTHLY':
if (isset($rdata['BYDAY'])) {
$this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
$this->setRecurOnDay($maskdays[$m[2]]);
$this->setRecurNthWeekday($m[1]);
}
} else {
$this->setRecurType(self::RECUR_MONTHLY_DATE);
}
break;
case 'YEARLY':
if (isset($rdata['BYYEARDAY'])) {
$this->setRecurType(self::RECUR_YEARLY_DAY);
} elseif (isset($rdata['BYDAY'])) {
$this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
$this->setRecurOnDay($maskdays[$m[2]]);
$this->setRecurNthWeekday($m[1]);
}
if ($rdata['BYMONTH']) {
$months = explode(',', $rdata['BYMONTH']);
$this->setRecurByMonth($months);
}
} else {
$this->setRecurType(self::RECUR_YEARLY_DATE);
}
break;
}
// MUST take into account the time portion if it is present.
// See Bug: 12869 and Bug: 2813
if (isset($rdata['UNTIL'])) {
if (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $rdata['UNTIL'], $parts)) {
$until = new Horde_Date($rdata['UNTIL'], 'UTC');
$until->setTimezone($this->start->timezone);
} else {
list($year, $month, $mday) = sscanf($rdata['UNTIL'],
'%04d%02d%02d');
$until = new Horde_Date(
array('year' => $year,
'month' => $month,
'mday' => $mday + 1),
$this->start->timezone
);
}
$this->setRecurEnd($until);
}
if (isset($rdata['COUNT'])) {
$this->setRecurCount($rdata['COUNT']);
}
} else {
// No recurrence data - event does not recur.
$this->setRecurType(self::RECUR_NONE);
}
}
/**
* Creates an iCalendar 2.0 recurrence rule.
*
* @link http://rfc.net/rfc2445.html#s4.3.10
* @link http://rfc.net/rfc2445.html#s4.8.5
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param Horde_Icalendar $calendar A Horde_Icalendar object instance.
*
* @return string An iCalendar 2.0 conform RRULE value.
*/
public function toRRule20($calendar)
{
switch ($this->recurType) {
case self::RECUR_NONE:
return '';
case self::RECUR_DAILY:
$rrule = 'FREQ=DAILY;INTERVAL=' . $this->recurInterval;
break;
case self::RECUR_WEEKLY:
$rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval;
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
for ($i = $flag = 0; $i <= 7; ++$i) {
if ($this->recurOnDay(pow(2, $i))) {
if ($flag == 0) {
$rrule .= ';BYDAY=';
$flag = 1;
} else {
$rrule .= ',';
}
$rrule .= $vcaldays[$i];
}
}
break;
case self::RECUR_MONTHLY_DATE:
$rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval;
break;
case self::RECUR_MONTHLY_WEEKDAY:
if (isset($this->recurNthDay)) {
$nth_weekday = $this->recurNthDay;
$day_of_week = log($this->recurData, 2);
} else {
$day_of_week = $this->start->dayOfWeek();
$nth_weekday = (int)($this->start->mday / 7);
if (($this->start->mday % 7) > 0) {
$nth_weekday++;
}
}
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
. ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week];
break;
case self::RECUR_YEARLY_DATE:
$rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval;
break;
case self::RECUR_YEARLY_DAY:
$rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
. ';BYYEARDAY=' . $this->start->dayOfYear();
break;
case self::RECUR_YEARLY_WEEKDAY:
if (isset($this->recurNthDay)) {
$nth_weekday = $this->recurNthDay;
$day_of_week = log($this->recurData, 2);
} else {
$day_of_week = $this->start->dayOfWeek();
$nth_weekday = (int)($this->start->mday / 7);
if (($this->start->mday % 7) > 0) {
$nth_weekday++;
}
}
$months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month;
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
. ';BYDAY='
. $nth_weekday
. $vcaldays[$day_of_week]
. ';BYMONTH=' . $this->start->month;
break;
}
if ($this->hasRecurEnd()) {
$recurEnd = clone $this->recurEnd;
$rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd);
}
if ($count = $this->getRecurCount()) {
$rrule .= ';COUNT=' . $count;
}
return $rrule;
}
/**
* Parses the recurrence data from a Kolab hash.
*
* @param array $hash The hash to convert.
*
* @return boolean True if the hash seemed valid, false otherwise.
*/
public function fromKolab($hash)
{
$this->reset();
if (!isset($hash['interval']) || !isset($hash['cycle'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$this->setRecurInterval((int)$hash['interval']);
$month2number = array(
'january' => 1,
'february' => 2,
'march' => 3,
'april' => 4,
'may' => 5,
'june' => 6,
'july' => 7,
'august' => 8,
'september' => 9,
'october' => 10,
'november' => 11,
'december' => 12,
);
$parse_day = false;
$set_daymask = false;
$update_month = false;
$update_daynumber = false;
$update_weekday = false;
$nth_weekday = -1;
switch ($hash['cycle']) {
case 'daily':
$this->setRecurType(self::RECUR_DAILY);
break;
case 'weekly':
$this->setRecurType(self::RECUR_WEEKLY);
$parse_day = true;
$set_daymask = true;
break;
case 'monthly':
if (!isset($hash['daynumber'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
switch ($hash['type']) {
case 'daynumber':
$this->setRecurType(self::RECUR_MONTHLY_DATE);
$update_daynumber = true;
break;
case 'weekday':
$this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
$this->setRecurNthWeekday($hash['daynumber']);
$parse_day = true;
$set_daymask = true;
break;
}
break;
case 'yearly':
if (!isset($hash['type'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
switch ($hash['type']) {
case 'monthday':
$this->setRecurType(self::RECUR_YEARLY_DATE);
$update_month = true;
$update_daynumber = true;
break;
case 'yearday':
if (!isset($hash['daynumber'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$this->setRecurType(self::RECUR_YEARLY_DAY);
// Start counting days in January.
$hash['month'] = 'january';
$update_month = true;
$update_daynumber = true;
break;
case 'weekday':
if (!isset($hash['daynumber'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
$this->setRecurNthWeekday($hash['daynumber']);
$parse_day = true;
$set_daymask = true;
if ($hash['month'] && isset($month2number[$hash['month']])) {
$this->setRecurByMonth($month2number[$hash['month']]);
}
break;
}
}
if (isset($hash['range-type']) && isset($hash['range'])) {
switch ($hash['range-type']) {
case 'number':
$this->setRecurCount((int)$hash['range']);
break;
case 'date':
$recur_end = new Horde_Date($hash['range']);
$recur_end->hour = 23;
$recur_end->min = 59;
$recur_end->sec = 59;
$this->setRecurEnd($recur_end);
break;
}
}
// Need to parse <day>?
$last_found_day = -1;
if ($parse_day) {
if (!isset($hash['day'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$mask = 0;
$bits = array(
'monday' => Horde_Date::MASK_MONDAY,
'tuesday' => Horde_Date::MASK_TUESDAY,
'wednesday' => Horde_Date::MASK_WEDNESDAY,
'thursday' => Horde_Date::MASK_THURSDAY,
'friday' => Horde_Date::MASK_FRIDAY,
'saturday' => Horde_Date::MASK_SATURDAY,
'sunday' => Horde_Date::MASK_SUNDAY,
);
$days = array(
'monday' => Horde_Date::DATE_MONDAY,
'tuesday' => Horde_Date::DATE_TUESDAY,
'wednesday' => Horde_Date::DATE_WEDNESDAY,
'thursday' => Horde_Date::DATE_THURSDAY,
'friday' => Horde_Date::DATE_FRIDAY,
'saturday' => Horde_Date::DATE_SATURDAY,
'sunday' => Horde_Date::DATE_SUNDAY,
);
foreach ($hash['day'] as $day) {
// Validity check.
if (empty($day) || !isset($bits[$day])) {
continue;
}
$mask |= $bits[$day];
$last_found_day = $days[$day];
}
if ($set_daymask) {
$this->setRecurOnDay($mask);
}
}
if ($update_month || $update_daynumber || $update_weekday) {
if ($update_month) {
if (isset($month2number[$hash['month']])) {
$this->start->month = $month2number[$hash['month']];
}
}
if ($update_daynumber) {
if (!isset($hash['daynumber'])) {
$this->setRecurType(self::RECUR_NONE);
return false;
}
$this->start->mday = $hash['daynumber'];
}
if ($update_weekday) {
$this->setNthWeekday($nth_weekday);
}
}
// Exceptions.
if (isset($hash['exclusion'])) {
foreach ($hash['exclusion'] as $exception) {
if ($exception instanceof DateTime) {
$this->exceptions[] = $exception->format('Ymd');
}
}
}
if (isset($hash['complete'])) {
foreach ($hash['complete'] as $completion) {
if ($exception instanceof DateTime) {
$this->completions[] = $completion->format('Ymd');
}
}
}
return true;
}
/**
* Export this object into a Kolab hash.
*
* @return array The recurrence hash.
*/
public function toKolab()
{
if ($this->getRecurType() == self::RECUR_NONE) {
return array();
}
$day2number = array(
0 => 'sunday',
1 => 'monday',
2 => 'tuesday',
3 => 'wednesday',
4 => 'thursday',
5 => 'friday',
6 => 'saturday'
);
$month2number = array(
1 => 'january',
2 => 'february',
3 => 'march',
4 => 'april',
5 => 'may',
6 => 'june',
7 => 'july',
8 => 'august',
9 => 'september',
10 => 'october',
11 => 'november',
12 => 'december'
);
$hash = array('interval' => $this->getRecurInterval());
$start = $this->getRecurStart();
switch ($this->getRecurType()) {
case self::RECUR_DAILY:
$hash['cycle'] = 'daily';
break;
case self::RECUR_WEEKLY:
$hash['cycle'] = 'weekly';
$bits = array(
'monday' => Horde_Date::MASK_MONDAY,
'tuesday' => Horde_Date::MASK_TUESDAY,
'wednesday' => Horde_Date::MASK_WEDNESDAY,
'thursday' => Horde_Date::MASK_THURSDAY,
'friday' => Horde_Date::MASK_FRIDAY,
'saturday' => Horde_Date::MASK_SATURDAY,
'sunday' => Horde_Date::MASK_SUNDAY,
);
$days = array();
foreach ($bits as $name => $bit) {
if ($this->recurOnDay($bit)) {
$days[] = $name;
}
}
$hash['day'] = $days;
break;
case self::RECUR_MONTHLY_DATE:
$hash['cycle'] = 'monthly';
$hash['type'] = 'daynumber';
$hash['daynumber'] = $start->mday;
break;
case self::RECUR_MONTHLY_WEEKDAY:
$hash['cycle'] = 'monthly';
$hash['type'] = 'weekday';
$hash['daynumber'] = $start->weekOfMonth();
$hash['day'] = array ($day2number[$start->dayOfWeek()]);
break;
case self::RECUR_YEARLY_DATE:
$hash['cycle'] = 'yearly';
$hash['type'] = 'monthday';
$hash['daynumber'] = $start->mday;
$hash['month'] = $month2number[$start->month];
break;
case self::RECUR_YEARLY_DAY:
$hash['cycle'] = 'yearly';
$hash['type'] = 'yearday';
$hash['daynumber'] = $start->dayOfYear();
break;
case self::RECUR_YEARLY_WEEKDAY:
$hash['cycle'] = 'yearly';
$hash['type'] = 'weekday';
$hash['daynumber'] = $start->weekOfMonth();
$hash['day'] = array ($day2number[$start->dayOfWeek()]);
$hash['month'] = $month2number[$start->month];
}
if ($this->hasRecurCount()) {
$hash['range-type'] = 'number';
$hash['range'] = $this->getRecurCount();
} elseif ($this->hasRecurEnd()) {
$date = $this->getRecurEnd();
$hash['range-type'] = 'date';
$hash['range'] = $date->toDateTime();
} else {
$hash['range-type'] = 'none';
$hash['range'] = '';
}
// Recurrence exceptions
$hash['exclusion'] = $hash['complete'] = array();
foreach ($this->exceptions as $exception) {
$hash['exclusion'][] = new DateTime($exception);
}
foreach ($this->completions as $completionexception) {
$hash['complete'][] = new DateTime($completionexception);
}
return $hash;
}
/**
* Returns a simple object suitable for json transport representing this
* object.
*
* Possible properties are:
* - t: type
* - i: interval
* - e: end date
* - c: count
* - d: data
* - co: completions
* - ex: exceptions
*
* @return object A simple object.
*/
public function toJson()
{
$json = new stdClass;
$json->t = $this->recurType;
$json->i = $this->recurInterval;
if ($this->hasRecurEnd()) {
$json->e = $this->recurEnd->toJson();
}
if ($this->recurCount) {
$json->c = $this->recurCount;
}
if ($this->recurData) {
$json->d = $this->recurData;
}
if ($this->completions) {
$json->co = $this->completions;
}
if ($this->exceptions) {
$json->ex = $this->exceptions;
}
return $json;
}
}
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
@@ -1,235 +1,253 @@
<?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
*
* @param calendar The calendar plugin instance
*/
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;
}
/**
* Initialize recurrence engine
*
* @param array The recurrence properties
* @param DateTime The recurrence start date
*/
public function init($recurrence, $start = null)
{
$this->recurrence = $recurrence;
$this->engine = new Horde_Date_Recurrence($start);
$this->engine->fromRRule20(libcalendaring::to_rrule($recurrence));
$this->set_start($start);
if (!empty($recurrence['EXDATE'])) {
foreach ((array) $recurrence['EXDATE'] as $exdate) {
if ($exdate instanceof DateTimeInterface) {
$this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
}
}
}
if (!empty($recurrence['RDATE'])) {
foreach ((array) $recurrence['RDATE'] as $rdate) {
if ($rdate instanceof DateTimeInterface) {
$this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
}
}
}
}
/**
* Setter for (new) recurrence start date
*
* @param DateTime The recurrence start date
*/
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);
}
/**
* 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;
}
/**
* 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
if ($this->recurrence['UNTIL'] instanceof DateTimeInterface) {
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']);
if ($last instanceof DateTimeInterface) {
return $last;
}
}
// 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());
while (($next = $this->next()) && $c < 1000) {
$last = $next;
$c++;
}
}
return $last;
}
/**
* 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;
$interval = !empty($r['INTERVAL']) ? intval($r['INTERVAL']) : 1;
$frequency = isset($this->recurrence['FREQ']) ? $this->recurrence['FREQ'] : null;
switch ($frequency) {
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;
if (!empty($r['COUNT'])) {
// 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;
}
- if ($start Instanceof Horde_Date) {
- $start = $start->toDateTime();
+ $start = $this->toDateTime($start);
+
+ return $start;
+ }
+
+ private function toDateTime($date)
+ {
+ if ($date Instanceof Horde_Date) {
+ $date = $date->toDateTime();
}
- $start->_dateonly = $this->dateonly;
+ if ($date instanceof DateTimeInterface) {
+ $date = libcalendaring_datetime::createFromFormat(
+ 'Y-m-d\\TH:i:s',
+ $date->format('Y-m-d\\TH:i:s'),
+ $date->getTimezone()
+ );
+ }
- return $start;
+ $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 @@
+<?php
+
+/**
+ * libcalendaring_recurrence tests
+ *
+ * @author Aleksander Machniak <machniak@apheleia-it.ch>
+ *
+ * Copyright (C) 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 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
@@ -1,609 +1,609 @@
<?php
/**
* libcalendaring plugin's iCalendar functions tests
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 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 LibvcalendarTest extends PHPUnit\Framework\TestCase
+class VcalendarTest extends PHPUnit\Framework\TestCase
{
private $attachment_data;
function setUp(): void
{
require_once __DIR__ . '/../libcalendaring.php';
require_once __DIR__ . '/../lib/libcalendaring_vcalendar.php';
require_once __DIR__ . '/../lib/libcalendaring_datetime.php';
}
/**
* Simple iCal parsing test
*/
function test_import()
{
$ical = new libcalendaring_vcalendar();
$ics = file_get_contents(__DIR__ . '/resources/snd.ics');
$events = $ical->import($ics, 'UTF-8');
$this->assertEquals(1, count($events));
$event = $events[0];
$this->assertInstanceOf('DateTimeInterface', $event['created'], "'created' property is DateTime object");
$this->assertInstanceOf('DateTimeInterface', $event['changed'], "'changed' property is DateTime object");
$this->assertEquals('UTC', $event['created']->getTimezone()->getName(), "'created' date is in UTC");
$this->assertInstanceOf('DateTimeInterface', $event['start'], "'start' property is DateTime object");
$this->assertInstanceOf('DateTimeInterface', $event['end'], "'end' property is DateTime object");
$this->assertEquals('08-01', $event['start']->format('m-d'), "Start date is August 1st");
$this->assertTrue($event['allday'], "All-day event flag");
$this->assertEquals('B968B885-08FB-40E5-B89E-6DA05F26AA79', $event['uid'], "Event UID");
$this->assertEquals('Swiss National Day', $event['title'], "Event title");
$this->assertEquals('http://en.wikipedia.org/wiki/Swiss_National_Day', $event['url'], "URL property");
$this->assertEquals(2, $event['sequence'], "Sequence number");
$desclines = explode("\n", $event['description']);
$this->assertEquals(4, count($desclines), "Multiline description");
$this->assertEquals("French: Fête nationale Suisse", rtrim($desclines[1]), "UTF-8 encoding");
}
/**
* Test parsing from files
*/
function test_import_from_file()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
$this->assertEquals(2, count($events));
$events = $ical->import_from_file(__DIR__ . '/resources/invalid.txt', 'UTF-8');
$this->assertEmpty($events);
}
/**
* Test parsing from files with multiple VCALENDAR blocks (#2884)
*/
function test_import_from_file_multiple()
{
$ical = new libcalendaring_vcalendar();
$ical->fopen(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
$events = [];
foreach ($ical as $event) {
$events[] = $event;
}
$this->assertEquals(2, count($events));
$this->assertEquals("AAAA6A8C3CCE4EE2C1257B5C00FFFFFF-Lotus_Notes_Generated", $events[0]['uid']);
$this->assertEquals("AAAA1C572093EC3FC125799C004AFFFF-Lotus_Notes_Generated", $events[1]['uid']);
}
function test_invalid_dates()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/invalid-dates.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals(1, count($events), "Import event data");
$this->assertInstanceOf('DateTimeInterface', $event['created'], "Created date field");
$this->assertFalse(array_key_exists('changed', $event), "No changed date field");
}
/**
* Test some extended ical properties such as attendees, recurrence rules, alarms and attachments
*/
function test_extended()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals('REQUEST', $ical->method, "iTip method");
// attendees
$this->assertEquals(3, count($event['attendees']), "Attendees list (including organizer)");
$organizer = $event['attendees'][0];
$this->assertEquals('ORGANIZER', $organizer['role'], 'Organizer ROLE');
$this->assertEquals('Rolf Test', $organizer['name'], 'Organizer name');
$attendee = $event['attendees'][1];
$this->assertEquals('REQ-PARTICIPANT', $attendee['role'], 'Attendee ROLE');
$this->assertEquals('NEEDS-ACTION', $attendee['status'], 'Attendee STATUS');
$this->assertEquals('rolf2@mykolab.com', $attendee['email'], 'Attendee mailto:');
$this->assertEquals('carl@mykolab.com', $attendee['delegated-from'], 'Attendee delegated-from');
$this->assertTrue($attendee['rsvp'], 'Attendee RSVP');
$delegator = $event['attendees'][2];
$this->assertEquals('NON-PARTICIPANT', $delegator['role'], 'Delegator ROLE');
$this->assertEquals('DELEGATED', $delegator['status'], 'Delegator STATUS');
$this->assertEquals('INDIVIDUAL', $delegator['cutype'], 'Delegator CUTYPE');
$this->assertEquals('carl@mykolab.com', $delegator['email'], 'Delegator mailto:');
$this->assertEquals('rolf2@mykolab.com', $delegator['delegated-to'], 'Delegator delegated-to');
$this->assertFalse($delegator['rsvp'], 'Delegator RSVP');
// attachments
$this->assertEquals(1, count($event['attachments']), "Embedded attachments");
$attachment = $event['attachments'][0];
$this->assertEquals('text/html', $attachment['mimetype'], "Attachment mimetype attribute");
$this->assertEquals('calendar.html', $attachment['name'], "Attachment filename (X-LABEL) attribute");
$this->assertStringContainsString('<title>Kalender</title>', $attachment['data'], "Attachment content (decoded)");
// recurrence rules
$events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
$event = $events[0];
$this->assertTrue(is_array($event['recurrence']), 'Recurrences rule as hash array');
$rrule = $event['recurrence'];
$this->assertEquals('MONTHLY', $rrule['FREQ'], "Recurrence frequency");
$this->assertEquals('1', $rrule['INTERVAL'], "Recurrence interval");
$this->assertEquals('3WE', $rrule['BYDAY'], "Recurrence frequency");
$this->assertInstanceOf('DateTimeInterface', $rrule['UNTIL'], "Recurrence end date");
$this->assertEquals(2, count($rrule['EXDATE']), "Recurrence EXDATEs");
$this->assertInstanceOf('DateTimeInterface', $rrule['EXDATE'][0], "Recurrence EXDATE as DateTime");
$this->assertTrue(is_array($rrule['EXCEPTIONS']));
$this->assertEquals(1, count($rrule['EXCEPTIONS']), "Recurrence Exceptions");
$exception = $rrule['EXCEPTIONS'][0];
$this->assertEquals($event['uid'], $event['uid'], "Exception UID");
$this->assertEquals('Recurring Test (Exception)', $exception['title'], "Exception title");
$this->assertInstanceOf('DateTimeInterface', $exception['start'], "Exception start");
// categories, class
$this->assertEquals('libcalendaring tests', join(',', (array)$event['categories']), "Event categories");
// parse a recurrence chain instance
$events = $ical->import_from_file(__DIR__ . '/resources/recurrence-id.ics', 'UTF-8');
$this->assertEquals(1, count($events), "Fall back to Component::getComponents() when getBaseComponents() is empty");
$this->assertInstanceOf('DateTimeInterface', $events[0]['recurrence_date'], "Recurrence-ID as date");
$this->assertTrue($events[0]['thisandfuture'], "Range=THISANDFUTURE");
$this->assertEquals(count($events[0]['exceptions']), 1, "Second VEVENT as exception");
$this->assertEquals($events[0]['exceptions'][0]['uid'], $events[0]['uid'], "Exception UID match");
$this->assertEquals($events[0]['exceptions'][0]['sequence'], '2', "Exception sequence");
}
/**
*
*/
function test_alarms()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals('-12H:DISPLAY', $event['alarms'], "Serialized alarms string");
$alarm = libcalendaring::parse_alarm_value($event['alarms']);
$this->assertEquals('12', $alarm[0], "Alarm value");
$this->assertEquals('-H', $alarm[1], "Alarm unit");
$this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "Full alarm item (action)");
$this->assertEquals('-PT12H', $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
$this->assertEquals('END', $event['valarms'][0]['related'], "Full alarm item (related)");
// alarm trigger with 0 values
$events = $ical->import_from_file(__DIR__ . '/resources/alarms.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals('-30M:DISPLAY', $event['alarms'], "Stripped alarm string");
$alarm = libcalendaring::parse_alarm_value($event['alarms']);
$this->assertEquals('30', $alarm[0], "Alarm value");
$this->assertEquals('-M', $alarm[1], "Alarm unit");
$this->assertEquals('-30M', $alarm[2], "Alarm string");
$this->assertEquals('-PT30M', $alarm[3], "Unified alarm string (stripped zero-values)");
$this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "First alarm action");
$this->assertTrue(empty($event['valarms'][0]['related']), "First alarm related property");
$this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'], "First alarm text");
$this->assertEquals(3, count($event['valarms']), "List all VALARM blocks");
$valarm = $event['valarms'][1];
$this->assertEquals(1, count($valarm['attendees']), "Email alarm attendees");
$this->assertEquals('EMAIL', $valarm['action'], "Second alarm item (action)");
$this->assertEquals('-P1D', $valarm['trigger'], "Second alarm item (trigger)");
$this->assertEquals('This is the reminder message', $valarm['summary'], "Email alarm text");
$this->assertInstanceOf('DateTimeInterface', $event['valarms'][2]['trigger'], "Absolute trigger date/time");
// test alarms export
$ics = $ical->export([$event]);
$this->assertStringContainsString('ACTION:DISPLAY', $ics, "Display alarm block");
$this->assertStringContainsString('ACTION:EMAIL', $ics, "Email alarm block");
$this->assertStringContainsString('DESCRIPTION:This is the first event reminder', $ics, "Alarm description");
$this->assertStringContainsString('SUMMARY:This is the reminder message', $ics, "Email alarm summary");
$this->assertStringContainsString('ATTENDEE:mailto:reminder-recipient@example.org', $ics, "Email alarm recipient");
$this->assertStringContainsString('TRIGGER;VALUE=DATE-TIME:20130812', $ics, "Date-Time trigger");
}
/**
* @depends test_import_from_file
*/
function test_attachment()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/attachment.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals(2, count($events));
$this->assertEquals(1, count($event['attachments']));
$this->assertEquals('image/png', $event['attachments'][0]['mimetype']);
$this->assertEquals('500px-Opensource.svg.png', $event['attachments'][0]['name']);
}
/**
* @depends test_import
*/
function test_apple_alarms()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/apple-alarms.ics', 'UTF-8');
$event = $events[0];
// alarms
$this->assertEquals('-45M:AUDIO', $event['alarms'], "Relative alarm string");
$alarm = libcalendaring::parse_alarm_value($event['alarms']);
$this->assertEquals('45', $alarm[0], "Alarm value");
$this->assertEquals('-M', $alarm[1], "Alarm unit");
$this->assertEquals(1, count($event['valarms']), "Ignore invalid alarm blocks");
$this->assertEquals('AUDIO', $event['valarms'][0]['action'], "Full alarm item (action)");
$this->assertEquals('-PT45M', $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
$this->assertEquals('Basso', $event['valarms'][0]['uri'], "Full alarm item (attachment)");
}
/**
*
*/
function test_escaped_values()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/escaped.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals("House, Street, Zip Place", $event['location'], "Decode escaped commas in location value");
$this->assertEquals("Me, meets Them\nThem, meet Me", $event['description'], "Decode description value");
$this->assertEquals("Kolab, Thomas", $event['attendees'][3]['name'], "Unescaped");
$ics = $ical->export($events);
$this->assertStringContainsString('ATTENDEE;CN="Kolab, Thomas";PARTSTAT=', $ics, "Quoted attendee parameters");
}
/**
* Parse RDATE properties (#2885)
*/
function test_rdate()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
$event = $events[0];
$this->assertEquals(9, count($event['recurrence']['RDATE']));
$this->assertInstanceOf('DateTimeInterface', $event['recurrence']['RDATE'][0]);
$this->assertInstanceOf('DateTimeInterface', $event['recurrence']['RDATE'][1]);
}
/**
* @depends test_import
*/
function test_freebusy()
{
$ical = new libcalendaring_vcalendar();
$ical->import_from_file(__DIR__ . '/resources/freebusy.ifb', 'UTF-8');
$freebusy = $ical->freebusy;
$this->assertInstanceOf('DateTimeInterface', $freebusy['start'], "'start' property is DateTime object");
$this->assertInstanceOf('DateTimeInterface', $freebusy['end'], "'end' property is DateTime object");
$this->assertEquals(11, count($freebusy['periods']), "Number of freebusy periods defined");
$periods = $ical->get_busy_periods();
$this->assertEquals(9, count($periods), "Number of busy periods found");
$this->assertEquals('BUSY-TENTATIVE', $periods[8][2], "FBTYPE=BUSY-TENTATIVE");
}
/**
* @depends test_import
*/
function test_freebusy_dummy()
{
$ical = new libcalendaring_vcalendar();
$ical->import_from_file(__DIR__ . '/resources/dummy.ifb', 'UTF-8');
$freebusy = $ical->freebusy;
$this->assertEquals(0, count($freebusy['periods']), "Ignore 0-length freebudy periods");
$this->assertStringContainsString('dummy', $freebusy['comment'], "Parse comment");
}
function test_vtodo()
{
$ical = new libcalendaring_vcalendar();
$tasks = $ical->import_from_file(__DIR__ . '/resources/vtodo.ics', 'UTF-8', true);
$task = $tasks[0];
$this->assertInstanceOf('DateTimeInterface', $task['start'], "'start' property is DateTime object");
$this->assertInstanceOf('DateTimeInterface', $task['due'], "'due' property is DateTime object");
$this->assertEquals('-1D:DISPLAY', $task['alarms'], "Taks alarm value");
$this->assertEquals('IN-PROCESS', $task['status'], "Task status property");
$this->assertEquals(1, count($task['x-custom']), "Custom properties");
$this->assertEquals(4, count($task['categories']));
$this->assertEquals('1234567890-12345678-PARENT', $task['parent_id'], "Parent Relation");
$completed = $tasks[1];
$this->assertEquals('COMPLETED', $completed['status'], "Task status=completed when COMPLETED property is present");
$this->assertEquals(100, $completed['complete'], "Task percent complete value");
$ics = $ical->export([$completed]);
$this->assertMatchesRegularExpression('/COMPLETED(;VALUE=DATE-TIME)?:[0-9TZ]+/', $ics, "Export COMPLETED property");
}
/**
* Test for iCal export from internal hash array representation
*/
function test_export()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
$event = $events[0];
$events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
$event += $events[0];
$this->attachment_data = $event['attachments'][0]['data'];
unset($event['attachments'][0]['data']);
$event['attachments'][0]['id'] = '1';
$event['description'] = '*Exported by libcalendaring_vcalendar*';
$event['start']->setTimezone(new DateTimezone('America/Montreal'));
$event['end']->setTimezone(new DateTimezone('Europe/Berlin'));
$ics = $ical->export([$event], 'REQUEST', false, [$this, 'get_attachment_data'], true);
$this->assertStringContainsString('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
$this->assertStringContainsString('BEGIN:VTIMEZONE', $ics, "VTIMEZONE encapsulation BEGIN");
$this->assertStringContainsString('TZID:Europe/Berlin', $ics, "Timezone ID");
$this->assertStringContainsString('TZOFFSETFROM:+0100', $ics, "Timzone transition FROM");
$this->assertStringContainsString('TZOFFSETTO:+0200', $ics, "Timzone transition TO");
$this->assertStringContainsString('TZOFFSETFROM:-0400', $ics, "TZOFFSETFROM with negative offset (Bug T428)");
$this->assertStringContainsString('TZOFFSETTO:-0500', $ics, "TZOFFSETTO with negative offset (Bug T428)");
$this->assertStringContainsString('END:VTIMEZONE', $ics, "VTIMEZONE encapsulation END");
$this->assertStringContainsString('BEGIN:VEVENT', $ics, "VEVENT encapsulation BEGIN");
$this->assertSame(2, substr_count($ics, 'DTSTAMP'), "Duplicate DTSTAMP (T1148)");
$this->assertStringContainsString('UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0', $ics, "Event UID");
$this->assertStringContainsString('SEQUENCE:' . $event['sequence'], $ics, "Export Sequence number");
$this->assertStringContainsString('DESCRIPTION:*Exported by', $ics, "Export Description");
$this->assertStringContainsString('ORGANIZER;CN=Rolf Test:mailto:rolf@', $ics, "Export organizer");
$this->assertMatchesRegularExpression('/ATTENDEE.*;ROLE=REQ-PARTICIPANT/', $ics, "Export Attendee ROLE");
$this->assertMatchesRegularExpression('/ATTENDEE.*;PARTSTAT=NEEDS-ACTION/', $ics, "Export Attendee Status");
$this->assertMatchesRegularExpression('/ATTENDEE.*;RSVP=TRUE/', $ics, "Export Attendee RSVP");
$this->assertMatchesRegularExpression('/:mailto:rolf2@/', $ics, "Export Attendee mailto:");
$rrule = $event['recurrence'];
$this->assertMatchesRegularExpression('/RRULE:.*FREQ='.$rrule['FREQ'].'/', $ics, "Export Recurrence Frequence");
$this->assertMatchesRegularExpression('/RRULE:.*INTERVAL='.$rrule['INTERVAL'].'/', $ics, "Export Recurrence Interval");
$this->assertMatchesRegularExpression('/RRULE:.*UNTIL=20140718T215959Z/', $ics, "Export Recurrence End date");
$this->assertMatchesRegularExpression('/RRULE:.*BYDAY='.$rrule['BYDAY'].'/', $ics, "Export Recurrence BYDAY");
$this->assertMatchesRegularExpression('/EXDATE.*:20131218/', $ics, "Export Recurrence EXDATE");
$this->assertStringContainsString('BEGIN:VALARM', $ics, "Export VALARM");
$this->assertStringContainsString('TRIGGER;RELATED=END:-PT12H', $ics, "Export Alarm trigger");
$this->assertMatchesRegularExpression('/ATTACH.*;VALUE=BINARY/', $ics, "Embed attachment");
$this->assertMatchesRegularExpression('/ATTACH.*;ENCODING=BASE64/', $ics, "Attachment B64 encoding");
$this->assertMatchesRegularExpression('!ATTACH.*;FMTTYPE=text/html!', $ics, "Attachment mimetype");
$this->assertMatchesRegularExpression('!ATTACH.*;X-LABEL=calendar.html!', $ics, "Attachment filename with X-LABEL");
$this->assertStringContainsString('END:VEVENT', $ics, "VEVENT encapsulation END");
$this->assertStringContainsString('END:VCALENDAR', $ics, "VCALENDAR encapsulation END");
}
/**
* @depends test_extended
* @depends test_export
*/
function test_export_multiple()
{
$ical = new libcalendaring_vcalendar();
$events = array_merge(
$ical->import_from_file(__DIR__ . '/resources/snd.ics', 'UTF-8'),
$ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8')
);
$num = count($events);
$ics = $ical->export($events, null, false);
$this->assertStringContainsString('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
$this->assertStringContainsString('END:VCALENDAR', $ics, "VCALENDAR encapsulation END");
$this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
$this->assertEquals($num, substr_count($ics, 'END:VEVENT'), "VEVENT encapsulation END");
}
/**
* @depends test_export
*/
function test_export_recurrence_exceptions()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
// add exceptions
$event = $events[0];
unset($event['recurrence']['EXCEPTIONS']);
$exception1 = $event;
$exception1['start'] = clone $event['start'];
$exception1['start']->setDate(2013, 8, 14);
$exception1['end'] = clone $event['end'];
$exception1['end']->setDate(2013, 8, 14);
$exception2 = $event;
$exception2['start'] = clone $event['start'];
$exception2['start']->setDate(2013, 11, 13);
$exception2['end'] = clone $event['end'];
$exception2['end']->setDate(2013, 11, 13);
$exception2['title'] = 'Recurring Exception';
$events[0]['recurrence']['EXCEPTIONS'] = [$exception1, $exception2];
$ics = $ical->export($events, null, false);
$num = count($events[0]['recurrence']['EXCEPTIONS']) + 1;
$this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
$this->assertEquals($num, substr_count($ics, 'UID:'.$event['uid']), "Recurrence Exceptions with same UID");
$this->assertEquals($num, substr_count($ics, 'END:VEVENT'), "VEVENT encapsulation END");
$this->assertStringContainsString('RECURRENCE-ID;TZID=Europe/Zurich:20130814', $ics, "Recurrence-ID (1) being the exception date");
$this->assertStringContainsString('RECURRENCE-ID;TZID=Europe/Zurich:20131113', $ics, "Recurrence-ID (2) being the exception date");
$this->assertStringContainsString('SUMMARY:'.$exception2['title'], $ics, "Exception title");
}
function test_export_valid_rrules()
{
$event = [
'uid' => '1234567890',
'start' => new DateTime('now'),
'end' => new DateTime('now + 30min'),
'title' => 'test_export_valid_rrules',
'recurrence' => [
'FREQ' => 'DAILY',
'COUNT' => 5,
'EXDATE' => [],
'RDATE' => [],
],
];
$ical = new libcalendaring_vcalendar();
$ics = $ical->export([$event], null, false, null, false);
$this->assertStringNotContainsString('EXDATE=', $ics);
$this->assertStringNotContainsString('RDATE=', $ics);
}
/**
*
*/
function test_export_rdate()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
$ics = $ical->export($events, null, false);
$this->assertStringContainsString('RDATE:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values");
}
/**
* @depends test_export
*/
function test_export_direct()
{
$ical = new libcalendaring_vcalendar();
$events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
$num = count($events);
ob_start();
$return = $ical->export($events, null, true);
$output = ob_get_contents();
ob_end_clean();
$this->assertTrue($return, "Return true on successful writing");
$this->assertStringContainsString('BEGIN:VCALENDAR', $output, "VCALENDAR encapsulation BEGIN");
$this->assertStringContainsString('END:VCALENDAR', $output, "VCALENDAR encapsulation END");
$this->assertEquals($num, substr_count($output, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
$this->assertEquals($num, substr_count($output, 'END:VEVENT'), "VEVENT encapsulation END");
}
function test_datetime()
{
$ical = new libcalendaring_vcalendar();
$cal = new \Sabre\VObject\Component\VCalendar();
$localtime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')));
$localdate = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true);
$utctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC')));
$asutctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true);
$this->assertStringContainsString('TZID=Europe/Berlin', $localtime->serialize());
$this->assertStringContainsString('VALUE=DATE', $localdate->serialize());
$this->assertStringContainsString('20130901T120000Z', $utctime->serialize());
$this->assertStringContainsString('20130901T100000Z', $asutctime->serialize());
}
function test_get_vtimezone()
{
$vtz = libcalendaring_vcalendar::get_vtimezone('Europe/Berlin', strtotime('2014-08-22T15:00:00+02:00'));
$this->assertInstanceOf('\Sabre\VObject\Component', $vtz, "VTIMEZONE is a Component object");
$this->assertEquals('Europe/Berlin', $vtz->TZID);
$this->assertEquals('4', $vtz->{'X-MICROSOFT-CDO-TZID'});
// check for transition to daylight saving time which is BEFORE the given date
$dst = array_first($vtz->select('DAYLIGHT'));
$this->assertEquals('DAYLIGHT', $dst->name);
$this->assertEquals('20140330T010000', $dst->DTSTART);
$this->assertEquals('+0100', $dst->TZOFFSETFROM);
$this->assertEquals('+0200', $dst->TZOFFSETTO);
$this->assertEquals('CEST', $dst->TZNAME);
// check (last) transition to standard time which is AFTER the given date
$std = $vtz->select('STANDARD');
$std = end($std);
$this->assertEquals('STANDARD', $std->name);
$this->assertEquals('20141026T010000', $std->DTSTART);
$this->assertEquals('+0200', $std->TZOFFSETFROM);
$this->assertEquals('+0100', $std->TZOFFSETTO);
$this->assertEquals('CET', $std->TZNAME);
// unknown timezone
$vtz = libcalendaring_vcalendar::get_vtimezone('America/Foo Bar');
$this->assertEquals(false, $vtz);
// invalid input data
$vtz = libcalendaring_vcalendar::get_vtimezone(new DateTime());
$this->assertEquals(false, $vtz);
// DateTimezone as input data
$vtz = libcalendaring_vcalendar::get_vtimezone(new DateTimezone('Pacific/Chatham'));
$this->assertInstanceOf('\Sabre\VObject\Component', $vtz);
$this->assertStringContainsString('TZOFFSETFROM:+1245', $vtz->serialize());
$this->assertStringContainsString('TZOFFSETTO:+1345', $vtz->serialize());
// Making sure VTIMEZOONE contains at least one STANDARD/DAYLIGHT component
// when there's only one transition in specified time period (T5626)
$vtz = libcalendaring_vcalendar::get_vtimezone('Europe/Istanbul', strtotime('2019-10-04T15:00:00'));
$this->assertInstanceOf('\Sabre\VObject\Component', $vtz);
$dst = $vtz->select('DAYLIGHT');
$std = $vtz->select('STANDARD');
$this->assertEmpty($dst);
$this->assertCount(1, $std);
$std = end($std);
$this->assertEquals('STANDARD', $std->name);
$this->assertEquals('20181009T150000', $std->DTSTART);
$this->assertEquals('+0300', $std->TZOFFSETFROM);
$this->assertEquals('+0300', $std->TZOFFSETTO);
$this->assertEquals('+03', $std->TZNAME);
}
function get_attachment_data($id, $event)
{
return $this->attachment_data;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Feb 6, 9:14 AM (3 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
428194
Default Alt Text
(106 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment