Completed
Push — fix/7357 ( 13a0c4...764bff )
by
unknown
11:52
created

_inc/lib/icalendar-reader.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/**
4
 * Gets and renders iCal feeds for the Upcoming Events widget and shortcode
5
 */
6
7
class iCalendarReader {
8
9
	public $todo_count = 0;
10
	public $event_count = 0;
11
	public $cal = array();
12
	public $_lastKeyWord = '';
13
	public $timezone = null;
14
15
	/**
16
	 * Class constructor
17
	 *
18
	 * @return void
19
	 */
20
	public function __construct() {}
21
22
	/**
23
	 * Return an array of events
24
	 *
25
	 * @param string $url (default: '')
26
	 * @return array | false on failure
27
	 */
28
	public function get_events( $url = '', $count = 5 ) {
29
		$count = (int) $count;
30
		$transient_id = 'icalendar_vcal_' . md5( $url ) . '_' . $count;
31
32
		$vcal = get_transient( $transient_id );
33
34
		if ( ! empty( $vcal ) ) {
35
			if ( isset( $vcal['TIMEZONE'] ) )
36
				$this->timezone = $this->timezone_from_string( $vcal['TIMEZONE'] );
37
38
			if ( isset( $vcal['VEVENT'] ) ) {
39
				$vevent = $vcal['VEVENT'];
40
41
				if ( $count > 0 )
42
					$vevent = array_slice( $vevent, 0, $count );
43
44
				$this->cal['VEVENT'] = $vevent;
45
46
				return $this->cal['VEVENT'];
47
			}
48
		}
49
50
		if ( ! $this->parse( $url ) )
51
			return false;
52
53
		$vcal = array();
54
55
		if ( $this->timezone ) {
56
			$vcal['TIMEZONE'] = $this->timezone->getName();
57
		} else {
58
			$this->timezone = $this->timezone_from_string( '' );
59
		}
60
61
		if ( ! empty( $this->cal['VEVENT'] ) ) {
62
			$vevent = $this->cal['VEVENT'];
63
64
			// check for recurring events
65
			// $vevent = $this->add_recurring_events( $vevent );
66
67
			// remove before caching - no sense in hanging onto the past
68
			$vevent = $this->filter_past_and_recurring_events( $vevent );
69
70
			// order by soonest start date
71
			$vevent = $this->sort_by_recent( $vevent );
72
73
			$vcal['VEVENT'] = $vevent;
74
		}
75
76
		set_transient( $transient_id, $vcal, HOUR_IN_SECONDS );
77
78
		if ( !isset( $vcal['VEVENT'] ) )
79
			return false;
80
81
		if ( $count > 0 )
82
			return array_slice( $vcal['VEVENT'], 0, $count );
83
84
		return $vcal['VEVENT'];
85
	}
86
87
	function apply_timezone_offset( $events ) {
88
		if ( ! $events ) {
89
			return $events;
90
		}
91
92
		// get timezone offset from the timezone name.
93
		$timezone_name = get_option( 'timezone_string' );
94
		if ( $timezone_name ) {
95
			$timezone = new DateTimeZone( $timezone_name );
96
			$timezone_offset_interval = false;
97
		} else {
98
			// If the timezone isn't set then the GMT offset must be set.
99
			// generate a DateInterval object from the timezone offset
100
			$gmt_offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
101
			$timezone_offset_interval = date_interval_create_from_date_string( "{$gmt_offset} seconds" );
102
			$timezone = new DateTimeZone( 'UTC' );
103
		}
104
105
		$offsetted_events = array();
106
107
		foreach ( $events as $event ) {
108
			// Don't handle all-day events
109
			if ( 8 < strlen( $event['DTSTART'] ) ) {
110
				$start_time = preg_replace( '/Z$/', '', $event['DTSTART'] );
111
				$start_time = new DateTime( $start_time, $this->timezone );
112
				$start_time->setTimeZone( $timezone );
113
114
				$end_time = preg_replace( '/Z$/', '', $event['DTEND'] );
115
				$end_time = new DateTime( $end_time, $this->timezone );
116
				$end_time->setTimeZone( $timezone );
117
118
				if ( $timezone_offset_interval ) {
119
					$start_time->add( $timezone_offset_interval );
120
					$end_time->add( $timezone_offset_interval );
121
				}
122
123
				$event['DTSTART'] = $start_time->format( 'YmdHis\Z' );
124
				$event['DTEND'] = $end_time->format( 'YmdHis\Z' );
125
			}
126
127
			$offsetted_events[] = $event;
128
		}
129
130
		return $offsetted_events;
131
	}
132
133
	protected function filter_past_and_recurring_events( $events ) {
134
		$upcoming = array();
135
		$set_recurring_events = array();
136
		$recurrences = array();
137
		/**
138
		 * This filter allows any time to be passed in for testing or changing timezones, etc...
139
		 *
140
		 * @module widgets
141
		 *
142
		 * @since 3.4.0
143
		 *
144
		 * @param object time() A time object.
145
		 */
146
		$current = apply_filters( 'ical_get_current_time', time() );
147
148
		foreach ( $events as $event ) {
149
150
			$date_from_ics = strtotime( $event['DTSTART'] );
151 View Code Duplication
			if ( isset( $event['DTEND'] ) ) {
152
				$duration = strtotime( $event['DTEND'] ) - strtotime( $event['DTSTART'] );
153
			} else {
154
				$duration = 0;
155
			}
156
157
			if ( isset( $event['RRULE'] ) && $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
158
				try {
159
					$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone('UTC') );
160
					$adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
161
					$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
162
					$date_from_ics = strtotime( $event['DTSTART'] );
163
164
					$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
165
				} catch ( Exception $e ) {
166
					// Invalid argument to DateTime
167
				}
168
169
				if ( isset( $event['EXDATE'] ) ) {
170
					$exdates = array();
171
					foreach ( (array) $event['EXDATE'] as $exdate ) {
172
						try {
173
							$adjusted_time = new DateTime( $exdate, new DateTimeZone('UTC') );
174
							$adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
175
							if ( 8 == strlen( $event['DTSTART'] ) ) {
176
								$exdates[] = $adjusted_time->format( 'Ymd' );
177
							} else {
178
								$exdates[] = $adjusted_time->format( 'Ymd\THis' );
179
							}
180
						} catch ( Exception $e ) {
181
							// Invalid argument to DateTime
182
						}
183
					}
184
					$event['EXDATE'] = $exdates;
185
				} else {
186
					$event['EXDATE'] = array();
187
				}
188
			}
189
190
			if ( ! isset( $event['DTSTART'] ) ) {
191
				continue;
192
			}
193
194
			// Process events with RRULE before other events
195
			$rrule = isset( $event['RRULE'] ) ? $event['RRULE'] : false ;
196
			$uid = $event['UID'];
197
198
			if ( $rrule && ! in_array( $uid, $set_recurring_events ) ) {
199
200
				// Break down the RRULE into digestible chunks
201
				$rrule_array = array();
202
203
				foreach ( explode( ";", $event['RRULE'] ) as $rline ) {
204
					list( $rkey, $rvalue ) = explode( "=", $rline, 2 );
205
					$rrule_array[$rkey] = $rvalue;
206
				}
207
208
				$interval = ( isset( $rrule_array['INTERVAL'] ) ) ? $rrule_array['INTERVAL'] : 1;
209
				$rrule_count = ( isset( $rrule_array['COUNT'] ) ) ? $rrule_array['COUNT'] : 0;
210
				$until = ( isset( $rrule_array['UNTIL'] ) ) ? strtotime( $rrule_array['UNTIL'] ) : strtotime( '+1 year', $current );
211
212
				// Used to bound event checks
213
				$echo_limit = 10;
214
				$noop = false;
215
216
				// Set bydays for the event
217
				$weekdays = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' );
218
				$bydays = $weekdays;
219
220
				// Calculate a recent start date for incrementing depending on the frequency and interval
221
				switch ( $rrule_array['FREQ'] ) {
222
223
					case 'DAILY':
224
						$frequency = 'day';
225
						$echo_limit = 10;
226
227
						if ( $date_from_ics >= $current ) {
228
							$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
229
						} else {
230
							// Interval and count
231
							$catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * DAY_IN_SECONDS ) );
232
							if ( $rrule_count && $catchup > 0 ) {
233
								if ( $catchup < $rrule_count ) {
234
									$rrule_count = $rrule_count - $catchup;
235
									$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
236
								} else {
237
									$noop = true;
238
								}
239 View Code Duplication
							} else {
240
								$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
241
							}
242
						}
243
						break;
244
245
					case 'WEEKLY':
246
						$frequency = 'week';
247
						$echo_limit = 4;
248
249
						// BYDAY exception to current date
250
						$day = false;
251
						if ( ! isset( $rrule_array['BYDAY'] ) ) {
252
							$day = $rrule_array['BYDAY'] = strtoupper( substr( date( 'D', strtotime( $event['DTSTART'] ) ), 0, 2 ) );
253
						}
254
						$bydays = explode( ',', $rrule_array['BYDAY'] );
255
256
						if ( $date_from_ics >= $current ) {
257
							$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
258
						} else {
259
							// Interval and count
260
							$catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * WEEK_IN_SECONDS ) );
261
							if ( $rrule_count && $catchup > 0 ) {
262
								if ( ( $catchup * count( $bydays ) ) < $rrule_count ) {
263
									$rrule_count = $rrule_count - ( $catchup * count( $bydays ) ); // Estimate current event count
264
									$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
265
								} else {
266
									$noop = true;
267
								}
268 View Code Duplication
							} else {
269
								$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
270
							}
271
						}
