Completed
Push — update/date-time ( 50c4ab )
by
unknown
06:46
created

iCalendarReader::apply_timezone_offset()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 1
dl 0
loc 30
rs 9.44
c 0
b 0
f 0
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
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
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();
0 ignored issues
show
Unused Code introduced by
$recurrences is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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'] ) );
0 ignored issues
show
Bug introduced by
The variable $recurring_event_date_start 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...
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'] ) );
0 ignored issues
show
Bug introduced by
The variable $catchup 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...
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 );
0 ignored issues
show
Unused Code introduced by
$end_time is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
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 );
0 ignored issues
show
Bug introduced by
The variable $type 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...
Documentation introduced by
false is of type boolean, 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...
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++ ) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $keyword of type string to the boolean false. If you are specifically checking for an empty string, consider using the more explicit === '' instead.
Loading history...
612
			$keyword = $this->last_keyword;
0 ignored issues
show
Bug introduced by
The property last_keyword does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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'
0 ignored issues
show
Bug introduced by
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...
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(
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 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $end of type integer|false is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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'
0 ignored issues
show
Bug introduced by
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...
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