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

iCalendarReader   D

Complexity

Total Complexity 183

Size/Duplication

Total Lines 882
Duplicated Lines 14.29 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 126
loc 882
rs 4.4444
c 0
b 0
f 0
wmc 183
lcom 1
cbo 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 1 1
C get_events() 0 58 10
B apply_timezone_offset() 0 45 6
F filter_past_and_recurring_events() 96 339 75
D parse() 0 101 34
A key_value_from_string() 0 8 2
A timezone_from_string() 0 14 3
F add_component() 14 89 21
A escape() 0 22 2
C render() 6 62 12
C formatted_date() 10 55 13
A sort_by_recent() 0 17 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like iCalendarReader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use iCalendarReader, and based on these observations, apply Extract Interface, too.

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_name = get_option( 'timezone_string' );
94
		if ( $timezone_name ) {
95
			$timezone = new DateTimeZone( $timezone_name );
96
			$timezone_offset_interval = false;
97
		} else {
98
			// If the timezone isn't set then the GMT offset must be set.
99
			// generate a DateInterval object from the timezone offset
100
			$gmt_offset = get_option( 'gmt_offset' ) * HOUR_IN_SECONDS;
101
			$timezone_offset_interval = date_interval_create_from_date_string( "{$gmt_offset} seconds" );
102
			$timezone = new DateTimeZone( 'UTC' );
103
		}
104
105
		$offsetted_events = array();
106
107
		foreach ( $events as $event ) {
108
			// Don't handle all-day events
109
			if ( 8 < strlen( $event['DTSTART'] ) ) {
110
				$start_time = preg_replace( '/Z$/', '', $event['DTSTART'] );
111
				$start_time = new DateTime( $start_time, $this->timezone );
112
				$start_time->setTimeZone( $timezone );
113
114
				$end_time = preg_replace( '/Z$/', '', $event['DTEND'] );
115
				$end_time = new DateTime( $end_time, $this->timezone );
116
				$end_time->setTimeZone( $timezone );
117
118
				if ( $timezone_offset_interval ) {
119
					$start_time->add( $timezone_offset_interval );
120
					$end_time->add( $timezone_offset_interval );
121
				}
122
123
				$event['DTSTART'] = $start_time->format( 'YmdHis\Z' );
124
				$event['DTEND'] = $end_time->format( 'YmdHis\Z' );
125
			}
126
127
			$offsetted_events[] = $event;
128
		}
129
130
		return $offsetted_events;
131
	}