272
273
						// Set to Sunday start
274
						if ( ! $noop && 'SU' !== strtoupper( substr( date( 'D', strtotime( $recurring_event_date_start ) ), 0, 2 ) ) ) {
275
							$recurring_event_date_start = date( 'Ymd', strtotime( "last Sunday", strtotime( $recurring_event_date_start ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
276
						}
277
						break;
278
279
					case 'MONTHLY':
280
						$frequency = 'month';
281
						$echo_limit = 1;
282
283
						if ( $date_from_ics >= $current ) {
284
							$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
285
						} else {
286
							// Describe the date in the month
287
							if ( isset( $rrule_array['BYDAY'] ) ) {
288
								$day_number = substr( $rrule_array['BYDAY'], 0, 1 );
289
								$week_day = substr( $rrule_array['BYDAY'], 1 );
290
								$day_cardinals = array( 1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth' );
291
								$weekdays = array( 'SU' => 'Sunday', 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday' );
292
								$event_date_desc = "{$day_cardinals[$day_number]} {$weekdays[$week_day]} of ";
293
							} else {
294
								$event_date_desc = date( 'd ', strtotime( $event['DTSTART'] ) );
295
							}
296
297
							// Interval only
298
							if ( $interval > 1 ) {
299
								$catchup = 0;
300
								$maybe = strtotime( $event['DTSTART'] );
301
								while ( $maybe < $current ) {
302
									$maybe = strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) );
303
									$catchup++;
304
								}
305
								$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * ( $catchup - 1 ) ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
306 View Code Duplication
							} else {
307
								$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', $current ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
308
							}
309
310
							// Add one interval if necessary
311
							if ( strtotime( $recurring_event_date_start ) < $current ) {
312
								if ( $interval > 1 ) {
313
									$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
314 View Code Duplication
								} else {
315
									try {
316
										$adjustment = new DateTime( date( 'Y-m-d', $current ) );
317
										$adjustment->modify( 'first day of next month' );
318
										$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . $adjustment->format( 'F Y' ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
319
									} catch ( Exception $e ) {
320
										// Invalid argument to DateTime
321
									}
322
								}
323
							}
324
						}
325
						break;
326
327
					case 'YEARLY':
328
						$frequency = 'year';
329
						$echo_limit = 1;
330
331
						if ( $date_from_ics >= $current ) {
332
							$recurring_event_date_start = date( "Ymd\THis", strtotime( $event['DTSTART'] ) );
333
						} else {
334
							$recurring_event_date_start = date( 'Y', $current ) . date( "md\THis", strtotime( $event['DTSTART'] ) );
335 View Code Duplication
							if ( strtotime( $recurring_event_date_start ) < $current ) {
336
								try {
337
									$next = new DateTime( date( 'Y-m-d', $current ) );
338
									$next->modify( 'first day of next year' );
339
									$recurring_event_date_start = $next->format( 'Y' ) . date ( 'md\THis', strtotime( $event['DTSTART'] ) );
340
								} catch ( Exception $e ) {
341
									// Invalid argument to DateTime
342
								}
343
							}
344
						}
345
						break;
346
347
					default:
348
						$frequency = false;
349
				}
350
351
				if ( $frequency !== false && ! $noop ) {
352
					$count_counter = 1;
353
354
					// If no COUNT limit, go to 10
355
					if ( empty( $rrule_count ) ) {
356
						$rrule_count = 10;
357
					}
358
359
					// Set up EXDATE handling for the event
360
					$exdates = ( isset( $event['EXDATE'] ) ) ? $event['EXDATE'] : array();
361
362
					for ( $i = 1; $i <= $echo_limit; $i++ ) {
363
364
						// Weeks need a daily loop and must check for inclusion in BYDAYS
365
						if ( 'week' == $frequency ) {
366
							$byday_event_date_start = strtotime( $recurring_event_date_start );
367
368
							foreach ( $weekdays as $day ) {
369
370
								$event_start_timestamp = $byday_event_date_start;
371
								$start_time = date( 'His', $event_start_timestamp );
372
								$event_end_timestamp = $event_start_timestamp + $duration;
373
								$end_time = date( 'His', $event_end_timestamp );
374 View Code Duplication
								if ( 8 == strlen( $event['DTSTART'] ) ) {
375
									$exdate_compare = date( 'Ymd', $event_start_timestamp );
376
								} else {
377
									$exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
378
								}
379
380 View Code Duplication
								if ( in_array( $day, $bydays ) && $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) {
381
									if ( 8 == strlen( $event['DTSTART'] ) ) {
382
										$event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
383
										$event['DTEND'] = date( 'Ymd', $event_end_timestamp );
384
									} else {
385
										$event['DTSTART'] = date( 'Ymd\THis', $event_start_timestamp );
386
										$event['DTEND'] = date( 'Ymd\THis', $event_end_timestamp );
387
									}
388
									if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
389
										try {
390
											$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
391
											$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
392
											$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
393
394
											$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
395
										} catch ( Exception $e ) {
396
											// Invalid argument to DateTime
397
										}
398
									}
399
									$upcoming[] = $event;
400
									$count_counter++;
401
								}
402
403
								// Move forward one day
404
								$byday_event_date_start = strtotime( date( 'Ymd\T', strtotime( '+ 1 day', $event_start_timestamp ) ) . $start_time );
405
							}
406
407
							// Restore first event timestamp
408
							$event_start_timestamp = strtotime( $recurring_event_date_start );
409
410
						} else {
411
412
							$event_start_timestamp = strtotime( $recurring_event_date_start );
413
							$start_time = date( 'His', $event_start_timestamp );
414
							$event_end_timestamp = $event_start_timestamp + $duration;
415
							$end_time = date( 'His', $event_end_timestamp );
416 View Code Duplication
							if ( 8 == strlen( $event['DTSTART'] ) ) {
417
								$exdate_compare = date( 'Ymd', $event_start_timestamp );
418
							} else {
419
								$exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
420
							}
421
422 View Code Duplication
							if ( $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) {
423
								if ( 8 == strlen( $event['DTSTART'] ) ) {
424
									$event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
425
									$event['DTEND'] = date( 'Ymd', $event_end_timestamp );
426
								} else {
427
									$event['DTSTART'] = date( 'Ymd\T', $event_start_timestamp ) . $start_time;
428
									$event['DTEND'] = date( 'Ymd\T', $event_end_timestamp ) . $end_time;
429
								}
430
								if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
431
									try {
432
										$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
433
										$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
434
										$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
435
436
										$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
437
									} catch ( Exception $e ) {
438
										// Invalid argument to DateTime
439
									}
440
								}
441
								$upcoming[] = $event;
442
								$count_counter++;
443
							}
444
						}
445
446
						// Set up next interval and reset $event['DTSTART'] and $event['DTEND'], keeping timestamps intact
447
						$next_start_timestamp = strtotime( "+ {$interval} {$frequency}s", $event_start_timestamp );
448
						if ( 8 == strlen( $event['DTSTART'] ) ) {
449
							$event['DTSTART'] = date( 'Ymd', $next_start_timestamp );
450
							$event['DTEND'] = date( 'Ymd', strtotime( $event['DTSTART'] ) + $duration );
451 View Code Duplication
						} else {
452
							$event['DTSTART'] = date( 'Ymd\THis', $next_start_timestamp );
453
							$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
454
						}
455
456
						// Move recurring event date forward
457
						$recurring_event_date_start = $event['DTSTART'];
458
					}
459
					$set_recurring_events[] = $uid;
460
461
				}
462
463 View Code Duplication
			} else {
464
				// Process normal events
465
				if ( strtotime( isset( $event['DTEND'] ) ? $event['DTEND'] : $event['DTSTART'] ) >= $current ) {
466
					$upcoming[] = $event;
467
				}
468
			}
