1 | <?php |
---|
2 | /** |
---|
3 | * Class for parsing RRule and getting us the dates |
---|
4 | * |
---|
5 | * @package awl |
---|
6 | * @subpackage caldav |
---|
7 | * @author Andrew McMillan <andrew@mcmillan.net.nz> |
---|
8 | * @copyright Morphoss Ltd |
---|
9 | * @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later |
---|
10 | */ |
---|
11 | |
---|
12 | if ( !class_exists('DateTime') ) return; |
---|
13 | |
---|
14 | $rrule_expand_limit = array( |
---|
15 | 'YEARLY' => array( 'bymonth' => 'expand', 'byweekno' => 'expand', 'byyearday' => 'expand', 'bymonthday' => 'expand', |
---|
16 | 'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' ), |
---|
17 | 'MONTHLY' => array( 'bymonth' => 'limit', 'bymonthday' => 'expand', |
---|
18 | 'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' ), |
---|
19 | 'WEEKLY' => array( 'bymonth' => 'limit', |
---|
20 | 'byday' => 'expand', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' ), |
---|
21 | 'DAILY' => array( 'bymonth' => 'limit', 'bymonthday' => 'limit', |
---|
22 | 'byday' => 'limit', 'byhour' => 'expand', 'byminute' => 'expand', 'bysecond' => 'expand' ), |
---|
23 | 'HOURLY' => array( 'bymonth' => 'limit', 'bymonthday' => 'limit', |
---|
24 | 'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'expand', 'bysecond' => 'expand' ), |
---|
25 | 'MINUTELY'=> array( 'bymonth' => 'limit', 'bymonthday' => 'limit', |
---|
26 | 'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'limit', 'bysecond' => 'expand' ), |
---|
27 | 'SECONDLY'=> array( 'bymonth' => 'limit', 'bymonthday' => 'limit', |
---|
28 | 'byday' => 'limit', 'byhour' => 'limit', 'byminute' => 'limit', 'bysecond' => 'limit' ), |
---|
29 | ); |
---|
30 | |
---|
31 | $rrule_day_numbers = array( 'SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6 ); |
---|
32 | |
---|
33 | $GLOBALS['debug_rrule'] = false; |
---|
34 | // $GLOBALS['debug_rrule'] = true; |
---|
35 | |
---|
36 | /** |
---|
37 | * Wrap the DateTimeZone class to allow parsing some iCalendar TZID strangenesses |
---|
38 | */ |
---|
39 | class RepeatRuleTimeZone extends DateTimeZone { |
---|
40 | private $tz_defined; |
---|
41 | |
---|
42 | public function __construct($dtz = null) { |
---|
43 | $this->tz_defined = false; |
---|
44 | if ( !isset($dtz) ) return; |
---|
45 | |
---|
46 | $dtz = olson_from_tzstring($dtz); |
---|
47 | |
---|
48 | try { |
---|
49 | parent::__construct($dtz); |
---|
50 | $this->tz_defined = $dtz; |
---|
51 | } |
---|
52 | catch (Exception $e) { |
---|
53 | $original = $dtz; |
---|
54 | |
---|
55 | if ( !isset($dtz) ) { |
---|
56 | dbg_error_log( 'ERROR', 'Could not parse timezone "%s" - will use floating time', $original ); |
---|
57 | $dtz = new DateTimeZone('UTC'); |
---|
58 | $this->tz_defined = false; |
---|
59 | } |
---|
60 | } |
---|
61 | } |
---|
62 | |
---|
63 | function tzid() { |
---|
64 | if ( $this->tz_defined === false ) return false; |
---|
65 | $tzid = $this->getName(); |
---|
66 | if ( $tzid != 'UTC' ) return $tzid; |
---|
67 | return $this->tz_defined; |
---|
68 | } |
---|
69 | } |
---|
70 | |
---|
71 | |
---|
72 | /** |
---|
73 | * Wrap the DateTime class to make it friendlier to passing in random strings from iCalendar |
---|
74 | * objects, and especially the random stuff used to identify timezones. We also add some |
---|
75 | * utility methods and stuff too, in order to simplify some of the operations we need to do |
---|
76 | * with dates. |
---|
77 | */ |
---|
78 | class RepeatRuleDateTime extends DateTime { |
---|
79 | // public static $Format = 'Y-m-d H:i:s'; |
---|
80 | public static $Format = 'c'; |
---|
81 | private $tzid; |
---|
82 | private $is_date; |
---|
83 | |
---|
84 | public function __construct($date = null, $dtz = null) { |
---|
85 | $this->is_date = false; |
---|
86 | if ( !isset($date) ) return; |
---|
87 | |
---|
88 | if ( preg_match('{;?VALUE=DATE[:;]}', $date, $matches) ) $this->is_date = true; |
---|
89 | elseif ( preg_match('{:([12]\d{3}) (0[1-9]|1[012]) (0[1-9]|[12]\d|3[01]Z?) $}x', $date, $matches) ) $this->is_date = true; |
---|
90 | if (preg_match('/;?TZID=([^:]+).*:(\d{8}(T\d{6})?)(Z)?/', $date, $matches) ) { |
---|
91 | if ( isset($matches[4]) && $matches[4] == 'Z' ) { |
---|
92 | $dtz = new RepeatRuleTimeZone('UTC'); |
---|
93 | $this->tzid = 'UTC'; |
---|
94 | } |
---|
95 | else if ( isset($matches[1]) && $matches[1] != '' ) { |
---|
96 | $dtz = new RepeatRuleTimeZone($matches[1]); |
---|
97 | $this->tzid = $dtz->tzid(); |
---|
98 | } |
---|
99 | else { |
---|
100 | $dtz = new RepeatRuleTimeZone('UTC'); |
---|
101 | $this->tzid = null; |
---|
102 | } |
---|
103 | } |
---|
104 | elseif( $dtz === null || $dtz == '' ) { |
---|
105 | $dtz = new RepeatRuleTimeZone('UTC'); |
---|
106 | if ( preg_match('/(\d{8}(T\d{6})?)Z/', $date, $matches) ) { |
---|
107 | if ( strlen($matches[1]) == 8 ) $this->is_date = true; |
---|
108 | $this->tzid = 'UTC'; |
---|
109 | } |
---|
110 | else { |
---|
111 | $this->tzid = null; |
---|
112 | } |
---|
113 | } |
---|
114 | elseif ( is_string($dtz) ) { |
---|
115 | $dtz = new RepeatRuleTimeZone($dtz); |
---|
116 | $this->tzid = $dtz->tzid(); |
---|
117 | } |
---|
118 | else { |
---|
119 | $this->tzid = $dtz->getName(); |
---|
120 | } |
---|
121 | |
---|
122 | parent::__construct($date, $dtz); |
---|
123 | |
---|
124 | return $this; |
---|
125 | } |
---|
126 | |
---|
127 | |
---|
128 | public function __toString() { |
---|
129 | return (string)parent::format(self::$Format) . ' ' . parent::getTimeZone()->getName(); |
---|
130 | } |
---|
131 | |
---|
132 | |
---|
133 | public function AsDate() { |
---|
134 | return $this->format('Ymd'); |
---|
135 | } |
---|
136 | |
---|
137 | |
---|
138 | public function modify( $interval ) { |
---|
139 | if ( preg_match('{^(-)?P((\d+)W)?((\d+)D)?T?((\d+)H)?((\d+)M)?((\d+)S)?$}', $interval, $matches) ) { |
---|
140 | $minus = $matches[1]; |
---|
141 | $interval = ''; |
---|
142 | if ( isset($matches[2]) && $matches[2] != '' ) $interval .= $minus . $matches[3] . ' weeks '; |
---|
143 | if ( isset($matches[4]) && $matches[4] != '' ) $interval .= $minus . $matches[5] . ' days '; |
---|
144 | if ( isset($matches[6]) && $matches[6] != '' ) $interval .= $minus . $matches[7] . ' hours '; |
---|
145 | if ( isset($matches[8]) && $matches[8] != '' ) $interval .= $minus . $matches[9] . ' minutes '; |
---|
146 | if (isset($matches[10]) &&$matches[10] != '' ) $interval .= $minus . $matches[11] . ' seconds '; |
---|
147 | } |
---|
148 | // printf( "Modify '%s' by: >>%s<<\n", $this->__toString(), $interval ); |
---|
149 | // print_r($this); |
---|
150 | if ( !isset($interval) || $interval == '' ) $interval = '1 day'; |
---|
151 | parent::modify($interval); |
---|
152 | return $this->__toString(); |
---|
153 | } |
---|
154 | |
---|
155 | |
---|
156 | public function UTC() { |
---|
157 | $gmt = clone($this); |
---|
158 | if ( isset($this->tzid) && $this->tzid != 'UTC' ) { |
---|
159 | $dtz = parent::getTimezone(); |
---|
160 | $offset = 0 - $dtz->getOffset($gmt); |
---|
161 | $gmt->modify( $offset . ' seconds' ); |
---|
162 | } |
---|
163 | if ( $this->is_date ) return $gmt->format('Ymd'); |
---|
164 | return $gmt->format('Ymd\THis\Z'); |
---|
165 | } |
---|
166 | |
---|
167 | |
---|
168 | public function RFC5545() { |
---|
169 | $result = ''; |
---|
170 | if ( isset($this->tzid) && $this->tzid != 'UTC' ) { |
---|
171 | $result = ';TZID='.$this->tzid; |
---|
172 | } |
---|
173 | if ( $this->is_date ) { |
---|
174 | $result .= ';VALUE=DATE:' . $this->format('Ymd'); |
---|
175 | } |
---|
176 | else { |
---|
177 | $result .= ':' . $this->format('Ymd\THis'); |
---|
178 | if ( isset($this->tzid) && $this->tzid == 'UTC' ) { |
---|
179 | $result .= 'Z'; |
---|
180 | } |
---|
181 | } |
---|
182 | return $result; |
---|
183 | } |
---|
184 | |
---|
185 | |
---|
186 | public function RFC5545Duration( $end_stamp ) { |
---|
187 | return sprintf( 'PT%dM', intval(($end_stamp->epoch() - $this->epoch()) / 60) ); |
---|
188 | } |
---|
189 | |
---|
190 | |
---|
191 | public function setTimeZone( $tz ) { |
---|
192 | if ( is_string($tz) ) { |
---|
193 | $tz = new RepeatRuleTimeZone($tz); |
---|
194 | $this->tzid = $tz->tzid(); |
---|
195 | } |
---|
196 | parent::setTimeZone( $tz ); |
---|
197 | return $this; |
---|
198 | } |
---|
199 | |
---|
200 | |
---|
201 | function setDate( $year=null, $month=null, $day=null ) { |
---|
202 | if ( !isset($year) ) $year = parent::format('Y'); |
---|
203 | if ( !isset($month) ) $month = parent::format('m'); |
---|
204 | if ( !isset($day) ) $day = parent::format('d'); |
---|
205 | parent::setDate( $year , $month , $day ); |
---|
206 | return $this; |
---|
207 | } |
---|
208 | |
---|
209 | function year() { |
---|
210 | return parent::format('Y'); |
---|
211 | } |
---|
212 | |
---|
213 | function month() { |
---|
214 | return parent::format('m'); |
---|
215 | } |
---|
216 | |
---|
217 | function day() { |
---|
218 | return parent::format('d'); |
---|
219 | } |
---|
220 | |
---|
221 | function hour() { |
---|
222 | return parent::format('H'); |
---|
223 | } |
---|
224 | |
---|
225 | function minute() { |
---|
226 | return parent::format('i'); |
---|
227 | } |
---|
228 | |
---|
229 | function second() { |
---|
230 | return parent::format('s'); |
---|
231 | } |
---|
232 | |
---|
233 | function epoch() { |
---|
234 | return parent::format('U'); |
---|
235 | } |
---|
236 | } |
---|
237 | |
---|
238 | |
---|
239 | class RepeatRule { |
---|
240 | |
---|
241 | private $base; |
---|
242 | private $until; |
---|
243 | private $freq; |
---|
244 | private $count; |
---|
245 | private $interval; |
---|
246 | private $bysecond; |
---|
247 | private $byminute; |
---|
248 | private $byhour; |
---|
249 | private $bymonthday; |
---|
250 | private $byyearday; |
---|
251 | private $byweekno; |
---|
252 | private $byday; |
---|
253 | private $bymonth; |
---|
254 | private $bysetpos; |
---|
255 | private $wkst; |
---|
256 | |
---|
257 | private $instances; |
---|
258 | private $position; |
---|
259 | private $finished; |
---|
260 | private $current_base; |
---|
261 | |
---|
262 | |
---|
263 | public function __construct( $basedate, $rrule ) { |
---|
264 | $this->base = ( is_object($basedate) ? $basedate : new RepeatRuleDateTime($basedate) ); |
---|
265 | |
---|
266 | if ( $GLOBALS['debug_rrule'] ) { |
---|
267 | printf( "Constructing RRULE based on: '%s', rrule: '%s'\n", $basedate, $rrule ); |
---|
268 | } |
---|
269 | |
---|
270 | if ( preg_match('{FREQ=([A-Z]+)(;|$)}', $rrule, $m) ) $this->freq = $m[1]; |
---|
271 | if ( preg_match('{UNTIL=([0-9TZ]+)(;|$)}', $rrule, $m) ) $this->until = new RepeatRuleDateTime($m[1]); |
---|
272 | if ( preg_match('{COUNT=([0-9]+)(;|$)}', $rrule, $m) ) $this->count = $m[1]; |
---|
273 | if ( preg_match('{INTERVAL=([0-9]+)(;|$)}', $rrule, $m) ) $this->interval = $m[1]; |
---|
274 | if ( preg_match('{WKST=(MO|TU|WE|TH|FR|SA|SU)(;|$)}', $rrule, $m) ) $this->wkst = $m[1]; |
---|
275 | |
---|
276 | if ( preg_match('{BYDAY=(([+-]?[0-9]{0,2}(MO|TU|WE|TH|FR|SA|SU),?)+)(;|$)}', $rrule, $m) ) $this->byday = explode(',',$m[1]); |
---|
277 | |
---|
278 | if ( preg_match('{BYYEARDAY=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->byyearday = explode(',',$m[1]); |
---|
279 | if ( preg_match('{BYWEEKNO=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->byweekno = explode(',',$m[1]); |
---|
280 | if ( preg_match('{BYMONTHDAY=([0-9,+-]+)(;|$)}', $rrule, $m) ) $this->bymonthday = explode(',',$m[1]); |
---|
281 | if ( preg_match('{BYMONTH=(([+-]?[0-1]?[0-9],?)+)(;|$)}', $rrule, $m) ) $this->bymonth = explode(',',$m[1]); |
---|
282 | if ( preg_match('{BYSETPOS=(([+-]?[0-9]{1,3},?)+)(;|$)}', $rrule, $m) ) $this->bysetpos = explode(',',$m[1]); |
---|
283 | |
---|
284 | if ( preg_match('{BYSECOND=([0-9,]+)(;|$)}', $rrule, $m) ) $this->bysecond = explode(',',$m[1]); |
---|
285 | if ( preg_match('{BYMINUTE=([0-9,]+)(;|$)}', $rrule, $m) ) $this->byminute = explode(',',$m[1]); |
---|
286 | if ( preg_match('{BYHOUR=([0-9,]+)(;|$)}', $rrule, $m) ) $this->byhour = explode(',',$m[1]); |
---|
287 | |
---|
288 | if ( !isset($this->interval) ) $this->interval = 1; |
---|
289 | switch( $this->freq ) { |
---|
290 | case 'SECONDLY': $this->freq_name = 'second'; break; |
---|
291 | case 'MINUTELY': $this->freq_name = 'minute'; break; |
---|
292 | case 'HOURLY': $this->freq_name = 'hour'; break; |
---|
293 | case 'DAILY': $this->freq_name = 'day'; break; |
---|
294 | case 'WEEKLY': $this->freq_name = 'week'; break; |
---|
295 | case 'MONTHLY': $this->freq_name = 'month'; break; |
---|
296 | case 'YEARLY': $this->freq_name = 'year'; break; |
---|
297 | default: |
---|
298 | /** need to handle the error, but FREQ is mandatory so unlikely */ |
---|
299 | } |
---|
300 | $this->frequency_string = sprintf('+%d %s', $this->interval, $this->freq_name ); |
---|
301 | if ( $GLOBALS['debug_rrule'] ) printf( "Frequency modify string is: '%s', base is: '%s'\n", $this->frequency_string, $this->base->format('c') ); |
---|
302 | $this->Start(); |
---|
303 | } |
---|
304 | |
---|
305 | |
---|
306 | public function set_timezone( $tzstring ) { |
---|
307 | $this->base->setTimezone(new DateTimeZone($tzstring)); |
---|
308 | } |
---|
309 | |
---|
310 | |
---|
311 | public function Start() { |
---|
312 | $this->instances = array(); |
---|
313 | $this->GetMoreInstances(); |
---|
314 | $this->rewind(); |
---|
315 | $this->finished = false; |
---|
316 | } |
---|
317 | |
---|
318 | |
---|
319 | public function rewind() { |
---|
320 | $this->position = -1; |
---|
321 | } |
---|
322 | |
---|
323 | |
---|
324 | public function next() { |
---|
325 | $this->position++; |
---|
326 | return $this->current(); |
---|
327 | } |
---|
328 | |
---|
329 | |
---|
330 | public function current() { |
---|
331 | if ( !$this->valid() ) return null; |
---|
332 | if ( !isset($this->instances[$this->position]) ) $this->GetMoreInstances(); |
---|
333 | if ( !$this->valid() ) return null; |
---|
334 | if ( $GLOBALS['debug_rrule'] ) printf( "Returning date from position %d: %s (%s)\n", $this->position, $this->instances[$this->position]->format('c'), $this->instances[$this->position]->UTC() ); |
---|
335 | return $this->instances[$this->position]; |
---|
336 | } |
---|
337 | |
---|
338 | |
---|
339 | public function key() { |
---|
340 | if ( !$this->valid() ) return null; |
---|
341 | if ( !isset($this->instances[$this->position]) ) $this->GetMoreInstances(); |
---|
342 | if ( !isset($this->keys[$this->position]) ) { |
---|
343 | $this->keys[$this->position] = $this->instances[$this->position]; |
---|
344 | } |
---|
345 | return $this->keys[$this->position]; |
---|
346 | } |
---|
347 | |
---|
348 | |
---|
349 | public function valid() { |
---|
350 | if ( isset($this->instances[$this->position]) || !$this->finished ) return true; |
---|
351 | return false; |
---|
352 | } |
---|
353 | |
---|
354 | |
---|
355 | private function GetMoreInstances() { |
---|
356 | global $rrule_expand_limit; |
---|
357 | |
---|
358 | if ( $this->finished ) return; |
---|
359 | $got_more = false; |
---|
360 | $loop_limit = 10; |
---|
361 | $loops = 0; |
---|
362 | while( !$this->finished && !$got_more && $loops++ < $loop_limit ) { |
---|
363 | if ( !isset($this->current_base) ) { |
---|
364 | $this->current_base = clone($this->base); |
---|
365 | } |
---|
366 | else { |
---|
367 | $this->current_base->modify( $this->frequency_string ); |
---|
368 | } |
---|
369 | if ( $GLOBALS['debug_rrule'] ) printf( "Getting more instances from: '%s' - %d\n", $this->current_base->format('c'), count($this->instances) ); |
---|
370 | $this->current_set = array( clone($this->current_base) ); |
---|
371 | foreach( $rrule_expand_limit[$this->freq] AS $bytype => $action ) { |
---|
372 | if ( isset($this->{$bytype}) ) $this->{$action.'_'.$bytype}(); |
---|
373 | if ( !isset($this->current_set[0]) ) break; |
---|
374 | } |
---|
375 | sort($this->current_set); |
---|
376 | if ( isset($this->bysetpos) ) $this->limit_bysetpos(); |
---|
377 | |
---|
378 | $position = count($this->instances) - 1; |
---|
379 | if ( $GLOBALS['debug_rrule'] ) printf( "Inserting %d from current_set into position %d\n", count($this->current_set), $position + 1 ); |
---|
380 | foreach( $this->current_set AS $k => $instance ) { |
---|
381 | if ( $instance < $this->base ) continue; |
---|
382 | if ( isset($this->until) && $instance > $this->until ) { |
---|
383 | $this->finished = true; |
---|
384 | return; |
---|
385 | } |
---|
386 | if ( !isset($this->instances[$position]) || $instance != $this->instances[$position] ) { |
---|
387 | $got_more = true; |
---|
388 | $position++; |
---|
389 | $this->instances[$position] = $instance; |
---|
390 | if ( $GLOBALS['debug_rrule'] ) printf( "Added date %s into position %d in current set\n", $instance->format('c'), $position ); |
---|
391 | if ( isset($this->count) && ($position + 1) >= $this->count ) $this->finished = true; |
---|
392 | } |
---|
393 | } |
---|
394 | } |
---|
395 | } |
---|
396 | |
---|
397 | |
---|
398 | static public function date_mask( $date, $y, $mo, $d, $h, $mi, $s ) { |
---|
399 | $date_parts = explode(',',$date->format('Y,m,d,H,i,s')); |
---|
400 | |
---|
401 | $tz = $date->getTimezone(); |
---|
402 | if ( isset($y) || isset($mo) || isset($d) ) { |
---|
403 | if ( isset($y) ) $date_parts[0] = $y; |
---|
404 | if ( isset($mo) ) $date_parts[1] = $mo; |
---|
405 | if ( isset($d) ) $date_parts[2] = $d; |
---|
406 | $date->setDate( $date_parts[0], $date_parts[1], $date_parts[2] ); |
---|
407 | } |
---|
408 | if ( isset($h) || isset($mi) || isset($s) ) { |
---|
409 | if ( isset($h) ) $date_parts[3] = $h; |
---|
410 | if ( isset($mi) ) $date_parts[4] = $mi; |
---|
411 | if ( isset($s) ) $date_parts[5] = $s; |
---|
412 | $date->setTime( $date_parts[3], $date_parts[4], $date_parts[5] ); |
---|
413 | } |
---|
414 | return $date; |
---|
415 | } |
---|
416 | |
---|
417 | |
---|
418 | private function expand_bymonth() { |
---|
419 | $instances = $this->current_set; |
---|
420 | $this->current_set = array(); |
---|
421 | foreach( $instances AS $k => $instance ) { |
---|
422 | foreach( $this->bymonth AS $k => $month ) { |
---|
423 | $expanded = $this->date_mask( clone($instance), null, $month, null, null, null, null); |
---|
424 | if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYMONTH $month into date %s\n", $expanded->format('c') ); |
---|
425 | $this->current_set[] = $expanded; |
---|
426 | } |
---|
427 | } |
---|
428 | } |
---|
429 | |
---|
430 | private function expand_bymonthday() { |
---|
431 | $instances = $this->current_set; |
---|
432 | $this->current_set = array(); |
---|
433 | foreach( $instances AS $k => $instance ) { |
---|
434 | foreach( $this->bymonthday AS $k => $monthday ) { |
---|
435 | $this->current_set[] = $this->date_mask( clone($instance), null, null, $monthday, null, null, null); |
---|
436 | } |
---|
437 | } |
---|
438 | } |
---|
439 | |
---|
440 | |
---|
441 | private function expand_byday_in_week( $day_in_week ) { |
---|
442 | global $rrule_day_numbers; |
---|
443 | |
---|
444 | /** |
---|
445 | * @TODO: This should really allow for WKST, since if we start a series |
---|
446 | * on (eg.) TH and interval > 1, a MO, TU, FR repeat will not be in the |
---|
447 | * same week with this code. |
---|
448 | */ |
---|
449 | $dow_of_instance = $day_in_week->format('w'); // 0 == Sunday |
---|
450 | foreach( $this->byday AS $k => $weekday ) { |
---|
451 | $dow = $rrule_day_numbers[$weekday]; |
---|
452 | $offset = $dow - $dow_of_instance; |
---|
453 | if ( $offset < 0 ) $offset += 7; |
---|
454 | $expanded = clone($day_in_week); |
---|
455 | $expanded->modify( sprintf('+%d day', $offset) ); |
---|
456 | $this->current_set[] = $expanded; |
---|
457 | if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(W) $weekday into date %s\n", $expanded->format('c') ); |
---|
458 | } |
---|
459 | } |
---|
460 | |
---|
461 | |
---|
462 | private function expand_byday_in_month( $day_in_month ) { |
---|
463 | global $rrule_day_numbers; |
---|
464 | |
---|
465 | $first_of_month = $this->date_mask( clone($day_in_month), null, null, 1, null, null, null); |
---|
466 | $dow_of_first = $first_of_month->format('w'); // 0 == Sunday |
---|
467 | $days_in_month = cal_days_in_month(CAL_GREGORIAN, $first_of_month->format('m'), $first_of_month->format('Y')); |
---|
468 | foreach( $this->byday AS $k => $weekday ) { |
---|
469 | if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) { |
---|
470 | $dow = $rrule_day_numbers[$matches[3]]; |
---|
471 | $first_dom = 1 + $dow - $dow_of_first; if ( $first_dom < 1 ) $first_dom +=7; // e.g. 1st=WE, dow=MO => 1+1-3=-1 => MO is 6th, etc. |
---|
472 | $whichweek = intval($matches[2]); |
---|
473 | if ( $GLOBALS['debug_rrule'] ) printf( "Expanding BYDAY(M) $weekday in month of %s\n", $instance->format('c') ); |
---|
474 | if ( $whichweek > 0 ) { |
---|
475 | $whichweek--; |
---|
476 | $monthday = $first_dom; |
---|
477 | if ( $matches[1] == '-' ) { |
---|
478 | $monthday += 35; |
---|
479 | while( $monthday > $days_in_month ) $monthday -= 7; |
---|
480 | $monthday -= (7 * $whichweek); |
---|
481 | } |
---|
482 | else { |
---|
483 | $monthday += (7 * $whichweek); |
---|
484 | } |
---|
485 | if ( $monthday > 0 && $monthday <= $days_in_month ) { |
---|
486 | $expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null); |
---|
487 | if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(M) $weekday now $monthday into date %s\n", $expanded->format('c') ); |
---|
488 | $this->current_set[] = $expanded; |
---|
489 | } |
---|
490 | } |
---|
491 | else { |
---|
492 | for( $monthday = $first_dom; $monthday <= $days_in_month; $monthday += 7 ) { |
---|
493 | $expanded = $this->date_mask( clone($day_in_month), null, null, $monthday, null, null, null); |
---|
494 | if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(M) $weekday now $monthday into date %s\n", $expanded->format('c') ); |
---|
495 | $this->current_set[] = $expanded; |
---|
496 | } |
---|
497 | } |
---|
498 | } |
---|
499 | } |
---|
500 | } |
---|
501 | |
---|
502 | |
---|
503 | private function expand_byday_in_year( $day_in_year ) { |
---|
504 | global $rrule_day_numbers; |
---|
505 | |
---|
506 | $first_of_year = $this->date_mask( clone($day_in_year), null, 1, 1, null, null, null); |
---|
507 | $dow_of_first = $first_of_year->format('w'); // 0 == Sunday |
---|
508 | $days_in_year = 337 + cal_days_in_month(CAL_GREGORIAN, 2, $first_of_year->format('Y')); |
---|
509 | foreach( $this->byday AS $k => $weekday ) { |
---|
510 | if ( preg_match('{([+-])?(\d)?(MO|TU|WE|TH|FR|SA|SU)}', $weekday, $matches ) ) { |
---|
511 | $expanded = clone($first_of_year); |
---|
512 | $dow = $rrule_day_numbers[$matches[3]]; |
---|
513 | $first_doy = 1 + $dow - $dow_of_first; if ( $first_doy < 1 ) $first_doy +=7; // e.g. 1st=WE, dow=MO => 1+1-3=-1 => MO is 6th, etc. |
---|
514 | $whichweek = intval($matches[2]); |
---|
515 | if ( $GLOBALS['debug_rrule'] ) printf( "Expanding BYDAY(Y) $weekday from date %s\n", $instance->format('c') ); |
---|
516 | if ( $whichweek > 0 ) { |
---|
517 | $whichweek--; |
---|
518 | $yearday = $first_doy; |
---|
519 | if ( $matches[1] == '-' ) { |
---|
520 | $yearday += 371; |
---|
521 | while( $yearday > $days_in_year ) $yearday -= 7; |
---|
522 | $yearday -= (7 * $whichweek); |
---|
523 | } |
---|
524 | else { |
---|
525 | $yearday += (7 * $whichweek); |
---|
526 | } |
---|
527 | if ( $yearday > 0 && $yearday <= $days_in_year ) { |
---|
528 | $expanded->modify(sprintf('+%d day', $yearday - 1)); |
---|
529 | if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(Y) $weekday now $yearday into date %s\n", $expanded->format('c') ); |
---|
530 | $this->current_set[] = $expanded; |
---|
531 | } |
---|
532 | } |
---|
533 | else { |
---|
534 | $expanded->modify(sprintf('+%d day', $first_doy - 1)); |
---|
535 | for( $yearday = $first_doy; $yearday <= $days_in_year; $yearday += 7 ) { |
---|
536 | if ( $GLOBALS['debug_rrule'] ) printf( "Expanded BYDAY(Y) $weekday now $yearday into date %s\n", $expanded->format('c') ); |
---|
537 | $this->current_set[] = clone($expanded); |
---|
538 | $expanded->modify('+1 week'); |
---|
539 | } |
---|
540 | } |
---|
541 | } |
---|
542 | } |
---|
543 | } |
---|
544 | |
---|
545 | |
---|
546 | private function expand_byday() { |
---|
547 | if ( !isset($this->current_set[0]) ) return; |
---|
548 | if ( $this->freq == 'MONTHLY' || $this->freq == 'YEARLY' ) { |
---|
549 | if ( isset($this->bymonthday) || isset($this->byyearday) ) { |
---|
550 | $this->limit_byday(); /** Per RFC5545 3.3.10 from note 1&2 to table */ |
---|
551 | return; |
---|
552 | } |
---|
553 | } |
---|
554 | $instances = $this->current_set; |
---|
555 | $this->current_set = array(); |
---|
556 | foreach( $instances AS $k => $instance ) { |
---|
557 | if ( $this->freq == 'MONTHLY' ) { |
---|
558 | $this->expand_byday_in_month($instance); |
---|
559 | } |
---|
560 | else if ( $this->freq == 'WEEKLY' ) { |
---|
561 | $this->expand_byday_in_week($instance); |
---|
562 | } |
---|
563 | else { // YEARLY |
---|
564 | if ( isset($this->bymonth) ) { |
---|
565 | $this->expand_byday_in_month($instance); |
---|
566 | } |
---|
567 | else if ( isset($this->byweekno) ) { |
---|
568 | $this->expand_byday_in_week($instance); |
---|
569 | } |
---|
570 | else { |
---|
571 | $this->expand_byday_in_year($instance); |
---|
572 | } |
---|
573 | } |
---|
574 | |
---|
575 | } |
---|
576 | } |
---|
577 | |
---|
578 | private function expand_byhour() { |
---|
579 | $instances = $this->current_set; |
---|
580 | $this->current_set = array(); |
---|
581 | foreach( $instances AS $k => $instance ) { |
---|
582 | foreach( $this->bymonth AS $k => $month ) { |
---|
583 | $this->current_set[] = $this->date_mask( clone($instance), null, null, null, $hour, null, null); |
---|
584 | } |
---|
585 | } |
---|
586 | } |
---|
587 | |
---|
588 | private function expand_byminute() { |
---|
589 | $instances = $this->current_set; |
---|
590 | $this->current_set = array(); |
---|
591 | foreach( $instances AS $k => $instance ) { |
---|
592 | foreach( $this->bymonth AS $k => $month ) { |
---|
593 | $this->current_set[] = $this->date_mask( clone($instance), null, null, null, null, $minute, null); |
---|
594 | } |
---|
595 | } |
---|
596 | } |
---|
597 | |
---|
598 | private function expand_bysecond() { |
---|
599 | $instances = $this->current_set; |
---|
600 | $this->current_set = array(); |
---|
601 | foreach( $instances AS $k => $instance ) { |
---|
602 | foreach( $this->bymonth AS $k => $second ) { |
---|
603 | $this->current_set[] = $this->date_mask( clone($instance), null, null, null, null, null, $second); |
---|
604 | } |
---|
605 | } |
---|
606 | } |
---|
607 | |
---|
608 | |
---|
609 | private function limit_generally( $fmt_char, $element_name ) { |
---|
610 | $instances = $this->current_set; |
---|
611 | $this->current_set = array(); |
---|
612 | foreach( $instances AS $k => $instance ) { |
---|
613 | foreach( $this->{$element_name} AS $k => $element_value ) { |
---|
614 | if ( $GLOBALS['debug_rrule'] ) printf( "Limiting '$fmt_char' on '%s' => '%s' ?=? '%s' ? %s\n", $instance->format('c'), $instance->format($fmt_char), $element_value, ($instance->format($fmt_char) == $element_value ? 'Yes' : 'No') ); |
---|
615 | if ( $instance->format($fmt_char) == $element_value ) $this->current_set[] = $instance; |
---|
616 | } |
---|
617 | } |
---|
618 | } |
---|
619 | |
---|
620 | private function limit_byday() { |
---|
621 | global $rrule_day_numbers; |
---|
622 | |
---|
623 | $fmt_char = 'w'; |
---|
624 | $instances = $this->current_set; |
---|
625 | $this->current_set = array(); |
---|
626 | foreach( $this->byday AS $k => $weekday ) { |
---|
627 | $dow = $rrule_day_numbers[$weekday]; |
---|
628 | foreach( $instances AS $k => $instance ) { |
---|
629 | if ( $GLOBALS['debug_rrule'] ) printf( "Limiting '$fmt_char' on '%s' => '%s' ?=? '%s' (%d) ? %s\n", $instance->format('c'), $instance->format($fmt_char), $weekday, $dow, ($instance->format($fmt_char) == $dow ? 'Yes' : 'No') ); |
---|
630 | if ( $instance->format($fmt_char) == $dow ) $this->current_set[] = $instance; |
---|
631 | } |
---|
632 | } |
---|
633 | } |
---|
634 | |
---|
635 | private function limit_bymonth() { $this->limit_generally( 'm', 'bymonth' ); } |
---|
636 | private function limit_byyearday() { $this->limit_generally( 'z', 'byyearday' ); } |
---|
637 | private function limit_bymonthday() { $this->limit_generally( 'd', 'bymonthday' ); } |
---|
638 | private function limit_byhour() { $this->limit_generally( 'H', 'byhour' ); } |
---|
639 | private function limit_byminute() { $this->limit_generally( 'i', 'byminute' ); } |
---|
640 | private function limit_bysecond() { $this->limit_generally( 's', 'bysecond' ); } |
---|
641 | |
---|
642 | |
---|
643 | private function limit_bysetpos( ) { |
---|
644 | $instances = $this->current_set; |
---|
645 | $count = count($instances); |
---|
646 | $this->current_set = array(); |
---|
647 | foreach( $this->bysetpos AS $k => $element_value ) { |
---|
648 | if ( $GLOBALS['debug_rrule'] ) printf( "Limiting bysetpos %s of %d instances\n", $element_value, $count ); |
---|
649 | if ( $element_value > 0 ) { |
---|
650 | $this->current_set[] = $instances[$element_value - 1]; |
---|
651 | } |
---|
652 | else if ( $element_value < 0 ) { |
---|
653 | $this->current_set[] = $instances[$count + $element_value]; |
---|
654 | } |
---|
655 | } |
---|
656 | } |
---|
657 | |
---|
658 | |
---|
659 | } |
---|
660 | |
---|
661 | |
---|
662 | require_once("vComponent.php"); |
---|
663 | |
---|
664 | /** |
---|
665 | * Expand the event instances for an RDATE or EXDATE property |
---|
666 | * |
---|
667 | * @param string $property RDATE or EXDATE, depending... |
---|
668 | * @param array $component A vComponent which is a VEVENT, VTODO or VJOURNAL |
---|
669 | * @param array $range_end A date after which we care less about expansion |
---|
670 | * |
---|
671 | * @return array An array keyed on the UTC dates, referring to the component |
---|
672 | */ |
---|
673 | function rdate_expand( $dtstart, $property, $component, $range_end = null ) { |
---|
674 | $properties = $component->GetProperties($property); |
---|
675 | $expansion = array(); |
---|
676 | foreach( $properties AS $p ) { |
---|
677 | $timezone = $p->GetParameterValue('TZID'); |
---|
678 | $rdate = $p->Value(); |
---|
679 | $rdates = explode( ',', $rdate ); |
---|
680 | foreach( $rdates AS $k => $v ) { |
---|
681 | $rdate = new RepeatRuleDateTime( $v, $timezone); |
---|
682 | $expansion[$rdate->UTC()] = $component; |
---|
683 | if ( $rdate > $range_end ) break; |
---|
684 | } |
---|
685 | } |
---|
686 | return $expansion; |
---|
687 | } |
---|
688 | |
---|
689 | |
---|
690 | /** |
---|
691 | * Expand the event instances for an RRULE property |
---|
692 | * |
---|
693 | * @param object $dtstart A RepeatRuleDateTime which is the master dtstart |
---|
694 | * @param string $property RDATE or EXDATE, depending... |
---|
695 | * @param array $component A vComponent which is a VEVENT, VTODO or VJOURNAL |
---|
696 | * @param array $range_end A date after which we care less about expansion |
---|
697 | * |
---|
698 | * @return array An array keyed on the UTC dates, referring to the component |
---|
699 | */ |
---|
700 | function rrule_expand( $dtstart, $property, $component, $range_end ) { |
---|
701 | $expansion = array(); |
---|
702 | |
---|
703 | $recur = $component->GetProperty($property); |
---|
704 | if ( !isset($recur) ) return $expansion; |
---|
705 | $recur = $recur->Value(); |
---|
706 | |
---|
707 | $this_start = $component->GetProperty('DTSTART'); |
---|
708 | if ( isset($this_start) ) { |
---|
709 | $timezone = $this_start->GetParameterValue('TZID'); |
---|
710 | $this_start = new RepeatRuleDateTime($this_start->Value(),$timezone); |
---|
711 | } |
---|
712 | else { |
---|
713 | $this_start = clone($dtstart); |
---|
714 | } |
---|
715 | |
---|
716 | // print_r( $this_start ); |
---|
717 | // printf( "RRULE: %s\n", $recur ); |
---|
718 | $rule = new RepeatRule( $this_start, $recur ); |
---|
719 | $i = 0; |
---|
720 | $result_limit = 1000; |
---|
721 | while( $date = $rule->next() ) { |
---|
722 | // printf( "[%3d] %s\n", $i, $date->UTC() ); |
---|
723 | $expansion[$date->UTC()] = $component; |
---|
724 | if ( $i++ >= $result_limit || $date > $range_end ) break; |
---|
725 | } |
---|
726 | // print_r( $expansion ); |
---|
727 | return $expansion; |
---|
728 | } |
---|
729 | |
---|
730 | |
---|
731 | /** |
---|
732 | * Expand the event instances for an iCalendar VEVENT (or VTODO) |
---|
733 | * |
---|
734 | * @param object $vResource A vComponent which is a VCALENDAR containing components needing expansion |
---|
735 | * @param object $range_start A RepeatRuleDateTime which is the beginning of the range for events, default -6 weeks |
---|
736 | * @param object $range_end A RepeatRuleDateTime which is the end of the range for events, default +6 weeks |
---|
737 | * |
---|
738 | * @return vComponent The original vComponent, with the instances of the internal components expanded. |
---|
739 | */ |
---|
740 | function expand_event_instances( $vResource, $range_start = null, $range_end = null ) { |
---|
741 | $components = $vResource->GetComponents(); |
---|
742 | |
---|
743 | if ( !isset($range_start) ) { $range_start = new RepeatRuleDateTime(); $range_start->modify('-6 weeks'); } |
---|
744 | if ( !isset($range_end) ) { $range_end = clone($range_start); $range_end->modify('+6 months'); } |
---|
745 | |
---|
746 | $new_components = array(); |
---|
747 | $result_limit = 1000; |
---|
748 | $instances = array(); |
---|
749 | $expand = false; |
---|
750 | $dtstart = null; |
---|
751 | foreach( $components AS $k => $comp ) { |
---|
752 | if ( $comp->GetType() != 'VEVENT' && $comp->GetType() != 'VTODO' && $comp->GetType() != 'VJOURNAL' ) { |
---|
753 | $new_components[] = $comp; |
---|
754 | continue; |
---|
755 | } |
---|
756 | if ( !isset($dtstart) ) { |
---|
757 | $dtstart = $comp->GetProperty('DTSTART'); |
---|
758 | $tzid = $dtstart->GetParameterValue('TZID'); |
---|
759 | $dtstart = new RepeatRuleDateTime( $dtstart->Value(), $tzid ); |
---|
760 | $instances[$dtstart->UTC()] = $comp; |
---|
761 | } |
---|
762 | $p = $comp->GetProperty('RECURRENCE-ID'); |
---|
763 | if ( isset($p) && $p->Value() != '' ) { |
---|
764 | $range = $p->GetParameterValue('RANGE'); |
---|
765 | $recur_tzid = $p->GetParameterValue('TZID'); |
---|
766 | $recur_utc = new RepeatRuleDateTime($p->Value(),$recur_tzid); |
---|
767 | $recur_utc = $recur_utc->UTC(); |
---|
768 | if ( isset($range) && $range == 'THISANDFUTURE' ) { |
---|
769 | foreach( $instances AS $k => $v ) { |
---|
770 | if ( $k >= $recur_utc ) unset($instances[$k]); |
---|
771 | } |
---|
772 | } |
---|
773 | else { |
---|
774 | unset($instances[$recur_utc]); |
---|
775 | } |
---|
776 | $instances[] = $comp; |
---|
777 | } |
---|
778 | $instances = array_merge( $instances, rrule_expand($dtstart, 'RRULE', $comp, $range_end) ); |
---|
779 | $instances = array_merge( $instances, rdate_expand($dtstart, 'RDATE', $comp, $range_end) ); |
---|
780 | foreach ( rdate_expand($dtstart, 'EXDATE', $comp, $range_end) AS $k => $v ) { |
---|
781 | unset($instances[$k]); |
---|
782 | } |
---|
783 | } |
---|
784 | |
---|
785 | $last_duration = null; |
---|
786 | $in_range = false; |
---|
787 | $new_components = array(); |
---|
788 | $start_utc = $range_start->UTC(); |
---|
789 | $end_utc = $range_end->UTC(); |
---|
790 | foreach( $instances AS $utc => $comp ) { |
---|
791 | if ( $utc > $end_utc ) break; |
---|
792 | |
---|
793 | $end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND'); |
---|
794 | $duration = $comp->GetProperty('DURATION'); |
---|
795 | if ( !isset($duration) || $duration->Value() == '' ) { |
---|
796 | $instance_start = $comp->GetProperty('DTSTART'); |
---|
797 | $dtsrt = new RepeatRuleDateTime( $instance_start->Value(), $instance_start->GetParameterValue('TZID')); |
---|
798 | $instance_end = $comp->GetProperty($end_type); |
---|
799 | $dtend = new RepeatRuleDateTime( $instance_end->Value(), $instance_end->GetParameterValue('TZID')); |
---|
800 | $duration = $dtstart->RFC5545Duration( $dtend ); |
---|
801 | } |
---|
802 | else { |
---|
803 | $duration = $duration->Value(); |
---|
804 | } |
---|
805 | |
---|
806 | if ( $utc < $start_utc ) { |
---|
807 | if ( isset($last_duration) && $duration == $last_duration) { |
---|
808 | if ( $utc < $early_start ) continue; |
---|
809 | } |
---|
810 | else { |
---|
811 | $latest_start = clone($range_start); |
---|
812 | $latest_start->modify('-'.$duration); |
---|
813 | $early_start = $latest_start->UTC(); |
---|
814 | $last_duration = $duration; |
---|
815 | if ( $utc < $early_start ) continue; |
---|
816 | } |
---|
817 | } |
---|
818 | $component = clone($comp); |
---|
819 | $component->ClearProperties( array('DTSTART'=> true, 'DUE' => true, 'DTEND' => true) ); |
---|
820 | $component->AddProperty('DTSTART', $utc ); |
---|
821 | $component->AddProperty('DURATION', $duration ); |
---|
822 | $new_components[] = $component; |
---|
823 | $in_range = true; |
---|
824 | } |
---|
825 | |
---|
826 | if ( $in_range ) { |
---|
827 | $vResource->SetComponents($new_components); |
---|
828 | } |
---|
829 | else { |
---|
830 | $vResource->SetComponents(array()); |
---|
831 | } |
---|
832 | |
---|
833 | return $vResource; |
---|
834 | } |
---|