132
133
	protected function filter_past_and_recurring_events( $events ) {
134
		$upcoming = array();
135
		$set_recurring_events = array();
136
		$recurrences = array();
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...
137
		/**
138
		 * This filter allows any time to be passed in for testing or changing timezones, etc...
139
		 *
140
		 * @module widgets
141
		 *
142
		 * @since 3.4.0
143
		 *
144
		 * @param object time() A time object.
145
		 */
146
		$current = apply_filters( 'ical_get_current_time', time() );
147
148
		foreach ( $events as $event ) {
149
150
			$date_from_ics = strtotime( $event['DTSTART'] );
151 View Code Duplication
			if ( isset( $event['DTEND'] ) ) {
152
				$duration = strtotime( $event['DTEND'] ) - strtotime( $event['DTSTART'] );
153
			} else {
154
				$duration = 0;
155
			}
156
157
			if ( isset( $event['RRULE'] ) && $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
158
				try {
159
					$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone('UTC') );
160
					$adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
161
					$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
162
					$date_from_ics = strtotime( $event['DTSTART'] );
163
164
					$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
165
				} catch ( Exception $e ) {
166
					// Invalid argument to DateTime
167
				}
168
169
				if ( isset( $event['EXDATE'] ) ) {
170
					$exdates = array();
171
					foreach ( (array) $event['EXDATE'] as $exdate ) {
172
						try {
173
							$adjusted_time = new DateTime( $exdate, new DateTimeZone('UTC') );
174
							$adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
175
							if ( 8 == strlen( $event['DTSTART'] ) ) {
176
								$exdates[] = $adjusted_time->format( 'Ymd' );
177
							} else {
178
								$exdates[] = $adjusted_time->format( 'Ymd\THis' );
179
							}
180
						} catch ( Exception $e ) {
181
							// Invalid argument to DateTime
182
						}
183
					}
184
					$event['EXDATE'] = $exdates;
185
				} else {
186
					$event['EXDATE'] = array();
187
				}
188
			}
189
190
			if ( ! isset( $event['DTSTART'] ) ) {
191
				continue;
192
			}
193
194
			// Process events with RRULE before other events
195
			$rrule = isset( $event['RRULE'] ) ? $event['RRULE'] : false ;
196
			$uid = $event['UID'];
197
198
			if ( $rrule && ! in_array( $uid, $set_recurring_events ) ) {
199
200
				// Break down the RRULE into digestible chunks
201
				$rrule_array = array();
202
203
				foreach ( explode( ";", $event['RRULE'] ) as $rline ) {
204
					list( $rkey, $rvalue ) = explode( "=", $rline, 2 );
205
					$rrule_array[$rkey] = $rvalue;
206
				}
207
208
				$interval = ( isset( $rrule_array['INTERVAL'] ) ) ? $rrule_array['INTERVAL'] : 1;
209
				$rrule_count = ( isset( $rrule_array['COUNT'] ) ) ? $rrule_array['COUNT'] : 0;
210
				$until = ( isset( $rrule_array['UNTIL'] ) ) ? strtotime( $rrule_array['UNTIL'] ) : strtotime( '+1 year', $current );
211
212
				// Used to bound event checks
213
				$echo_limit = 10;
214
				$noop = false;
215
216
				// Set bydays for the event
217
				$weekdays = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' );
218
				$bydays = $weekdays;
219
220
				// Calculate a recent start date for incrementing depending on the frequency and interval
221
				switch ( $rrule_array['FREQ'] ) {
222
223
					case 'DAILY':
224
						$frequency = 'day';
225
						$echo_limit = 10;
226
227
						if ( $date_from_ics >= $current ) {
228
							$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
229
						} else {
230
							// Interval and count
231
							$catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * DAY_IN_SECONDS ) );
232
							if ( $rrule_count && $catchup > 0 ) {
233
								if ( $catchup < $rrule_count ) {
234
									$rrule_count = $rrule_count - $catchup;
235
									$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
236
								} else {
237
									$noop = true;
238
								}
239 View Code Duplication
							} else {
240
								$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' days', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
241
							}
242
						}
243
						break;
244
245
					case 'WEEKLY':
246
						$frequency = 'week';
247
						$echo_limit = 4;
248
249
						// BYDAY exception to current date
250
						$day = false;
251
						if ( ! isset( $rrule_array['BYDAY'] ) ) {
252
							$day = $rrule_array['BYDAY'] = strtoupper( substr( date( 'D', strtotime( $event['DTSTART'] ) ), 0, 2 ) );
253
						}
254
						$bydays = explode( ',', $rrule_array['BYDAY'] );
255
256
						if ( $date_from_ics >= $current ) {
257
							$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
258
						} else {
259
							// Interval and count
260
							$catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * WEEK_IN_SECONDS ) );
261
							if ( $rrule_count && $catchup > 0 ) {
262
								if ( ( $catchup * count( $bydays ) ) < $rrule_count ) {
263
									$rrule_count = $rrule_count - ( $catchup * count( $bydays ) ); // Estimate current event count
264
									$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
265
								} else {
266
									$noop = true;
267
								}
268 View Code Duplication
							} else {
269
								$recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
270
							}
271
						}
272
273
						// Set to Sunday start
