Completed
Push — add/changelog-55 ( 8beac4...406ec3 )
by Jeremy
15:59 queued 07:35
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
		} else {
97
			// If the timezone isn't set then the GMT offset must be set.
98
			// generate a DateInterval object from the timezone offset
99
			$gmt_offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
100
			$timezone_offset_interval = date_interval_create_from_date_string( "{$gmt_offset} seconds" );
101
			$timezone = new DateTimeZone( 'UTC' );
102
		}
103
104
		$offsetted_events = array();
105
106
		foreach ( $events as $event ) {
107
			// Don't handle all-day events
108
			if ( 8 < strlen( $event['DTSTART'] ) ) {
109
				$start_time = preg_replace( '/Z$/', '', $event['DTSTART'] );
110
				$start_time = new DateTime( $start_time, $this->timezone );
111
				$start_time->setTimeZone( $timezone );
112
113
				$end_time = preg_replace( '/Z$/', '', $event['DTEND'] );
114
				$end_time = new DateTime( $end_time, $this->timezone );
115
				$end_time->setTimeZone( $timezone );
116
117
				if ( $timezone_offset_interval ) {
118
					$start_time->add( $timezone_offset_interval );
0 ignored issues
show
The variable $timezone_offset_interval does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
119
					$end_time->add( $timezone_offset_interval );
120
				}
121
122
				$event['DTSTART'] = $start_time->format( 'YmdHis\Z' );
123
				$event['DTEND'] = $end_time->format( 'YmdHis\Z' );
124
			}
125
126
			$offsetted_events[] = $event;
127
		}
128
129
		return $offsetted_events;
130
	}
