1<?php 2 3/** 4 * Recurrence computation class for xcal-based Kolab format objects 5 * 6 * Utility class to compute instances of recurring events. 7 * It requires the libcalendaring PHP module to be installed and loaded. 8 * 9 * @version @package_version@ 10 * @author Thomas Bruederli <bruederli@kolabsys.com> 11 * 12 * Copyright (C) 2012-2016, Kolab Systems AG <contact@kolabsys.com> 13 * 14 * This program is free software: you can redistribute it and/or modify 15 * it under the terms of the GNU Affero General Public License as 16 * published by the Free Software Foundation, either version 3 of the 17 * License, or (at your option) any later version. 18 * 19 * This program is distributed in the hope that it will be useful, 20 * but WITHOUT ANY WARRANTY; without even the implied warranty of 21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 * GNU Affero General Public License for more details. 23 * 24 * You should have received a copy of the GNU Affero General Public License 25 * along with this program. If not, see <http://www.gnu.org/licenses/>. 26 */ 27class kolab_date_recurrence 28{ 29 private /* EventCal */ $engine; 30 private /* kolab_format_xcal */ $object; 31 private /* DateTime */ $start; 32 private /* DateTime */ $next; 33 private /* cDateTime */ $cnext; 34 private /* DateInterval */ $duration; 35 private /* bool */ $allday; 36 37 38 /** 39 * Default constructor 40 * 41 * @param kolab_format_xcal The Kolab object to operate on 42 */ 43 function __construct($object) 44 { 45 $data = $object->to_array(); 46 47 $this->object = $object; 48 $this->engine = $object->to_libcal(); 49 $this->start = $this->next = $data['start']; 50 $this->allday = !empty($data['allday']); 51 $this->cnext = kolab_format::get_datetime($this->next); 52 53 if (is_object($data['start']) && is_object($data['end'])) { 54 $this->duration = $data['start']->diff($data['end']); 55 } 56 else { 57 // Prevent from errors when end date is not set (#5307) RFC5545 3.6.1 58 $seconds = !empty($data['end']) ? ($data['end'] - $data['start']) : 0; 59 $this->duration = new DateInterval('PT' . $seconds . 'S'); 60 } 61 } 62 63 /** 64 * Get date/time of the next occurence of this event 65 * 66 * @param boolean Return a Unix timestamp instead of a DateTime object 67 * 68 * @return mixed DateTime object/unix timestamp or False if recurrence ended 69 */ 70 public function next_start($timestamp = false) 71 { 72 $time = false; 73 74 if ($this->engine && $this->next) { 75 if (($cnext = new cDateTime($this->engine->getNextOccurence($this->cnext))) && $cnext->isValid()) { 76 $next = kolab_format::php_datetime($cnext, $this->start->getTimezone()); 77 $time = $timestamp ? $next->format('U') : $next; 78 79 if ($this->allday) { 80 // it looks that for allday events the occurrence time 81 // is reset to 00:00:00, this is causing various issues 82 $next->setTime($this->start->format('G'), $this->start->format('i'), $this->start->format('s')); 83 $next->_dateonly = true; 84 } 85 86 $this->cnext = $cnext; 87 $this->next = $next; 88 } 89 } 90 91 return $time; 92 } 93 94 /** 95 * Get the next recurring instance of this event 96 * 97 * @return mixed Array with event properties or False if recurrence ended 98 */ 99 public function next_instance() 100 { 101 if ($next_start = $this->next_start()) { 102 $next_end = clone $next_start; 103 $next_end->add($this->duration); 104 105 $next = $this->object->to_array(); 106 $recurrence_id_format = libkolab::recurrence_id_format($next); 107 $next['start'] = $next_start; 108 $next['end'] = $next_end; 109 $next['recurrence_date'] = clone $next_start; 110 $next['_instance'] = $next_start->format($recurrence_id_format); 111 112 unset($next['_formatobj']); 113 114 return $next; 115 } 116 117 return false; 118 } 119 120 /** 121 * Get the end date of the occurence of this recurrence cycle 122 * 123 * @return DateTime|bool End datetime of the last event or False if recurrence exceeds limit 124 */ 125 public function end() 126 { 127 $event = $this->object->to_array(); 128 129 // recurrence end date is given 130 if ($event['recurrence']['UNTIL'] instanceof DateTime) { 131 return $event['recurrence']['UNTIL']; 132 } 133 134 // let libkolab do the work 135 if ($this->engine && ($cend = $this->engine->getLastOccurrence()) 136 && ($end_dt = kolab_format::php_datetime(new cDateTime($cend))) 137 ) { 138 return $end_dt; 139 } 140 141 // determine a reasonable end date if none given 142 if (!$event['recurrence']['COUNT'] && $event['end'] instanceof DateTime) { 143 $end_dt = clone $event['end']; 144 $end_dt->add(new DateInterval('P100Y')); 145 146 return $end_dt; 147 } 148 149 return false; 150 } 151 152 /** 153 * Find date/time of the first occurrence 154 */ 155 public function first_occurrence() 156 { 157 $event = $this->object->to_array(); 158 $start = clone $this->start; 159 $orig_start = clone $this->start; 160 $interval = intval($event['recurrence']['INTERVAL'] ?: 1); 161 162 switch ($event['recurrence']['FREQ']) { 163 case 'WEEKLY': 164 if (empty($event['recurrence']['BYDAY'])) { 165 return $orig_start; 166 } 167 168 $start->sub(new DateInterval("P{$interval}W")); 169 break; 170 171 case 'MONTHLY': 172 if (empty($event['recurrence']['BYDAY']) && empty($event['recurrence']['BYMONTHDAY'])) { 173 return $orig_start; 174 } 175 176 $start->sub(new DateInterval("P{$interval}M")); 177 break; 178 179 case 'YEARLY': 180 if (empty($event['recurrence']['BYDAY']) && empty($event['recurrence']['BYMONTH'])) { 181 return $orig_start; 182 } 183 184 $start->sub(new DateInterval("P{$interval}Y")); 185 break; 186 187 case 'DAILY': 188 if (!empty($event['recurrence']['BYMONTH'])) { 189 break; 190 } 191 192 default: 193 return $orig_start; 194 } 195 196 $event['start'] = $start; 197 $event['recurrence']['INTERVAL'] = $interval; 198 if ($event['recurrence']['COUNT']) { 199 // Increase count so we do not stop the loop to early 200 $event['recurrence']['COUNT'] += 100; 201 } 202 203 // Create recurrence that starts in the past 204 $object_type = $this->object instanceof kolab_format_task ? 'task' : 'event'; 205 $object = kolab_format::factory($object_type, 3.0); 206 $object->set($event); 207 $recurrence = new self($object); 208 209 $orig_date = $orig_start->format('Y-m-d'); 210 $found = false; 211 212 // find the first occurrence 213 while ($next = $recurrence->next_start()) { 214 $start = $next; 215 if ($next->format('Y-m-d') >= $orig_date) { 216 $found = true; 217 break; 218 } 219 } 220 221 if (!$found) { 222 rcube::raise_error(array( 223 'file' => __FILE__, 224 'line' => __LINE__, 225 'message' => sprintf("Failed to find a first occurrence. Start: %s, Recurrence: %s", 226 $orig_start->format(DateTime::ISO8601), json_encode($event['recurrence'])), 227 ), true); 228 229 return null; 230 } 231 232 if ($this->allday) { 233 $start->_dateonly = true; 234 } 235 236 return $start; 237 } 238} 239