274
						if ( ! $noop && 'SU' !== strtoupper( substr( date( 'D', strtotime( $recurring_event_date_start ) ), 0, 2 ) ) ) {
275
							$recurring_event_date_start = date( 'Ymd', strtotime( "last Sunday", strtotime( $recurring_event_date_start ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
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...
276
						}
277
						break;
278
279
					case 'MONTHLY':
280
						$frequency = 'month';
281
						$echo_limit = 1;
282
283
						if ( $date_from_ics >= $current ) {
284
							$recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
285
						} else {
286
							// Describe the date in the month
287
							if ( isset( $rrule_array['BYDAY'] ) ) {
288
								$day_number = substr( $rrule_array['BYDAY'], 0, 1 );
289
								$week_day = substr( $rrule_array['BYDAY'], 1 );
290
								$day_cardinals = array( 1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth' );
291
								$weekdays = array( 'SU' => 'Sunday', 'MO' => 'Monday', 'TU' => 'Tuesday', 'WE' => 'Wednesday', 'TH' => 'Thursday', 'FR' => 'Friday', 'SA' => 'Saturday' );
292
								$event_date_desc = "{$day_cardinals[$day_number]} {$weekdays[$week_day]} of ";
293
							} else {
294
								$event_date_desc = date( 'd ', strtotime( $event['DTSTART'] ) );
295
							}
296
297
							// Interval only
298
							if ( $interval > 1 ) {
299
								$catchup = 0;
300
								$maybe = strtotime( $event['DTSTART'] );
301
								while ( $maybe < $current ) {
302
									$maybe = strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) );
303
									$catchup++;
304
								}
305
								$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * ( $catchup - 1 ) ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
306 View Code Duplication
							} else {
307
								$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', $current ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
308
							}
309
310
							// Add one interval if necessary
311
							if ( strtotime( $recurring_event_date_start ) < $current ) {
312
								if ( $interval > 1 ) {
313
									$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) ) ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
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...
314 View Code Duplication
								} else {
315
									try {
316
										$adjustment = new DateTime( date( 'Y-m-d', $current ) );
317
										$adjustment->modify( 'first day of next month' );
318
										$recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . $adjustment->format( 'F Y' ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
319
									} catch ( Exception $e ) {
320
										// Invalid argument to DateTime
321
									}
322
								}
323
							}
324
						}
325
						break;
326
327
					case 'YEARLY':
328
						$frequency = 'year';
329
						$echo_limit = 1;
330
331
						if ( $date_from_ics >= $current ) {
332
							$recurring_event_date_start = date( "Ymd\THis", strtotime( $event['DTSTART'] ) );
333
						} else {
334
							$recurring_event_date_start = date( 'Y', $current ) . date( "md\THis", strtotime( $event['DTSTART'] ) );
335 View Code Duplication
							if ( strtotime( $recurring_event_date_start ) < $current ) {
336
								try {
337
									$next = new DateTime( date( 'Y-m-d', $current ) );
338
									$next->modify( 'first day of next year' );
339
									$recurring_event_date_start = $next->format( 'Y' ) . date ( 'md\THis', strtotime( $event['DTSTART'] ) );
340
								} catch ( Exception $e ) {
341
									// Invalid argument to DateTime
342
								}
343
							}
344
						}
345
						break;
346
347
					default:
348
						$frequency = false;
349
				}
