Completed
Push — renovate/history-4.x ( 8706da...6c1ea7 )
by
unknown
17:57 queued 11:18
created

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

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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