131
132
	protected function filter_past_and_recurring_events( $events ) {
133
		$upcoming = array();
134
		$set_recurring_events = array();
135
		$recurrences = array();
136
		/**
137
		 * This filter allows any time to be passed in for testing or changing timezones, etc...
138
		 *
139
		 * @module widgets
140
		 *
141
		 * @since 3.4.0
142
		 *
143
		 * @param object time() A time object.
144
		 */
145
		$current = apply_filters( 'ical_get_current_time', time() );
146
147
		foreach ( $events as $event ) {
148
149
			$date_from_ics = strtotime( $event['DTSTART'] );
150 View Code Duplication
			if ( isset( $event['DTEND'] ) ) {
151
				$duration = strtotime( $event['DTEND'] ) - strtotime( $event['DTSTART'] );
152
			} else {
153
				$duration = 0;
154
			}
155
156
			if ( isset( $event['RRULE'] ) && $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
157
				try {
158
					$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone('UTC') );
159
					$adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
160
					$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
161
					$date_from_ics = strtotime( $event['DTSTART'] );
162
163
					$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
164
				} catch ( Exception $e ) {
165
					// Invalid argument to DateTime
166
				}
167
168
				if ( isset( $event['EXDATE'] ) ) {
169
					$exdates = array();
170
					foreach ( (array) $event['EXDATE'] as $exdate ) {
171
						try {
172
							$adjusted_time = new DateTime( $exdate, new DateTimeZone('UTC') );
173
							$adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
174
							if ( 8 == strlen( $event['DTSTART'] ) ) {
175
								$exdates[] = $adjusted_time->format( 'Ymd' );
176
							} else {
177
								$exdates[] = $adjusted_time->format( 'Ymd\THis' );
178
							}
179
						} catch ( Exception $e ) {
180
							// Invalid argument to DateTime
181
						}
182
					}
183
					$event['EXDATE'] = $exdates;
184
				} else {
185
					$event['EXDATE'] = array();
186
				}
187
			}
188
189
			if ( ! isset( $event['DTSTART'] ) ) {
190
				continue;
191
			}
192
193
			// Process events with RRULE before other events
194
			$rrule = isset( $event['RRULE'] ) ? $event['RRULE'] : false ;
195
			$uid = $event['UID'];
196
197
			if ( $rrule && ! in_array( $uid, $set_recurring_events ) ) {
198
199
				// Break down the RRULE into digestible chunks
200
				$rrule_array = array();
201
202
				foreach ( explode( ";", $event['RRULE'] ) as $rline ) {
203
					list( $rkey, $rvalue ) = explode( "=", $rline, 2 );
204
					$rrule_array[$rkey] = $rvalue;
205
				}
206
207
				$interval = ( isset( $rrule_array['INTERVAL'] ) ) ? $rrule_array['INTERVAL'] : 1;
208
				$rrule_count = ( isset( $rrule_array['COUNT'] ) ) ? $rrule_array['COUNT'] : 0;
209
				$until = ( isset( $rrule_array['UNTIL'] ) ) ? strtotime( $rrule_array['UNTIL'] ) : strtotime( '+1 year', $current );
210
211
				// Used to bound event checks
212
				$echo_limit = 10;
213
				$noop = false;
214
215
				// Set bydays for the event
216
				$weekdays = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' );
217
				$bydays = $weekdays;
218
219
				// Calculate a recent start date for incrementing depending on the frequency and interval
220
				switch ( $rrule_array['FREQ'] ) {
221
222
					case 'DAILY':
223
						$frequency = 'day';
224
						$echo_limit = 10;
225
226
						if ( $date_from_ics >= $current ) {
227
							$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
228
						} else {
229
							// Interval and count
230
							$catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * DAY_IN_SECONDS ) );
231
							if ( $rrule_count && $catchup > 0 ) {
232
								if ( $catchup < $rrule_count ) {
233
									$rrule_count = $rrule_count - $catchup;
234
									$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
235
								} else {
236
									$noop = true;
237
								}
238 View Code Duplication
							} else {
239
								$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
240
							}
241
						}
242
						break;
243
244
					case 'WEEKLY':
245
						$frequency = 'week';
246
						$echo_limit = 4;
247
248
						// BYDAY exception to current date
249
						$day = false;
250
						if ( ! isset( $rrule_array['BYDAY'] ) ) {
251
							$day = $rrule_array['BYDAY'] = strtoupper( substr( date( 'D', strtotime( $event['DTSTART'] ) ), 0, 2 ) );
252
						}
253
						$bydays = explode( ',', $rrule_array['BYDAY'] );
254
255
						if ( $date_from_ics >= $current ) {
256
							$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
257
						} else {
258
							// Interval and count
259
							$catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * WEEK_IN_SECONDS ) );
260
							if ( $rrule_count && $catchup > 0 ) {
261
								if ( ( $catchup * count( $bydays ) ) < $rrule_count ) {
262
									$rrule_count = $rrule_count - ( $catchup * count( $bydays ) ); // Estimate current event count
263
									$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
264
								} else {
265
									$noop = true;
266
								}
267 View Code Duplication
							} else {
268
								$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
269
							}
270
						}
271
272
						// Set to Sunday start