469
		}
470
		return $upcoming;
471
	}
472
473
	/**
474
	 * Parse events from an iCalendar feed
475
	 *
476
	 * @param string $url (default: '')
477
	 * @return array | false on failure
478
	 */
479
	public function parse( $url = '' ) {
480
		$cache_group = 'icalendar_reader_parse';
481
		$disable_get_key = 'disable:' . md5( $url );
482
483
		// Check to see if previous attempts have failed
484
		if ( false !== wp_cache_get( $disable_get_key, $cache_group ) )
485
			return false;
486
487
		// rewrite webcal: URI schem to HTTP
488
		$url = preg_replace('/^webcal/', 'http', $url );
489
		// try to fetch
490
		$r = wp_remote_get( $url, array( 'timeout' => 3, 'sslverify' => false ) );
491
		if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
492
			// We were unable to fetch any content, so don't try again for another 60 seconds
493
			wp_cache_set( $disable_get_key, 1, $cache_group, 60 );
494
			return false;
495
		}
496
497
		$body = wp_remote_retrieve_body( $r );
498
		if ( empty( $body ) )
499
			return false;
500
501
		$body = str_replace( "\r\n", "\n", $body );
502
		$lines = preg_split( "/\n(?=[A-Z])/", $body );
503
504
		if ( empty( $lines ) )