350
351
				if ( $frequency !== false && ! $noop ) {
352
					$count_counter = 1;
353
354
					// If no COUNT limit, go to 10
355
					if ( empty( $rrule_count ) ) {
356
						$rrule_count = 10;
357
					}
358
359
					// Set up EXDATE handling for the event
360
					$exdates = ( isset( $event['EXDATE'] ) ) ? $event['EXDATE'] : array();
361
362
					for ( $i = 1; $i <= $echo_limit; $i++ ) {
363
364
						// Weeks need a daily loop and must check for inclusion in BYDAYS
365
						if ( 'week' == $frequency ) {
366
							$byday_event_date_start = strtotime( $recurring_event_date_start );
367
368
							foreach ( $weekdays as $day ) {
369
370
								$event_start_timestamp = $byday_event_date_start;
371
								$start_time = date( 'His', $event_start_timestamp );
372
								$event_end_timestamp = $event_start_timestamp + $duration;
373
								$end_time = date( 'His', $event_end_timestamp );
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...
374 View Code Duplication
								if ( 8 == strlen( $event['DTSTART'] ) ) {
375
									$exdate_compare = date( 'Ymd', $event_start_timestamp );
376
								} else {
377
									$exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
378
								}
379
380 View Code Duplication
								if ( in_array( $day, $bydays ) && $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) {
381
									if ( 8 == strlen( $event['DTSTART'] ) ) {
382
										$event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
383
										$event['DTEND'] = date( 'Ymd', $event_end_timestamp );
384
									} else {
385
										$event['DTSTART'] = date( 'Ymd\THis', $event_start_timestamp );
386
										$event['DTEND'] = date( 'Ymd\THis', $event_end_timestamp );
387
									}
388
									if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
389
										try {
390
											$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
391
											$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
392
											$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
393
394
											$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
395
										} catch ( Exception $e ) {
396
											// Invalid argument to DateTime
397
										}
398
									}
399
									$upcoming[] = $event;
400
									$count_counter++;
401
								}
402
403
								// Move forward one day
404
								$byday_event_date_start = strtotime( date( 'Ymd\T', strtotime( '+ 1 day', $event_start_timestamp ) ) . $start_time );
405
							}
406
407
							// Restore first event timestamp
408
							$event_start_timestamp = strtotime( $recurring_event_date_start );
409
410
						} else {
411
412
							$event_start_timestamp = strtotime( $recurring_event_date_start );
413
							$start_time = date( 'His', $event_start_timestamp );
414
							$event_end_timestamp = $event_start_timestamp + $duration;
415
							$end_time = date( 'His', $event_end_timestamp );
416 View Code Duplication
							if ( 8 == strlen( $event['DTSTART'] ) ) {
417
								$exdate_compare = date( 'Ymd', $event_start_timestamp );
418
							} else {
419
								$exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
420
							}
421
422 View Code Duplication
							if ( $event_end_timestamp > $current && $event_start_timestamp < $until && $count_counter <= $rrule_count && $event_start_timestamp >= $date_from_ics && ! in_array( $exdate_compare, $exdates ) ) {
423
								if ( 8 == strlen( $event['DTSTART'] ) ) {
424
									$event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
425
									$event['DTEND'] = date( 'Ymd', $event_end_timestamp );
426
								} else {
427
									$event['DTSTART'] = date( 'Ymd\T', $event_start_timestamp ) . $start_time;
428
									$event['DTEND'] = date( 'Ymd\T', $event_end_timestamp ) . $end_time;
429
								}
430
								if ( $this->timezone->getName() && 8 != strlen( $event['DTSTART'] ) ) {
431
									try {
432
										$adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
433
										$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
434
										$event['DTSTART'] = $adjusted_time->format('Ymd\THis');
435
436
										$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
437
									} catch ( Exception $e ) {
438
										// Invalid argument to DateTime
439
									}
440
								}
441
								$upcoming[] = $event;
442
								$count_counter++;
443
							}
444
						}
445
446
						// Set up next interval and reset $event['DTSTART'] and $event['DTEND'], keeping timestamps intact
447
						$next_start_timestamp = strtotime( "+ {$interval} {$frequency}s", $event_start_timestamp );
448
						if ( 8 == strlen( $event['DTSTART'] ) ) {
449
							$event['DTSTART'] = date( 'Ymd', $next_start_timestamp );
450
							$event['DTEND'] = date( 'Ymd', strtotime( $event['DTSTART'] ) + $duration );
451 View Code Duplication
						} else {
452
							$event['DTSTART'] = date( 'Ymd\THis', $next_start_timestamp );
453
							$event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
454
						}
455
456
						// Move recurring event date forward
457
						$recurring_event_date_start = $event['DTSTART'];
458
					}
459
					$set_recurring_events[] = $uid;
460
461
				}
462
463 View Code Duplication
			} else {
464
				// Process normal events
465
				if ( strtotime( isset( $event['DTEND'] ) ? $event['DTEND'] : $event['DTSTART'] ) >= $current ) {
466
					$upcoming[] = $event;
467
				}
468
			}
469
		}
470
		return $upcoming;
471
	}
472
473
	/**
474
	 * Parse events from an iCalendar feed
475
	 *
476
	 * @param string $url (default: '')
477
	 * @return array | false on failure
478
	 */
