Page MenuHomePhorge

No OneTemporary

Size
106 KB
Referenced Files
None
Subscribers
None
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

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)

Event Timeline