505
			return false;
506
507
		if ( false === stristr( $lines[0], 'BEGIN:VCALENDAR' ) )
508
			return false;
509
510
		foreach ( $lines as $line ) {
511
			$add  = $this->key_value_from_string( $line );
512
			if ( ! $add ) {
513
				$this->add_component( $type, false, $line );
514
				continue;
515
			}
516
			list( $keyword, $value ) = $add;
517
518
			switch ( $keyword ) {
519
				case 'BEGIN':
520
				case 'END':
521
					switch ( $line ) {
522
						case 'BEGIN:VTODO':
523
							$this->todo_count++;
524
							$type = 'VTODO';
525
							break;
526
						case 'BEGIN:VEVENT':
527
							$this->event_count++;
528
							$type = 'VEVENT';
529
							break;
530
						case 'BEGIN:VCALENDAR':
531
						case 'BEGIN:DAYLIGHT':
532
						case 'BEGIN:VTIMEZONE':
533
						case 'BEGIN:STANDARD':
534
							$type = $value;
535
							break;
536
						case 'END:VTODO':
537
						case 'END:VEVENT':
538
						case 'END:VCALENDAR':
539
						case 'END:DAYLIGHT':
540
						case 'END:VTIMEZONE':
541
						case 'END:STANDARD':
542
							$type = 'VCALENDAR';
543
							break;
544
					}
545
					break;
546
				case 'TZID':
547
					if ( 'VTIMEZONE' == $type && ! $this->timezone )
548
						$this->timezone = $this->timezone_from_string( $value );
549
					break;
550
				case 'X-WR-TIMEZONE':
551
					if ( ! $this->timezone )
552
						$this->timezone = $this->timezone_from_string( $value );
553
					break;
554
				default:
555
					$this->add_component( $type, $keyword, $value );
556
					break;
557
			}
558
		}