273
						if ( ! $noop && 'SU' !== strtoupper( substr( date( 'D', strtotime( $recurring_event_date_start ) ), 0, 2 ) ) ) {
274
							$recurring_event_date_start = date( 'Ymd', strtotime( "last Sunday", strtotime( $recurring_event_date_start ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
275
						}
276
						break;
277
278
					case 'MONTHLY':
279
						$frequency = 'month';
280
						$echo_limit = 1;
281
282
						if ( $date_from_ics >= $current ) {
283
							$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
284
						} else {
285
							// Describe the date in the month
286
							if ( isset( $rrule_array['BYDAY'] ) ) {
287
								$day_number = substr( $rrule_array['BYDAY'], 0, 1 );
288
								$week_day = substr( $rrule_array['BYDAY'], 1 );
289
								$day_cardinals = array( 1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth' );
290
								$weekdays = array( 'SU' => 'Sunday', 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday' );
291
								$event_date_desc = "{$day_cardinals[$day_number]} {$weekdays[$week_day]} of ";
292
							} else {
293
								$event_date_desc = date( 'd ', strtotime( $event['DTSTART'] ) );
294
							}
295
296
							// Interval only
297
							if ( $interval > 1 ) {
298
								$catchup = 0;
299
								$maybe = strtotime( $event['DTSTART'] );
300
								while ( $maybe < $current ) {
301
									$maybe = strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) );
302
									$catchup++;
303
								}
304
								$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'] ) );
305 View Code Duplication
							} else {
306
								$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', $current ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
307
							}
308
309
							// Add one interval if necessary
310
							if ( strtotime( $recurring_event_date_start ) < $current ) {
311
								if ( $interval > 1 ) {
312
									$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'] ) );
313 View Code Duplication
								} else {
314
									try {
315
										$adjustment = new DateTime( date( 'Y-m-d', $current ) );
316
										$adjustment->modify( 'first day of next month' );
317
										$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . $adjustment->format( 'F Y' ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
318
									} catch ( Exception $e ) {
319
										// Invalid argument to DateTime
320
									}
321
								}
322
							}
323
						}
324
						break;
325
326
					case 'YEARLY':
327
						$frequency = 'year';
328
						$echo_limit = 1;
329
330
						if ( $date_from_ics >= $current ) {
331
							$recurring_event_date_start = date( "Ymd\THis", strtotime( $event['DTSTART'] ) );
332
						} else {
333
							$recurring_event_date_start = date( 'Y', $current ) . date( "md\THis", strtotime( $event['DTSTART'] ) );
334 View Code Duplication
							if ( strtotime( $recurring_event_date_start ) < $current ) {
335
								try {
336
									$next = new DateTime( date( 'Y-m-d', $current ) );
337
									$next->modify( 'first day of next year' );
338
									$recurring_event_date_start = $next->format( 'Y' ) . date ( 'md\THis', strtotime( $event['DTSTART'] ) );
339
								} catch ( Exception $e ) {
340
									// Invalid argument to DateTime
341
								}
342
							}
343
						}
344
						break;
345
346
					default:
347
						$frequency = false;
348
				}
349
350
				if ( $frequency !== false && ! $noop ) {
351
					$count_counter = 1;
352
353
					// If no COUNT limit, go to 10
354
					if ( empty( $rrule_count ) ) {
355
						$rrule_count = 10;
356
					}
357
358
					// Set up EXDATE handling for the event
359
					$exdates = ( isset( $event['EXDATE'] ) ) ? $event['EXDATE'] : array();
360
361
					for ( $i = 1; $i <= $echo_limit; $i++ ) {
362
363
						// Weeks need a daily loop and must check for inclusion in BYDAYS
364
						if ( 'week' == $frequency ) {
365
							$byday_event_date_start = strtotime( $recurring_event_date_start );
366
367
							foreach ( $weekdays as $day ) {
368
369
								$event_start_timestamp = $byday_event_date_start;
370
								$start_time = date( 'His', $event_start_timestamp );
371
								$event_end_timestamp = $event_start_timestamp + $duration;
372
								$end_time = date( 'His', $event_end_timestamp );
373 View Code Duplication
								if ( 8 == strlen( $event['DTSTART'] ) ) {
374
									$exdate_compare = date( 'Ymd', $event_start_timestamp );
375
								} else {
376
									$exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
377
								}
378
379 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 ) ) {
380
									if ( 8 == strlen( $event['DTSTART'] ) ) {
381
										$event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
382
										$event['DTEND'] = date( 'Ymd', $event_end_timestamp );
383
									} else {
384
										$event['DTSTART'] = date( 'Ymd\THis', $event_start_timestamp );
385
										$event['DTEND'] = date( 'Ymd\THis', $event_end_timestamp );
386
									}
387
									if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
388
										try {
389
											$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
390
											$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
391
											$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
392
393
											$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
394
										} catch ( Exception $e ) {
395
											// Invalid argument to DateTime
396
										}
397
									}
398
									$upcoming[] = $event;
399
									$count_counter++;
400
								}
401
402
								// Move forward one day
403
								$byday_event_date_start = strtotime( date( 'Ymd\T', strtotime( '+ 1 day', $event_start_timestamp ) ) . $start_time );
404
							}
405
406
							// Restore first event timestamp
407
							$event_start_timestamp = strtotime( $recurring_event_date_start );
408
409
						} else {
410
411
							$event_start_timestamp = strtotime( $recurring_event_date_start );
412
							$start_time = date( 'His', $event_start_timestamp );
413
							$event_end_timestamp = $event_start_timestamp + $duration;
414
							$end_time = date( 'His', $event_end_timestamp );
415 View Code Duplication
							if ( 8 == strlen( $event['DTSTART'] ) ) {
416
								$exdate_compare = date( 'Ymd', $event_start_timestamp );
417
							} else {
418
								$exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
419
							}
420
421 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 ) ) {
422
								if ( 8 == strlen( $event['DTSTART'] ) ) {
423
									$event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
424
									$event['DTEND'] = date( 'Ymd', $event_end_timestamp );
425
								} else {
426
									$event['DTSTART'] = date( 'Ymd\T', $event_start_timestamp ) . $start_time;
427
									$event['DTEND'] = date( 'Ymd\T', $event_end_timestamp ) . $end_time;
428
								}
429
								if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
430
									try {
431
										$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
432
										$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
433
										$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
434
435
										$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
436
									} catch ( Exception $e ) {
437
										// Invalid argument to DateTime
438
									}
439
								}
440
								$upcoming[] = $event;
441
								$count_counter++;
442
							}