479
	public function parse( $url = '' ) {
480
		$cache_group = 'icalendar_reader_parse';
481
		$disable_get_key = 'disable:' . md5( $url );
482
483
		// Check to see if previous attempts have failed
484
		if ( false !== wp_cache_get( $disable_get_key, $cache_group ) )
485
			return false;
486
487
		// rewrite webcal: URI schem to HTTP
488
		$url = preg_replace('/^webcal/', 'http', $url );
489
		// try to fetch
490
		$r = wp_remote_get( $url, array( 'timeout' => 3, 'sslverify' => false ) );
491
		if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
492
			// We were unable to fetch any content, so don't try again for another 60 seconds
493
			wp_cache_set( $disable_get_key, 1, $cache_group, 60 );
494
			return false;
495
		}
496
497
		$body = wp_remote_retrieve_body( $r );
498
		if ( empty( $body ) )
499
			return false;
500
501
		$body = str_replace( "\r\n", "\n", $body );
502
		$lines = preg_split( "/\n(?=[A-Z])/", $body );
503
504
		if ( empty( $lines ) )
505
			return false;
506
507
		if ( false === stristr( $lines[0], 'BEGIN:VCALENDAR' ) )
508
			return false;
509
510
		foreach ( $lines as $line ) {
511
			$add  = $this->key_value_from_string( $line );
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned correctly; expected 1 space but found 2 spaces

This check looks for improperly formatted assignments.

Every assignment must have exactly one space before and one space after the equals operator.

To illustrate:

$a = "a";
$ab = "ab";
$abc = "abc";

will have no issues, while

$a   = "a";
$ab  = "ab";
$abc = "abc";

will report issues in lines 1 and 2.

Loading history...
512
			if ( ! $add ) {
513
				$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...
514
				continue;
515
			}
516
			list( $keyword, $value ) = $add;
517
518
			switch ( $keyword ) {
519
				case 'BEGIN':
520
				case 'END':
521
					switch ( $line ) {
522
						case 'BEGIN:VTODO':
523
							$this->todo_count++;
524
							$type = 'VTODO';
525
							break;
526
						case 'BEGIN:VEVENT':
527
							$this->event_count++;
528
							$type = 'VEVENT';
529
							break;
530
						case 'BEGIN:VCALENDAR':
531
						case 'BEGIN:DAYLIGHT':
532
						case 'BEGIN:VTIMEZONE':
533
						case 'BEGIN:STANDARD':
534
							$type = $value;
535
							break;
536
						case 'END:VTODO':
537
						case 'END:VEVENT':
538
						case 'END:VCALENDAR':
539
						case 'END:DAYLIGHT':
540
						case 'END:VTIMEZONE':
541
						case 'END:STANDARD':
542
							$type = 'VCALENDAR';
543
							break;
544
					}
545
					break;
546
				case 'TZID':
547
					if ( 'VTIMEZONE' == $type && ! $this->timezone )
548
						$this->timezone = $this->timezone_from_string( $value );
549
					break;
550
				case 'X-WR-TIMEZONE':
551
					if ( ! $this->timezone )
552
						$this->timezone = $this->timezone_from_string( $value );
553
					break;
554
				default:
555
					$this->add_component( $type, $keyword, $value );
556
					break;
557
			}
558
		}
559
560
		// Filter for RECURRENCE-IDs
561
		$recurrences = array();
562
		if ( array_key_exists( 'VEVENT', $this->cal ) ) {
563
			foreach ( $this->cal['VEVENT'] as $event ) {
564
				if ( isset( $event['RECURRENCE-ID'] ) ) {
565
					$recurrences[] = $event;
566
				}
567
			}
568
			foreach ( $recurrences as $recurrence ) {
569
				for ( $i = 0; $i < count( $this->cal['VEVENT'] ); $i++ ) {
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...
570
					if ( $this->cal['VEVENT'][ $i ]['UID'] == $recurrence['UID'] && ! isset( $this->cal['VEVENT'][ $i ]['RECURRENCE-ID'] ) ) {
571
						$this->cal['VEVENT'][ $i ]['EXDATE'][] = $recurrence['RECURRENCE-ID'];
572
						break;
573
					}
574
				}
575
			}
576
		}
577
578
		return $this->cal;
579
	}