559
560
		// Filter for RECURRENCE-IDs
561
		$recurrences = array();
562
		if ( array_key_exists( 'VEVENT', $this->cal ) ) {
563
			foreach ( $this->cal['VEVENT'] as $event ) {
564
				if ( isset( $event['RECURRENCE-ID'] ) ) {
565
					$recurrences[] = $event;
566
				}
567
			}
568
			foreach ( $recurrences as $recurrence ) {
569
				for ( $i = 0; $i < count( $this->cal['VEVENT'] ); $i++ ) {
570
					if ( $this->cal['VEVENT'][ $i ]['UID'] == $recurrence['UID'] && ! isset( $this->cal['VEVENT'][ $i ]['RECURRENCE-ID'] ) ) {
571
						$this->cal['VEVENT'][ $i ]['EXDATE'][] = $recurrence['RECURRENCE-ID'];
572
						break;
573
					}
574
				}
575
			}
576
		}
577
578
		return $this->cal;
579
	}
580
581
	/**
582
	 * Parse key:value from a string
583
	 *
584
	 * @param string $text (default: '')
585
	 * @return array
586
	 */
587
	public function key_value_from_string( $text = '' ) {
588
		preg_match( '/([^:]+)(;[^:]+)?[:]([\w\W]*)/', $text, $matches );
589
590
		if ( 0 == count( $matches ) )
591
			return false;
592
593
		return array( $matches[1], $matches[3] );
594
	}