443
						}
444
445
						// Set up next interval and reset $event['DTSTART'] and $event['DTEND'], keeping timestamps intact
446
						$next_start_timestamp = strtotime( "+ {$interval} {$frequency}s", $event_start_timestamp );
447
						if ( 8 == strlen( $event['DTSTART'] ) ) {
448
							$event['DTSTART'] = date( 'Ymd', $next_start_timestamp );
449
							$event['DTEND'] = date( 'Ymd', strtotime( $event['DTSTART'] ) + $duration );
450 View Code Duplication
						} else {
451
							$event['DTSTART'] = date( 'Ymd\THis', $next_start_timestamp );
452
							$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
453
						}
454
455
						// Move recurring event date forward
456
						$recurring_event_date_start = $event['DTSTART'];
457
					}
458
					$set_recurring_events[] = $uid;
459
460
				}
461
462 View Code Duplication
			} else {
463
				// Process normal events
464
				if ( strtotime( isset( $event['DTEND'] ) ? $event['DTEND'] : $event['DTSTART'] ) >= $current ) {
465
					$upcoming[] = $event;
466
				}
467
			}
468
		}
469
		return $upcoming;
470
	}
471
472
	/**
473
	 * Parse events from an iCalendar feed
474
	 *
475
	 * @param string $url (default: '')
476
	 * @return array | false on failure
477
	 */
478
	public function parse( $url = '' ) {
479
		$cache_group = 'icalendar_reader_parse';
480
		$disable_get_key = 'disable:' . md5( $url );
481
482
		// Check to see if previous attempts have failed
483
		if ( false !== wp_cache_get( $disable_get_key, $cache_group ) )
484
			return false;
485
486
		// rewrite webcal: URI schem to HTTP
487
		$url = preg_replace('/^webcal/', 'http', $url );
488
		// try to fetch
489
		$r = wp_remote_get( $url, array( 'timeout' => 3, 'sslverify' => false ) );
490
		if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
491
			// We were unable to fetch any content, so don't try again for another 60 seconds
492
			wp_cache_set( $disable_get_key, 1, $cache_group, 60 );
493
			return false;
494
		}