580
581
	/**
582
	 * Parse key:value from a string
583
	 *
584
	 * @param string $text (default: '')
585
	 * @return array
586
	 */
587
	public function key_value_from_string( $text = '' ) {
588
		preg_match( '/([^:]+)(;[^:]+)?[:]([\w\W]*)/', $text, $matches );
589
590
		if ( 0 == count( $matches ) )
591
			return false;
592
593
		return array( $matches[1], $matches[3] );
594
	}
595
596
	/**
597
	 * Convert a timezone name into a timezone object.
598
	 *
599
	 * @param string $text Timezone name. Example: America/Chicago
600
	 * @return object|null A DateTimeZone object if the conversion was successful.
601
	 */
602
	private function timezone_from_string( $text ) {
603
		try {
604
			$timezone = new DateTimeZone( $text );
605
		} catch ( Exception $e ) {
606
			$blog_timezone = get_option( 'timezone_string' );
607
			if ( ! $blog_timezone ) {
608
				$blog_timezone = 'Etc/UTC';
609
			}
610
611
			$timezone = new DateTimeZone( $blog_timezone );
612
		}
613
614
		return $timezone;
615
	}
616
617
	/**
618
	 * Add a component to the calendar array
619
	 *
620
	 * @param string $component (default: '')
621
	 * @param string $keyword (default: '')
622
	 * @param string $value (default: '')
623
	 * @return void
624
	 */
625
	public function add_component( $component = '', $keyword = '', $value = '' ) {
626
		if ( false == $keyword ) {
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...
627
			$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...
628
			switch ( $component ) {
629
			case 'VEVENT':
630
				$value = $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] . $value;
631
				break;
632
			case 'VTODO' :
633
				$value = $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] . $value;
634
				break;
635
			}
636
		}
637
638
		/*
639
		 * Some events have a specific timezone set in their start/end date,
640
		 * and it may or may not be different than the calendar timzeone.
641
		 * Valid formats include:
642
		 * DTSTART;TZID=Pacific Standard Time:20141219T180000
643
		 * DTEND;TZID=Pacific Standard Time:20141219T200000
644
		 * EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
645
		 * EXDATE;VALUE=DATE:2015050
646
		 * EXDATE;TZID=America/New_York:20150424T170000
647
		 * EXDATE;TZID=Pacific Standard Time:20120615T140000,20120629T140000,20120706T140000
648
		 */
649
650
		// Always store EXDATE as an array
651
		if ( stristr( $keyword, 'EXDATE' ) ) {
652
			$value = explode( ',', $value );
653
		}
654
655
		// Adjust DTSTART, DTEND, and EXDATE according to their TZID if set
656
		if ( strpos( $keyword, ';' ) && ( stristr( $keyword, 'DTSTART' ) || stristr( $keyword, 'DTEND' ) || stristr( $keyword, 'EXDATE' ) || stristr( $keyword, 'RECURRENCE-ID' ) ) ) {
657
			$keyword = explode( ';', $keyword );
658
659
			$tzid = false;
660
			if ( 2 == count( $keyword ) ) {
661
				$tparam = $keyword[1];
662
663
				if ( strpos( $tparam, "TZID" ) !== false ) {
664
					$tzid = $this->timezone_from_string( str_replace( 'TZID=', '', $tparam ) );
665
				}
666
			}
667
668
			// Normalize all times to default UTC
669
			if ( $tzid ) {
670
				$adjusted_times = array();
671
				foreach ( (array) $value as $v ) {
672
					try {
673
						$adjusted_time = new DateTime( $v, $tzid );
674
						$adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
675
						$adjusted_times[] = $adjusted_time->format('Ymd\THis');
676
					} catch ( Exception $e ) {
677
						// Invalid argument to DateTime
678
						return;
679
					}
680
				}
681
				$value = $adjusted_times;
682
			}
683
684
			// Format for adding to event
685
			$keyword = $keyword[0];
686
			if ( 'EXDATE' != $keyword ) {
687
				$value = implode( (array) $value );
688
			}
689
		}
690
691
		foreach ( (array) $value as $v ) {
692
			switch ($component) {
693 View Code Duplication
				case 'VTODO':
694
					if ( 'EXDATE' == $keyword ) {
695
						$this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ][] = $v;
696
					} else {
697
						$this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] = $v;
698
					}