595
596
	/**
597
	 * Convert a timezone name into a timezone object.
598
	 *
599
	 * @param string $text Timezone name. Example: America/Chicago
600
	 * @return object|null A DateTimeZone object if the conversion was successful.
601
	 */
602
	private function timezone_from_string( $text ) {
603
		try {
604
			$timezone = new DateTimeZone( $text );
605
		} catch ( Exception $e ) {
606
			$blog_timezone = get_option( 'timezone_string' );
607
			if ( ! $blog_timezone ) {
608
				$blog_timezone = 'Etc/UTC';
609
			}
610
611
			$timezone = new DateTimeZone( $blog_timezone );
612
		}
613
614
		return $timezone;
615
	}
616
617
	/**
618
	 * Add a component to the calendar array
619
	 *
620
	 * @param string $component (default: '')
621
	 * @param string $keyword (default: '')
622
	 * @param string $value (default: '')
623
	 * @return void
624
	 */
625
	public function add_component( $component = '', $keyword = '', $value = '' ) {
626
		if ( false == $keyword ) {
627
			$keyword = $this->last_keyword;
628
			switch ( $component ) {
629
			case 'VEVENT':
630
				$value = $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] . $value;
631
				break;
632
			case 'VTODO' :
633
				$value = $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] . $value;
634
				break;
635
			}
636
		}
637
638
		/*
639
		 * Some events have a specific timezone set in their start/end date,
640
		 * and it may or may not be different than the calendar timzeone.
641
		 * Valid formats include:
642
		 * DTSTART;TZID=Pacific Standard Time:20141219T180000
643
		 * DTEND;TZID=Pacific Standard Time:20141219T200000
644
		 * EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
645
		 * EXDATE;VALUE=DATE:2015050
646
		 * EXDATE;TZID=America/New_York:20150424T170000
647
		 * EXDATE;TZID=Pacific Standard Time:20120615T140000,20120629T140000,20120706T140000
648
		 */
649
650
		// Always store EXDATE as an array
651
		if ( stristr( $keyword, 'EXDATE' ) ) {
652
			$value = explode( ',', $value );
653
		}
654
655
		// Adjust DTSTART, DTEND, and EXDATE according to their TZID if set
656
		if ( strpos( $keyword, ';' ) && ( stristr( $keyword, 'DTSTART' ) || stristr( $keyword, 'DTEND' ) || stristr( $keyword, 'EXDATE' ) || stristr( $keyword, 'RECURRENCE-ID' ) ) ) {
657
			$keyword = explode( ';', $keyword );
658
659
			$tzid = false;
660
			if ( 2 == count( $keyword ) ) {
661
				$tparam = $keyword[1];
662
663
				if ( strpos( $tparam, "TZID" ) !== false ) {
664
					$tzid = $this->timezone_from_string( str_replace( 'TZID=', '', $tparam ) );
665
				}
666
			}
667
668
			// Normalize all times to default UTC
669
			if ( $tzid ) {
670
				$adjusted_times = array();
671
				foreach ( (array) $value as $v ) {
672
					try {
673
						$adjusted_time = new DateTime( $v, $tzid );
674
						$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
675
						$adjusted_times[] = $adjusted_time->format('Ymd\THis');
676
					} catch ( Exception $e ) {
677
						// Invalid argument to DateTime
678
						return;
679
					}
680
				}
681
				$value = $adjusted_times;
682
			}
683
684
			// Format for adding to event
685
			$keyword = $keyword[0];
686
			if ( 'EXDATE' != $keyword ) {
687
				$value = implode( (array) $value );
688
			}
689
		}