495
496
		$body = wp_remote_retrieve_body( $r );
497
		if ( empty( $body ) )
498
			return false;
499
500
		$body = str_replace( "\r\n", "\n", $body );
501
		$lines = preg_split( "/\n(?=[A-Z])/", $body );
502
503
		if ( empty( $lines ) )
504
			return false;
505
506
		if ( false === stristr( $lines[0], 'BEGIN:VCALENDAR' ) )
507
			return false;
508
509
		foreach ( $lines as $line ) {
510
			$add  = $this->key_value_from_string( $line );
511
			if ( ! $add ) {
512
				$this->add_component( $type, false, $line );
513
				continue;
514
			}
515
			list( $keyword, $value ) = $add;
516
517
			switch ( $keyword ) {
518
				case 'BEGIN':
519
				case 'END':
520
					switch ( $line ) {
521
						case 'BEGIN:VTODO':
522
							$this->todo_count++;
523
							$type = 'VTODO';
524
							break;
525
						case 'BEGIN:VEVENT':
526
							$this->event_count++;
527
							$type = 'VEVENT';
528
							break;
529
						case 'BEGIN:VCALENDAR':
530
						case 'BEGIN:DAYLIGHT':
531
						case 'BEGIN:VTIMEZONE':
532
						case 'BEGIN:STANDARD':
533
							$type = $value;
534
							break;
535
						case 'END:VTODO':
536
						case 'END:VEVENT':
537
						case 'END:VCALENDAR':
538
						case 'END:DAYLIGHT':
539
						case 'END:VTIMEZONE':
540
						case 'END:STANDARD':
541
							$type = 'VCALENDAR';
542
							break;
543
					}
544
					break;
545
				case 'TZID':
546
					if ( 'VTIMEZONE' == $type && ! $this->timezone )
547
						$this->timezone = $this->timezone_from_string( $value );
548
					break;
549
				case 'X-WR-TIMEZONE':
550
					if ( ! $this->timezone )
551
						$this->timezone = $this->timezone_from_string( $value );
552
					break;
553
				default:
554
					$this->add_component( $type, $keyword, $value );
555
					break;
556
			}
557
		}
558
559
		// Filter for RECURRENCE-IDs
560
		$recurrences = array();
561
		if ( array_key_exists( 'VEVENT', $this->cal ) ) {
562
			foreach ( $this->cal['VEVENT'] as $event ) {
563
				if ( isset( $event['RECURRENCE-ID'] ) ) {
564
					$recurrences[] = $event;
565
				}
566
			}
567
			foreach ( $recurrences as $recurrence ) {
568
				for ( $i = 0; $i < count( $this->cal['VEVENT'] ); $i++ ) {
569
					if ( $this->cal['VEVENT'][ $i ]['UID'] == $recurrence['UID'] && ! isset( $this->cal['VEVENT'][ $i ]['RECURRENCE-ID'] ) ) {
570
						$this->cal['VEVENT'][ $i ]['EXDATE'][] = $recurrence['RECURRENCE-ID'];
571
						break;
572
					}
573
				}
574
			}
575
		}
576
577
		return $this->cal;
578
	}
579
580
	/**
581
	 * Parse key:value from a string
582
	 *
583
	 * @param string $text (default: '')
584
	 * @return array
585
	 */
586
	public function key_value_from_string( $text = '' ) {
587
		preg_match( '/([^:]+)(;[^:]+)?[:]([\w\W]*)/', $text, $matches );
588
589
		if ( 0 == count( $matches ) )
590
			return false;
591
592
		return array( $matches[1], $matches[3] );
593
	}
594
595
	/**
596
	 * Convert a timezone name into a timezone object.
597
	 *
598
	 * @param string $text Timezone name. Example: America/Chicago
599
	 * @return object|null A DateTimeZone object if the conversion was successful.
600
	 */