699
					break;
700 View Code Duplication
				case 'VEVENT':
701
					if ( 'EXDATE' == $keyword ) {
702
						$this->cal[ $component ][ $this->event_count - 1 ][ $keyword ][] = $v;
703
					} else {
704
						$this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] = $v;
705
					}
706
					break;
707
				default:
708
					$this->cal[ $component ][ $keyword ] = $v;
709
					break;
710
			}
711
		}
712
		$this->last_keyword = $keyword;
713
	}
714
715
	/**
716
	 * Escape strings with wp_kses, allow links
717
	 *
718
	 * @param string $string (default: '')
719
	 * @return string
720
	 */
721
	public function escape( $string = '' ) {
722
		// Unfold content lines per RFC 5545
723
		$string = str_replace( "\n\t", '', $string );
724
		$string = str_replace( "\n ", '', $string );
725
726
		$allowed_html = array(
727
			'a' => array(
728
				'href'  => array(),
729
				'title' => array()
730
			)
731
		);
732
733
		$allowed_tags = '';
734
		foreach ( array_keys( $allowed_html ) as $tag ) {
735
			$allowed_tags .= "<{$tag}>";
736
		}
737
738
		// Running strip_tags() first with allowed tags to get rid of remaining gallery markup, etc
739
		// because wp_kses() would only htmlentity'fy that. Then still running wp_kses(), for extra
740
		// safety and good measure.
741
		return wp_kses( strip_tags( $string, $allowed_tags ), $allowed_html );
742
	}
743
744
	/**
745
	 * Render the events
746
	 *
747
	 * @param string $url (default: '')
748
	 * @param string $context (default: 'widget') or 'shortcode'
0 ignored issues
show
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...
749
	 * @return mixed bool|string false on failure, rendered HTML string on success.
750
	 */
751
	public function render( $url = '', $args = array() ) {
752
753
		$args = wp_parse_args( $args, array(
754
			'context' => 'widget',
755
			'number' => 5
756
		) );
757
758
		$events = $this->get_events( $url, $args['number'] );
759
		$events = $this->apply_timezone_offset( $events );
760
761
		if ( empty( $events ) )
762
			return false;
763
764
		ob_start();
765
766
		if ( 'widget' == $args['context'] ) : ?>
767
		<ul class="upcoming-events">
768
			<?php foreach ( $events as $event ) : ?>
769
			<li>
770
				<strong class="event-summary"><?php echo $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></strong>
771
				<span class="event-when"><?php echo $this->formatted_date( $event ); ?></span>
772 View Code Duplication
				<?php if ( ! empty( $event['LOCATION'] ) ) : ?>
773
					<span class="event-location"><?php echo $this->escape( stripslashes( $event['LOCATION'] ) ); ?></span>
774
				<?php endif; ?>
775 View Code Duplication
				<?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?>
776
					<span class="event-description"><?php echo wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></span>
777
				<?php endif; ?>
778
			</li>
779
			<?php endforeach; ?>
780
		</ul>
781
		<?php endif;
782
783
		if ( 'shortcode' == $args['context'] ) : ?>
784
		<table class="upcoming-events">
785
			<thead>
786
				<tr>
787
					<th><?php esc_html_e( 'Location', 'jetpack' ); ?></th>
788
					<th><?php esc_html_e( 'When', 'jetpack' ); ?></th>
789
					<th><?php esc_html_e( 'Summary', 'jetpack' ); ?></th>
790
					<th><?php esc_html_e( 'Description', 'jetpack' ); ?></th>
791
				</tr>
792
			</thead>
793
			<tbody>
794
			<?php foreach ( $events as $event ) : ?>
795
				<tr>
796
					<td><?php echo empty( $event['LOCATION'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['LOCATION'] ) ); ?></td>
797
					<td><?php echo $this->formatted_date( $event ); ?></td>
798
					<td><?php echo empty( $event['SUMMARY'] ) ? '&nbsp;' : $this->escape( stripslashes( $event['SUMMARY'] ) ); ?></td>
799
					<td><?php echo empty( $event['DESCRIPTION'] ) ? '&nbsp;' : wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); ?></td>