690
691
		foreach ( (array) $value as $v ) {
692
			switch ($component) {
693 View Code Duplication
				case 'VTODO':
694
					if ( 'EXDATE' == $keyword ) {
695
						$this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ][] = $v;
696
					} else {
697
						$this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] = $v;
698
					}
699
					break;
700 View Code Duplication
				case 'VEVENT':
701
					if ( 'EXDATE' == $keyword ) {
702
						$this->cal[ $component ][ $this->event_count - 1 ][ $keyword ][] = $v;
703
					} else {
704
						$this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] = $v;
705
					}
706
					break;
707
				default:
708
					$this->cal[ $component ][ $keyword ] = $v;
709
					break;
710
			}
711
		}
712
		$this->last_keyword = $keyword;
713
	}
714
715
	/**
716
	 * Escape strings with wp_kses, allow links
717
	 *
718
	 * @param string $string (default: '')
719
	 * @return string
720
	 */
721
	public function escape( $string = '' ) {
722
		// Unfold content lines per RFC 5545
723
		$string = str_replace( "\n\t", '', $string );
724
		$string = str_replace( "\n ", '', $string );
725
726
		$allowed_html = array(
727
			'a' => array(
728
				'href'  => array(),
729
				'title' => array()
730
			)
731
		);
732
733
		$allowed_tags = '';
734
		foreach ( array_keys( $allowed_html ) as $tag ) {
735
			$allowed_tags .= "<{$tag}>";
736
		}
737
738
		// Running strip_tags() first with allowed tags to get rid of remaining gallery markup, etc
739
		// because wp_kses() would only htmlentity'fy that. Then still running wp_kses(), for extra
740
		// safety and good measure.
741
		return wp_kses( strip_tags( $string, $allowed_tags ), $allowed_html );
742
	}
743
744
	/**
745
	 * Render the events
746
	 *
747
	 * @param string $url (default: '')
748
	 * @param string $context (default: 'widget') or 'shortcode'
0 ignored issues
show
There is no parameter named $context. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
749
	 * @return mixed bool|string false on failure, rendered HTML string on success.
750
	 */