601
	private function timezone_from_string( $text ) {
602
		try {
603
			$timezone = new DateTimeZone( $text );
604
		} catch ( Exception $e ) {
605
			$blog_timezone = get_option( 'timezone_string' );
606
			if ( ! $blog_timezone ) {
607
				$blog_timezone = 'Etc/UTC';
608
			}
609
610
			$timezone = new DateTimeZone( $blog_timezone );
611
		}
612
613
		return $timezone;
614
	}
615
616
	/**
617
	 * Add a component to the calendar array
618
	 *
619
	 * @param string $component (default: '')
620
	 * @param string $keyword (default: '')
621
	 * @param string $value (default: '')
622
	 * @return void
623
	 */
624
	public function add_component( $component = '', $keyword = '', $value = '' ) {
625
		if ( false == $keyword ) {
626
			$keyword = $this->last_keyword;
627
			switch ( $component ) {
628
			case 'VEVENT':
629
				$value = $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] . $value;
630
				break;
631
			case 'VTODO' :
632
				$value = $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] . $value;
633
				break;
634
			}
635
		}
636
637
		/*
638
		 * Some events have a specific timezone set in their start/end date,
639
		 * and it may or may not be different than the calendar timzeone.
640
		 * Valid formats include:
641
		 * DTSTART;TZID=Pacific Standard Time:20141219T180000
642
		 * DTEND;TZID=Pacific Standard Time:20141219T200000
643
		 * EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
644
		 * EXDATE;VALUE=DATE:2015050
645
		 * EXDATE;TZID=America/New_York:20150424T170000
646
		 * EXDATE;TZID=Pacific Standard Time:20120615T140000,20120629T140000,20120706T140000
647
		 */
648
649
		// Always store EXDATE as an array
650
		if ( stristr( $keyword, 'EXDATE' ) ) {
651
			$value = explode( ',', $value );
652
		}
653
654
		// Adjust DTSTART, DTEND, and EXDATE according to their TZID if set
655
		if ( strpos( $keyword, ';' ) && ( stristr( $keyword, 'DTSTART' ) || stristr( $keyword, 'DTEND' ) || stristr( $keyword, 'EXDATE' ) || stristr( $keyword, 'RECURRENCE-ID' ) ) ) {
656
			$keyword = explode( ';', $keyword );
657
658
			$tzid = false;
659
			if ( 2 == count( $keyword ) ) {
660
				$tparam = $keyword[1];
661
662
				if ( strpos( $tparam, "TZID" ) !== false ) {
663
					$tzid = $this->timezone_from_string( str_replace( 'TZID=', '', $tparam ) );
664
				}
665
			}
666
667
			// Normalize all times to default UTC
668
			if ( $tzid ) {
669
				$adjusted_times = array();
670
				foreach ( (array) $value as $v ) {
671
					try {
672
						$adjusted_time = new DateTime( $v, $tzid );
673
						$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
674
						$adjusted_times[] = $adjusted_time->format('Ymd\THis');
675
					} catch ( Exception $e ) {
676
						// Invalid argument to DateTime
677
						return;
678
					}
679
				}
680
				$value = $adjusted_times;
681
			}
682
683
			// Format for adding to event
684
			$keyword = $keyword[0];
685
			if ( 'EXDATE' != $keyword ) {
686
				$value = implode( (array) $value );
687
			}
688
		}
689
690
		foreach ( (array) $value as $v ) {
691
			switch ($component) {
692 View Code Duplication
				case 'VTODO':
693
					if ( 'EXDATE' == $keyword ) {
694
						$this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ][] = $v;
695
					} else {
696
						$this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] = $v;
697
					}
698
					break;
699 View Code Duplication
				case 'VEVENT':
700
					if ( 'EXDATE' == $keyword ) {
701
						$this->cal[ $component ][ $this->event_count - 1 ][ $keyword ][] = $v;
702
					} else {
703
						$this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] = $v;