800
				</tr>
801
			<?php endforeach; ?>
802
			</tbody>
803
		</table>
804
		<?php endif;
805
806
		$rendered = ob_get_clean();
807
808
		if ( empty( $rendered ) )
809
			return false;
810
811
		return $rendered;
812
	}
813
814
	public function formatted_date( $event ) {
815
816
		$date_format = get_option( 'date_format' );
817
		$time_format = get_option( 'time_format' );
818
		$start = strtotime( $event['DTSTART'] );
819
		$end = isset( $event['DTEND'] ) ? strtotime( $event['DTEND'] ) : false;
820
821
		$all_day = ( 8 == strlen( $event['DTSTART'] ) );
822
823
		if ( !$all_day && $this->timezone ) {
824
			try {
825
				$start_time = new DateTime( $event['DTSTART'] );
826
				$timezone_offset = $this->timezone->getOffset( $start_time );
827
				$start += $timezone_offset;
828
829
				if ( $end ) {
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...
830
					$end += $timezone_offset;
831
				}
832
			} catch ( Exception $e ) {
833
				// Invalid argument to DateTime
834
			}
835
		}
836
		$single_day = $end ? ( $end - $start ) <= DAY_IN_SECONDS : true;
837
838
		/* translators: Date and time */
839
		$date_with_time = __( '%1$s at %2$s' , 'jetpack' );
840
		/* translators: Two dates with a separator */
841
		$two_dates = __( '%1$s &ndash; %2$s' , 'jetpack' );
842
843
		// we'll always have the start date. Maybe with time
844 View Code Duplication
		if ( $all_day )
845
			$date = date_i18n( $date_format, $start );
846
		else
847
			$date = sprintf( $date_with_time, date_i18n( $date_format, $start ), date_i18n( $time_format, $start ) );
848
849
		// single day, timed
850
		if ( $single_day && ! $all_day && false !== $end )
851
			$date = sprintf( $two_dates, $date, date_i18n( $time_format, $end ) );
852
853
		// multi-day
854
		if ( ! $single_day ) {
855
856 View Code Duplication
			if ( $all_day ) {
857
				// DTEND for multi-day events represents "until", not "including", so subtract one minute
858
				$end_date = date_i18n( $date_format, $end - 60 );
859
			} else {
860
				$end_date = sprintf( $date_with_time, date_i18n( $date_format, $end ), date_i18n( $time_format, $end ) );
861
			}
862
863
			$date = sprintf( $two_dates, $date, $end_date );
864
865
		}
866
867
		return $date;
868
	}
869
870
	protected function sort_by_recent( $list ) {
871
		$dates = $sorted_list = array();
872
873
		foreach ( $list as $key => $row ) {
874
			$date = $row['DTSTART'];
875
			// pad some time onto an all day date
876
			if ( 8 === strlen( $date ) )
877
				$date .= 'T000000Z';
878
			$dates[$key] = $date;
879
		}
880
		asort( $dates );
881
		foreach( $dates as $key => $value ) {
882
			$sorted_list[$key] = $list[$key];
883
		}
884
		unset($list);
885
		return $sorted_list;
886
	}
887
888
}
889
890
891
/**
892
 * Wrapper function for iCalendarReader->get_events()
893
 *
894
 * @param string $url (default: '')
895
 * @return array
896
 */
897
function icalendar_get_events( $url = '', $count = 5 ) {
898
	// Find your calendar's address http://support.google.com/calendar/bin/answer.py?hl=en&answer=37103
899
	$ical = new iCalendarReader();
900
	return $ical->get_events( $url, $count );
901
}
902
903
/**
904
 * Wrapper function for iCalendarReader->render()
905
 *
906
 * @param string $url (default: '')
907
 * @param string $context (default: 'widget') or 'shortcode'
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...
908
 * @return mixed bool|string false on failure, rendered HTML string on success.
909
 */
910
function icalendar_render_events( $url = '', $args = array() ) {
911
	$ical = new iCalendarReader();
912
	return $ical->render( $url, $args );
913
}
914