751
	public function render( $url = '', $args = array() ) {
752
753
		$args = wp_parse_args( $args, array(
754
			'context' => 'widget',
755
			'number' => 5
756
		) );
757
758
		$events = $this->get_events( $url, $args['number'] );
759
		$events = $this->apply_timezone_offset( $events );
760
761
		if ( empty( $events ) )
762
			return false;
763
764
		ob_start();
765
766
		if ( 'widget' == $args['context'] ) : ?>
767
		<ul class="upcoming-events">
768
			<?php foreach ( $events as $event ) : ?>
769
			<li>
770
				<strong class="event-summary"><?php echo $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></strong>
771
				<span class="event-when"><?php echo $this->formatted_date( $event ); ?></span>
772 View Code Duplication
				<?php if ( ! empty( $event['LOCATION'] ) ) : ?>
773
					<span class="event-location"><?php echo $this->escape( stripslashes( $event['LOCATION'] ) ); ?></span>
774
				<?php endif; ?>
775 View Code Duplication
				<?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?>
776
					<span class="event-description"><?php echo wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></span>
777
				<?php endif; ?>
778
			</li>
779
			<?php endforeach; ?>
780
		</ul>
781
		<?php endif;
782
783
		if ( 'shortcode' == $args['context'] ) : ?>
784
		<table class="upcoming-events">
785
			<thead>
786
				<tr>
787
					<th><?php esc_html_e( 'Location', 'jetpack' ); ?></th>
788
					<th><?php esc_html_e( 'When', 'jetpack' ); ?></th>
789
					<th><?php esc_html_e( 'Summary', 'jetpack' ); ?></th>
790
					<th><?php esc_html_e( 'Description', 'jetpack' ); ?></th>
791
				</tr>
792
			</thead>
793
			<tbody>
794
			<?php foreach ( $events as $event ) : ?>
795
				<tr>
796
					<td><?php echo empty( $event['LOCATION'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['LOCATION'] ) ); ?></td>
797
					<td><?php echo $this->formatted_date( $event ); ?></td>
798
					<td><?php echo empty( $event['SUMMARY'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></td>
799
					<td><?php echo empty( $event['DESCRIPTION'] ) ? '&nbsp;' : wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></td>
800
				</tr>
801
			<?php endforeach; ?>
802
			</tbody>
803
		</table>
804
		<?php endif;
805
806
		$rendered = ob_get_clean();
807
808
		if ( empty( $rendered ) )
809
			return false;
810
811
		return $rendered;
812
	}
813
814
	public function formatted_date( $event ) {
815
816
		$date_format = get_option( 'date_format' );
817
		$time_format = get_option( 'time_format' );
818
		$start = strtotime( $event['DTSTART'] );
819
		$end = isset( $event['DTEND'] ) ? strtotime( $event['DTEND'] ) : false;
820
821
		$all_day = ( 8 == strlen( $event['DTSTART'] ) );
822
823
		if ( !$all_day && $this->timezone ) {
824
			try {
825
				$start_time = new DateTime( $event['DTSTART'] );
826
				$timezone_offset = $this->timezone->getOffset( $start_time );
827
				$start += $timezone_offset;
828
829
				if ( $end ) {
830
					$end += $timezone_offset;
831
				}
832
			} catch ( Exception $e ) {
833
				// Invalid argument to DateTime
834
			}
835
		}
836
		$single_day = $end ? ( $end - $start ) <= DAY_IN_SECONDS : true;
837
838
		/* translators: Date and time */
839
		$date_with_time = __( '%1$s at %2$s' , 'jetpack' );
840
		/* translators: Two dates with a separator */
841
		$two_dates = __( '%1$s &ndash; %2$s' , 'jetpack' );
842
843
		// we'll always have the start date. Maybe with time
844 View Code Duplication
		if ( $all_day )
845
			$date = date_i18n( $date_format, $start );
846
		else
847
			$date = sprintf( $date_with_time, date_i18n( $date_format, $start ), date_i18n( $time_format, $start ) );
848
849
		// single day, timed
850
		if ( $single_day && ! $all_day && false !== $end )
851
			$date = sprintf( $two_dates, $date, date_i18n( $time_format, $end ) );
852
853
		// multi-day
854
		if ( ! $single_day ) {
855
856 View Code Duplication
			if ( $all_day ) {
857
				// DTEND for multi-day events represents "until", not "including", so subtract one minute
858
				$end_date = date_i18n( $date_format, $end - 60 );
859
			} else {
860
				$end_date = sprintf( $date_with_time, date_i18n( $date_format, $end ), date_i18n( $time_format, $end ) );
861
			}
862
863
			$date = sprintf( $two_dates, $date, $end_date );
864
865
		}
866
867
		return $date;
868
	}
869
870
	protected function sort_by_recent( $list ) {
871
		$dates = $sorted_list = array();
872
873
		foreach ( $list as $key => $row ) {
874
			$date = $row['DTSTART'];
875
			// pad some time onto an all day date
876
			if ( 8 === strlen( $date ) )
877
				$date .= 'T000000Z';
878
			$dates[$key] = $date;
879
		}
880
		asort( $dates );
881
		foreach( $dates as $key => $value ) {
882
			$sorted_list[$key] = $list[$key];
883
		}
884
		unset($list);
885
		return $sorted_list;
886
	}
887
888
}
889
890
891
/**
892
 * Wrapper function for iCalendarReader->get_events()
893
 *
894
 * @param string $url (default: '')
895
 * @return array
896
 */
897
function icalendar_get_events( $url = '', $count = 5 ) {
898
	// Find your calendar's address http://support.google.com/calendar/bin/answer.py?hl=en&answer=37103
899
	$ical = new iCalendarReader();
900
	return $ical->get_events( $url, $count );
901
}
902
903
/**
904
 * Wrapper function for iCalendarReader->render()
905
 *
906
 * @param string $url (default: '')
907
 * @param string $context (default: 'widget') or 'shortcode'
908
 * @return mixed bool|string false on failure, rendered HTML string on success.
909
 */
910
function icalendar_render_events( $url = '', $args = array() ) {
911
	$ical = new iCalendarReader();
912
	return $ical->render( $url, $args );
913
}
914