704
					}
705
					break;
706
				default:
707
					$this->cal[ $component ][ $keyword ] = $v;
708
					break;
709
			}
710
		}
711
		$this->last_keyword = $keyword;
712
	}
713
714
	/**
715
	 * Escape strings with wp_kses, allow links
716
	 *
717
	 * @param string $string (default: '')
718
	 * @return string
719
	 */
720
	public function escape( $string = '' ) {
721
		// Unfold content lines per RFC 5545
722
		$string = str_replace( "\n\t", '', $string );
723
		$string = str_replace( "\n ", '', $string );
724
725
		$allowed_html = array(
726
			'a' => array(
727
				'href'  => array(),
728
				'title' => array()
729
			)
730
		);
731
732
		$allowed_tags = '';
733
		foreach ( array_keys( $allowed_html ) as $tag ) {
734
			$allowed_tags .= "<{$tag}>";
735
		}
736
737
		// Running strip_tags() first with allowed tags to get rid of remaining gallery markup, etc
738
		// because wp_kses() would only htmlentity'fy that. Then still running wp_kses(), for extra
739
		// safety and good measure.
740
		return wp_kses( strip_tags( $string, $allowed_tags ), $allowed_html );
741
	}
742
743
	/**
744
	 * Render the events
745
	 *
746
	 * @param string $url (default: '')
747
	 * @param string $context (default: 'widget') or 'shortcode'
748
	 * @return mixed bool|string false on failure, rendered HTML string on success.
749
	 */
750
	public function render( $url = '', $args = array() ) {
751
752
		$args = wp_parse_args( $args, array(
753
			'context' => 'widget',
754
			'number' => 5
755
		) );
756
757
		$events = $this->get_events( $url, $args['number'] );
758
		$events = $this->apply_timezone_offset( $events );
759
760
		if ( empty( $events ) )
761
			return false;
762
763
		ob_start();
764
765
		if ( 'widget' == $args['context'] ) : ?>
766
		<ul class="upcoming-events">
767
			<?php foreach ( $events as $event ) : ?>
768
			<li>
769
				<strong class="event-summary"><?php echo $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></strong>
770
				<span class="event-when"><?php echo $this->formatted_date( $event ); ?></span>
771 View Code Duplication
				<?php if ( ! empty( $event['LOCATION'] ) ) : ?>
772
					<span class="event-location"><?php echo $this->escape( stripslashes( $event['LOCATION'] ) ); ?></span>
773
				<?php endif; ?>
774 View Code Duplication
				<?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?>
775
					<span class="event-description"><?php echo wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></span>
776
				<?php endif; ?>
777
			</li>
778
			<?php endforeach; ?>
779
		</ul>
780
		<?php endif;
781
782
		if ( 'shortcode' == $args['context'] ) : ?>
783
		<table class="upcoming-events">
784
			<thead>
785
				<tr>
786
					<th><?php esc_html_e( 'Location', 'jetpack' ); ?></th>
787
					<th><?php esc_html_e( 'When', 'jetpack' ); ?></th>
788
					<th><?php esc_html_e( 'Summary', 'jetpack' ); ?></th>
789
					<th><?php esc_html_e( 'Description', 'jetpack' ); ?></th>
790
				</tr>
791
			</thead>
792
			<tbody>
793
			<?php foreach ( $events as $event ) : ?>
794
				<tr>
795
					<td><?php echo empty( $event['LOCATION'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['LOCATION'] ) ); ?></td>
796
					<td><?php echo $this->formatted_date( $event ); ?></td>
797
					<td><?php echo empty( $event['SUMMARY'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></td>
798
					<td><?php echo empty( $event['DESCRIPTION'] ) ? '&nbsp;' : wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></td>
799
				</tr>
800
			<?php endforeach; ?>
801
			</tbody>
802
		</table>
803
		<?php endif;
804
805
		$rendered = ob_get_clean();
806
807
		if ( empty( $rendered ) )
808
			return false;
809
810
		return $rendered;
811
	}
812
813
	public function formatted_date( $event ) {
814
815
		$date_format = get_option( 'date_format' );
816
		$time_format = get_option( 'time_format' );
817
		$start = strtotime( $event['DTSTART'] );
818
		$end = isset( $event['DTEND'] ) ? strtotime( $event['DTEND'] ) : false;
819
820
		$all_day = ( 8 == strlen( $event['DTSTART'] ) );
821
822
		if ( !$all_day && $this->timezone ) {
823
			try {
824
				$start_time = new DateTime( $event['DTSTART'] );
825
				$timezone_offset = $this->timezone->getOffset( $start_time );
826
				$start += $timezone_offset;
827
828
				if ( $end ) {
829
					$end += $timezone_offset;
830
				}
831
			} catch ( Exception $e ) {
832
				// Invalid argument to DateTime
833
			}
834
		}
835
		$single_day = $end ? ( $end - $start ) <= DAY_IN_SECONDS : true;
836
837
		/* Translators: Date and time */
838
		$date_with_time = __( '%1$s at %2$s' , 'jetpack' );
839
		/* Translators: Two dates with a separator */
840
		$two_dates = __( '%1$s &ndash; %2$s' , 'jetpack' );
841
842
		// we'll always have the start date. Maybe with time
843 View Code Duplication
		if ( $all_day )
844
			$date = date_i18n( $date_format, $start );
845
		else
846
			$date = sprintf( $date_with_time, date_i18n( $date_format, $start ), date_i18n( $time_format, $start ) );
847
848
		// single day, timed
849
		if ( $single_day && ! $all_day && false !== $end )
850
			$date = sprintf( $two_dates, $date, date_i18n( $time_format, $end ) );
851
852
		// multi-day
853
		if ( ! $single_day ) {
854
855 View Code Duplication
			if ( $all_day ) {
856
				// DTEND for multi-day events represents "until", not "including", so subtract one minute
857
				$end_date = date_i18n( $date_format, $end - 60 );
858
			} else {
859
				$end_date = sprintf( $date_with_time, date_i18n( $date_format, $end ), date_i18n( $time_format, $end ) );
860
			}
861
862
			$date = sprintf( $two_dates, $date, $end_date );
863
864
		}
865
866
		return $date;
867
	}
868
869
	protected function sort_by_recent( $list ) {
870
		$dates = $sorted_list = array();
871
872
		foreach ( $list as $key => $row ) {
873
			$date = $row['DTSTART'];
874
			// pad some time onto an all day date
875
			if ( 8 === strlen( $date ) )
876
				$date .= 'T000000Z';
877
			$dates[$key] = $date;
878
		}
879
		asort( $dates );
880
		foreach( $dates as $key => $value ) {
881
			$sorted_list[$key] = $list[$key];
882
		}
883
		unset($list);
884
		return $sorted_list;
885
	}
886
887
}
888
889
890
/**
891
 * Wrapper function for iCalendarReader->get_events()
892
 *
893
 * @param string $url (default: '')
894
 * @return array
895
 */
896
function icalendar_get_events( $url = '', $count = 5 ) {
897
	// Find your calendar's address http://support.google.com/calendar/bin/answer.py?hl=en&answer=37103
898
	$ical = new iCalendarReader();
899
	return $ical->get_events( $url, $count );
900
}
901
902
/**
903
 * Wrapper function for iCalendarReader->render()
904
 *
905
 * @param string $url (default: '')
906
 * @param string $context (default: 'widget') or 'shortcode'
907
 * @return mixed bool|string false on failure, rendered HTML string on success.
908
 */
909
function icalendar_render_events( $url = '', $args = array() ) {
910
	$ical = new iCalendarReader();
911
	return $ical->render( $url, $args );
912
}
913