PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 12.3.2
Jetpack – WP Security, Backup, Speed, & Growth v12.3.2
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / _inc / lib / icalendar-reader.php
jetpack / _inc / lib Last commit date
admin-pages 3 years ago core-api 3 years ago debugger 3 years ago markdown 4 years ago class-jetpack-ai-helper.php 3 years ago class-jetpack-currencies.php 5 years ago class-jetpack-google-drive-helper.php 3 years ago class-jetpack-instagram-gallery-helper.php 3 years ago class-jetpack-mapbox-helper.php 3 years ago class-jetpack-podcast-feed-locator.php 5 years ago class-jetpack-podcast-helper.php 3 years ago class-jetpack-recommendations.php 3 years ago class-jetpack-tweetstorm-helper.php 3 years ago class-jetpack-wizard.php 5 years ago class.color.php 4 years ago class.core-rest-api-endpoints.php 3 years ago class.jetpack-automatic-install-skin.php 4 years ago class.jetpack-iframe-embed.php 3 years ago class.jetpack-keyring-service-helper.php 3 years ago class.jetpack-password-checker.php 3 years ago class.jetpack-search-performance-logger.php 4 years ago class.media-extractor.php 3 years ago class.media-summary.php 3 years ago class.media.php 3 years ago components.php 3 years ago debugger.php 4 years ago functions.wp-notify.php 3 years ago icalendar-reader.php 3 years ago markdown.php 3 years ago plans.php 4 years ago plugins.php 4 years ago tonesque.php 3 years ago widgets.php 4 years ago
icalendar-reader.php
1079 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Get and render iCal feeds.
4 * Used by the Upcoming Events widget and the [upcomingevents] shortcode.
5 *
6 * @package automattic/jetpack
7 */
8
9 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
10
11 /**
12 * Calendar utilities class.
13 *
14 * phpcs:disable PEAR.NamingConventions.ValidClassName.StartWithCapital
15 */
16 class iCalendarReader {
17 // phpcs:enable PEAR.NamingConventions.ValidClassName.StartWithCapital
18 // phpcs:disable WordPress.DateTime.RestrictedFunctions.date_date -- we manually handle timezones all over the file.
19 // @todo Verify that we're manually handling timezones *correctly*. We probably need more `DateTime` with `$this->timezone` and maybe `wp_date()` and less `strtotime()` and `date()` and `date_i18n()`.
20 /**
21 * Count To Do events in calendar.
22 *
23 * @var int
24 */
25 public $todo_count = 0;
26
27 /**
28 * How many events can be found in calendar.
29 *
30 * @var int
31 */
32 public $event_count = 0;
33
34 /**
35 * Details about our calendar.
36 *
37 * @var array
38 */
39 public $cal = array();
40
41 /**
42 * Timezone parsed from the iCalendar feed, if any.
43 *
44 * @var null|DateTimeZone
45 */
46 public $timezone = null;
47
48 /**
49 * Class constructor
50 *
51 * @return void
52 */
53 public function __construct() {}
54
55 /**
56 * Return an array of events
57 *
58 * @param string $url (default: '') URL of the iCal feed.
59 * @param int $count Count the number of events.
60 *
61 * @return array | false on failure
62 */
63 public function get_events( $url = '', $count = 5 ) {
64 $count = (int) $count;
65 $transient_id = 'icalendar_vcal_' . md5( $url ) . '_' . $count;
66
67 $vcal = get_transient( $transient_id );
68 $vcal = false;
69 if ( ! empty( $vcal ) ) {
70 if ( isset( $vcal['TIMEZONE'] ) ) {
71 $this->timezone = $this->timezone_from_string( $vcal['TIMEZONE'] );
72 }
73
74 if ( isset( $vcal['VEVENT'] ) ) {
75 $vevent = $vcal['VEVENT'];
76
77 if ( $count > 0 ) {
78 $vevent = array_slice( $vevent, 0, $count );
79 }
80
81 $this->cal['VEVENT'] = $vevent;
82
83 return $this->cal['VEVENT'];
84 }
85 }
86
87 if ( ! $this->parse( $url ) ) {
88 return false;
89 }
90
91 $vcal = array();
92
93 if ( $this->timezone ) {
94 $vcal['TIMEZONE'] = $this->timezone->getName();
95 } else {
96 $this->timezone = $this->timezone_from_string( '' );
97 }
98
99 if ( ! empty( $this->cal['VEVENT'] ) ) {
100 $vevent = $this->cal['VEVENT'];
101
102 // check for recurring events.
103 // $vevent = $this->add_recurring_events( $vevent );.
104
105 // remove before caching - no sense in hanging onto the past.
106 $vevent = $this->filter_past_and_recurring_events( $vevent );
107
108 // order by soonest start date.
109 $vevent = $this->sort_by_recent( $vevent );
110
111 $vcal['VEVENT'] = $vevent;
112 }
113
114 set_transient( $transient_id, $vcal, HOUR_IN_SECONDS );
115
116 if ( ! isset( $vcal['VEVENT'] ) ) {
117 return false;
118 }
119
120 if ( $count > 0 ) {
121 return array_slice( $vcal['VEVENT'], 0, $count );
122 }
123
124 return $vcal['VEVENT'];
125 }
126
127 /**
128 * Adjust event's time based on site's timezone.
129 *
130 * @param array $events Array of events.
131 *
132 * @return array
133 */
134 public function apply_timezone_offset( $events ) {
135 if ( ! $events ) {
136 return $events;
137 }
138
139 // get timezone offset from the timezone name.
140 $timezone = wp_timezone();
141
142 $offsetted_events = array();
143
144 foreach ( $events as $event ) {
145 // Don't handle all-day events.
146 if ( 8 < strlen( $event['DTSTART'] ) ) {
147 $start_time = preg_replace( '/Z$/', '', $event['DTSTART'] );
148 $start_time = new DateTime( $start_time, $this->timezone );
149 $start_time->setTimeZone( $timezone );
150
151 $end_time = preg_replace( '/Z$/', '', $event['DTEND'] );
152 $end_time = new DateTime( $end_time, $this->timezone );
153 $end_time->setTimeZone( $timezone );
154
155 $event['DTSTART'] = $start_time->format( 'YmdHis\Z' );
156 $event['DTEND'] = $end_time->format( 'YmdHis\Z' );
157 }
158
159 $offsetted_events[] = $event;
160 }
161
162 return $offsetted_events;
163 }
164
165 /**
166 * Reorganize events into an array of events with standardized data.
167 *
168 * @param array $events Array of events.
169 *
170 * @return array
171 */
172 protected function filter_past_and_recurring_events( $events ) {
173 $upcoming = array();
174 $set_recurring_events = array();
175 /**
176 * This filter allows any time to be passed in for testing or changing timezones, etc...
177 *
178 * @module widgets
179 *
180 * @since 3.4.0
181 *
182 * @param object time() A time object.
183 */
184 $current = apply_filters( 'ical_get_current_time', time() );
185
186 foreach ( $events as $event ) {
187
188 $date_from_ics = strtotime( $event['DTSTART'] );
189 if ( isset( $event['DTEND'] ) ) {
190 $duration = strtotime( $event['DTEND'] ) - strtotime( $event['DTSTART'] );
191 } else {
192 $duration = 0;
193 }
194
195 if ( isset( $event['RRULE'] ) && $this->timezone->getName() && 8 !== strlen( $event['DTSTART'] ) ) {
196 try {
197 $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( 'UTC' ) );
198 $adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
199 $event['DTSTART'] = $adjusted_time->format( 'Ymd\THis' );
200 $date_from_ics = strtotime( $event['DTSTART'] );
201
202 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
203 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
204 // Invalid argument to DateTime.
205 }
206
207 if ( isset( $event['EXDATE'] ) ) {
208 $exdates = array();
209 foreach ( (array) $event['EXDATE'] as $exdate ) {
210 try {
211 $adjusted_time = new DateTime( $exdate, new DateTimeZone( 'UTC' ) );
212 $adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
213 if ( 8 === strlen( $event['DTSTART'] ) ) {
214 $exdates[] = $adjusted_time->format( 'Ymd' );
215 } else {
216 $exdates[] = $adjusted_time->format( 'Ymd\THis' );
217 }
218 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
219 // Invalid argument to DateTime.
220 }
221 }
222 $event['EXDATE'] = $exdates;
223 } else {
224 $event['EXDATE'] = array();
225 }
226 }
227
228 if ( ! isset( $event['DTSTART'] ) ) {
229 continue;
230 }
231
232 // Process events with RRULE before other events.
233 $rrule = isset( $event['RRULE'] ) ? $event['RRULE'] : false;
234 $uid = $event['UID'];
235
236 if ( $rrule && ! in_array( $uid, $set_recurring_events, true ) ) {
237
238 // Break down the RRULE into digestible chunks.
239 $rrule_array = array();
240
241 foreach ( explode( ';', $event['RRULE'] ) as $rline ) {
242 list( $rkey, $rvalue ) = explode( '=', $rline, 2 );
243 $rrule_array[ $rkey ] = $rvalue;
244 }
245
246 $interval = ( isset( $rrule_array['INTERVAL'] ) ) ? $rrule_array['INTERVAL'] : 1;
247 $rrule_count = ( isset( $rrule_array['COUNT'] ) ) ? $rrule_array['COUNT'] : 0;
248 $until = ( isset( $rrule_array['UNTIL'] ) ) ? strtotime( $rrule_array['UNTIL'] ) : strtotime( '+1 year', $current );
249
250 // Used to bound event checks.
251 $echo_limit = 10;
252 $noop = false;
253
254 // Set bydays for the event.
255 $weekdays = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' );
256 $bydays = $weekdays;
257
258 // Calculate a recent start date for incrementing depending on the frequency and interval.
259 switch ( $rrule_array['FREQ'] ) {
260
261 case 'DAILY':
262 $frequency = 'day';
263 $echo_limit = 10;
264
265 if ( $date_from_ics >= $current ) {
266 $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
267 } else {
268 // Interval and count.
269 $catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * DAY_IN_SECONDS ) );
270 if ( $rrule_count && $catchup > 0 ) {
271 if ( $catchup < $rrule_count ) {
272 $rrule_count = $rrule_count - $catchup;
273 $recurring_event_date_start = date(
274 'Ymd',
275 strtotime(
276 '+ ' . ( $interval * $catchup ) . ' days',
277 strtotime( $event['DTSTART'] )
278 )
279 ) . date(
280 '\THis',
281 strtotime( $event['DTSTART'] )
282 );
283 } else {
284 $noop = true;
285 }
286 } else {
287 $recurring_event_date_start = date(
288 'Ymd',
289 strtotime(
290 '+ ' . ( $interval * $catchup ) . ' days',
291 strtotime( $event['DTSTART'] )
292 )
293 ) . date(
294 '\THis',
295 strtotime( $event['DTSTART'] )
296 );
297 }
298 }
299 break;
300
301 case 'WEEKLY':
302 $frequency = 'week';
303 $echo_limit = 4;
304
305 // BYDAY exception to current date.
306 $day = false;
307 if ( ! isset( $rrule_array['BYDAY'] ) ) {
308 $rrule_array['BYDAY'] = strtoupper( substr( date( 'D', strtotime( $event['DTSTART'] ) ), 0, 2 ) );
309 $day = $rrule_array['BYDAY'];
310 }
311 $bydays = explode( ',', $rrule_array['BYDAY'] );
312
313 if ( $date_from_ics >= $current ) {
314 $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
315 } else {
316 // Interval and count.
317 $catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * WEEK_IN_SECONDS ) );
318 if ( $rrule_count && $catchup > 0 ) {
319 if ( ( $catchup * count( $bydays ) ) < $rrule_count ) {
320 $rrule_count = $rrule_count - ( $catchup * count( $bydays ) ); // Estimate current event count.
321 $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
322 } else {
323 $noop = true;
324 }
325 } else {
326 $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
327 }
328 }
329
330 // Set to Sunday start.
331 if ( ! $noop && 'SU' !== strtoupper( substr( date( 'D', strtotime( $recurring_event_date_start ) ), 0, 2 ) ) ) {
332 $recurring_event_date_start = date( 'Ymd', strtotime( 'last Sunday', strtotime( $recurring_event_date_start ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
333 }
334 break;
335
336 case 'MONTHLY':
337 $frequency = 'month';
338 $echo_limit = 1;
339
340 if ( $date_from_ics >= $current ) {
341 $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
342 } else {
343 // Describe the date in the month.
344 if ( isset( $rrule_array['BYDAY'] ) ) {
345 $day_number = substr( $rrule_array['BYDAY'], 0, 1 );
346 $week_day = substr( $rrule_array['BYDAY'], 1 );
347 $day_cardinals = array(
348 1 => 'first',
349 2 => 'second',
350 3 => 'third',
351 4 => 'fourth',
352 5 => 'fifth',
353 );
354 $weekdays = array(
355 'SU' => 'Sunday',
356 'MO' => 'Monday',
357 'TU' => 'Tuesday',
358 'WE' => 'Wednesday',
359 'TH' => 'Thursday',
360 'FR' => 'Friday',
361 'SA' => 'Saturday',
362 );
363 $event_date_desc = "{$day_cardinals[$day_number]} {$weekdays[$week_day]} of ";
364 } else {
365 $event_date_desc = date( 'd ', strtotime( $event['DTSTART'] ) );
366 }
367
368 // Interval only.
369 if ( $interval > 1 ) {
370 $catchup = 0;
371 $maybe = strtotime( $event['DTSTART'] );
372 while ( $maybe < $current ) {
373 $maybe = strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) );
374 ++$catchup;
375 }
376 $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'] ) );
377 } else {
378 $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', $current ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
379 }
380
381 // Add one interval if necessary.
382 if ( strtotime( $recurring_event_date_start ) < $current ) {
383 if ( $interval > 1 ) {
384 $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'] ) );
385 } else {
386 try {
387 $adjustment = new DateTime( date( 'Y-m-d', $current ) );
388 $adjustment->modify( 'first day of next month' );
389 $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . $adjustment->format( 'F Y' ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
390 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
391 // Invalid argument to DateTime.
392 }
393 }
394 }
395 }
396 break;
397
398 case 'YEARLY':
399 $frequency = 'year';
400 $echo_limit = 1;
401
402 if ( $date_from_ics >= $current ) {
403 $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
404 } else {
405 $recurring_event_date_start = date( 'Y', $current ) . date( 'md\THis', strtotime( $event['DTSTART'] ) );
406 if ( strtotime( $recurring_event_date_start ) < $current ) {
407 try {
408 $next = new DateTime( date( 'Y-m-d', $current ) );
409 $next->modify( 'first day of next year' );
410 $recurring_event_date_start = $next->format( 'Y' ) . date( 'md\THis', strtotime( $event['DTSTART'] ) );
411 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
412 // Invalid argument to DateTime.
413 }
414 }
415 }
416 break;
417
418 default:
419 $frequency = false;
420 }
421
422 if ( false !== $frequency && ! $noop ) {
423 $count_counter = 1;
424
425 // If no COUNT limit, go to 10.
426 if ( empty( $rrule_count ) ) {
427 $rrule_count = 10;
428 }
429
430 // Set up EXDATE handling for the event.
431 $exdates = ( isset( $event['EXDATE'] ) ) ? $event['EXDATE'] : array();
432
433 for ( $i = 1; $i <= $echo_limit; $i++ ) {
434
435 // Weeks need a daily loop and must check for inclusion in BYDAYS.
436 if ( 'week' === $frequency ) {
437 $byday_event_date_start = strtotime( $recurring_event_date_start );
438
439 foreach ( $weekdays as $day ) {
440
441 $event_start_timestamp = $byday_event_date_start;
442 $start_time = date( 'His', $event_start_timestamp );
443 $event_end_timestamp = $event_start_timestamp + $duration;
444 $end_time = date( 'His', $event_end_timestamp );
445 if ( 8 === strlen( $event['DTSTART'] ) ) {
446 $exdate_compare = date( 'Ymd', $event_start_timestamp );
447 } else {
448 $exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
449 }
450
451 if (
452 in_array( $day, $bydays, true )
453 && $event_end_timestamp > $current
454 && $event_start_timestamp < $until
455 && $count_counter <= $rrule_count
456 && $event_start_timestamp >= $date_from_ics
457 && ! in_array( $exdate_compare, $exdates, true )
458 ) {
459 if ( 8 === strlen( $event['DTSTART'] ) ) {
460 $event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
461 $event['DTEND'] = date( 'Ymd', $event_end_timestamp );
462 } else {
463 $event['DTSTART'] = date( 'Ymd\THis', $event_start_timestamp );
464 $event['DTEND'] = date( 'Ymd\THis', $event_end_timestamp );
465 }
466 if ( $this->timezone->getName() && 8 !== strlen( $event['DTSTART'] ) ) {
467 try {
468 $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
469 $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
470 $event['DTSTART'] = $adjusted_time->format( 'Ymd\THis' );
471
472 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
473 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
474 // Invalid argument to DateTime.
475 }
476 }
477 $upcoming[] = $event;
478 ++$count_counter;
479 }
480
481 // Move forward one day.
482 $byday_event_date_start = strtotime( date( 'Ymd\T', strtotime( '+ 1 day', $event_start_timestamp ) ) . $start_time );
483 }
484
485 // Restore first event timestamp.
486 $event_start_timestamp = strtotime( $recurring_event_date_start );
487
488 } else {
489
490 $event_start_timestamp = strtotime( $recurring_event_date_start );
491 $start_time = date( 'His', $event_start_timestamp );
492 $event_end_timestamp = $event_start_timestamp + $duration;
493 $end_time = date( 'His', $event_end_timestamp );
494 if ( 8 === strlen( $event['DTSTART'] ) ) {
495 $exdate_compare = date( 'Ymd', $event_start_timestamp );
496 } else {
497 $exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
498 }
499
500 if (
501 $event_end_timestamp > $current
502 && $event_start_timestamp < $until
503 && $count_counter <= $rrule_count
504 && $event_start_timestamp >= $date_from_ics
505 && ! in_array( $exdate_compare, $exdates, true )
506 ) {
507 if ( 8 === strlen( $event['DTSTART'] ) ) {
508 $event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
509 $event['DTEND'] = date( 'Ymd', $event_end_timestamp );
510 } else {
511 $event['DTSTART'] = date( 'Ymd\T', $event_start_timestamp ) . $start_time;
512 $event['DTEND'] = date( 'Ymd\T', $event_end_timestamp ) . $end_time;
513 }
514 if ( $this->timezone->getName() && 8 !== strlen( $event['DTSTART'] ) ) {
515 try {
516 $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
517 $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
518 $event['DTSTART'] = $adjusted_time->format( 'Ymd\THis' );
519
520 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
521 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
522 // Invalid argument to DateTime.
523 }
524 }
525 $upcoming[] = $event;
526 ++$count_counter;
527 }
528 }
529
530 // Set up next interval and reset $event['DTSTART'] and $event['DTEND'], keeping timestamps intact.
531 $next_start_timestamp = strtotime( "+ {$interval} {$frequency}s", $event_start_timestamp );
532 if ( 8 === strlen( $event['DTSTART'] ) ) {
533 $event['DTSTART'] = date( 'Ymd', $next_start_timestamp );
534 $event['DTEND'] = date( 'Ymd', strtotime( $event['DTSTART'] ) + $duration );
535 } else {
536 $event['DTSTART'] = date( 'Ymd\THis', $next_start_timestamp );
537 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
538 }
539
540 // Move recurring event date forward.
541 $recurring_event_date_start = $event['DTSTART'];
542 }
543 $set_recurring_events[] = $uid;
544
545 }
546 } elseif ( strtotime( isset( $event['DTEND'] ) ? $event['DTEND'] : $event['DTSTART'] ) >= $current ) { // Process normal events.
547 $upcoming[] = $event;
548 }
549 }
550 return $upcoming;
551 }
552
553 /**
554 * Parse events from an iCalendar feed
555 *
556 * @param string $url (default: '').
557 * @return array | false on failure
558 */
559 public function parse( $url = '' ) {
560 $cache_group = 'icalendar_reader_parse';
561 $disable_get_key = 'disable:' . md5( $url );
562
563 // Check to see if previous attempts have failed.
564 if ( false !== wp_cache_get( $disable_get_key, $cache_group ) ) {
565 return false;
566 }
567
568 // rewrite webcal: URI schem to HTTP.
569 $url = preg_replace( '/^webcal/', 'http', $url );
570 // try to fetch.
571 $r = wp_safe_remote_get(
572 $url,
573 array(
574 'timeout' => 3,
575 'sslverify' => false,
576 )
577 );
578 if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
579 // We were unable to fetch any content, so don't try again for another 60 seconds.
580 wp_cache_set( $disable_get_key, 1, $cache_group, 60 );
581 return false;
582 }
583
584 $body = wp_remote_retrieve_body( $r );
585 if ( empty( $body ) ) {
586 return false;
587 }
588
589 $body = str_replace( "\r\n", "\n", $body );
590 $lines = preg_split( "/\n(?=[A-Z])/", $body );
591
592 if ( empty( $lines ) ) {
593 return false;
594 }
595
596 if ( false === stristr( $lines[0], 'BEGIN:VCALENDAR' ) ) {
597 return false;
598 }
599
600 $type = '';
601 foreach ( $lines as $line ) {
602 $add = $this->key_value_from_string( $line );
603 if ( ! $add ) {
604 $this->add_component( $type, false, $line );
605 continue;
606 }
607 list( $keyword, $value ) = $add;
608
609 switch ( $keyword ) {
610 case 'BEGIN':
611 case 'END':
612 switch ( $line ) {
613 case 'BEGIN:VTODO':
614 ++$this->todo_count;
615 $type = 'VTODO';
616 break;
617 case 'BEGIN:VEVENT':
618 ++$this->event_count;
619 $type = 'VEVENT';
620 break;
621 case 'BEGIN:VCALENDAR':
622 case 'BEGIN:DAYLIGHT':
623 case 'BEGIN:VTIMEZONE':
624 case 'BEGIN:STANDARD':
625 $type = $value;
626 break;
627 case 'END:VTODO':
628 case 'END:VEVENT':
629 case 'END:VCALENDAR':
630 case 'END:DAYLIGHT':
631 case 'END:VTIMEZONE':
632 case 'END:STANDARD':
633 $type = 'VCALENDAR';
634 break;
635 }
636 break;
637 case 'TZID':
638 if (
639 'VTIMEZONE' === $type
640 && ! $this->timezone
641 ) {
642 $this->timezone = $this->timezone_from_string( $value );
643 }
644 break;
645 case 'X-WR-TIMEZONE':
646 if ( ! $this->timezone ) {
647 $this->timezone = $this->timezone_from_string( $value );
648 }
649 break;
650 default:
651 $this->add_component( $type, $keyword, $value );
652 break;
653 }
654 }
655
656 // Filter for RECURRENCE-IDs.
657 $recurrences = array();
658 if ( array_key_exists( 'VEVENT', $this->cal ) ) {
659 foreach ( $this->cal['VEVENT'] as $event ) {
660 if ( isset( $event['RECURRENCE-ID'] ) ) {
661 $recurrences[] = $event;
662 }
663 }
664 foreach ( $recurrences as $recurrence ) {
665 $count_vevent = count( $this->cal['VEVENT'] );
666 for ( $i = 0; $i < $count_vevent; $i++ ) {
667 if (
668 $this->cal['VEVENT'][ $i ]['UID'] === $recurrence['UID']
669 && ! isset( $this->cal['VEVENT'][ $i ]['RECURRENCE-ID'] )
670 ) {
671 $this->cal['VEVENT'][ $i ]['EXDATE'][] = $recurrence['RECURRENCE-ID'];
672 break;
673 }
674 }
675 }
676 }
677
678 return $this->cal;
679 }
680
681 /**
682 * Parse key:value from a string
683 *
684 * @param string $text (default: '').
685 * @return array
686 */
687 public function key_value_from_string( $text = '' ) {
688 preg_match( '/([^:]+)(;[^:]+)?[:]([\w\W]*)/', $text, $matches );
689
690 if ( 0 === count( $matches ) ) {
691 return false;
692 }
693
694 return array( $matches[1], $matches[3] );
695 }
696
697 /**
698 * Convert a timezone name into a timezone object.
699 *
700 * @param string $text Timezone name. Example: America/Chicago.
701 * @return object|null A DateTimeZone object if the conversion was successful.
702 */
703 private function timezone_from_string( $text ) {
704 try {
705 $timezone = new DateTimeZone( $text );
706 } catch ( Exception $e ) {
707 $blog_timezone = get_option( 'timezone_string' );
708 if ( ! $blog_timezone ) {
709 $blog_timezone = 'Etc/UTC';
710 }
711
712 $timezone = new DateTimeZone( $blog_timezone );
713 }
714
715 return $timezone;
716 }
717
718 /**
719 * Add a component to the calendar array
720 *
721 * @param string $component (default: '').
722 * @param bool|string $keyword (default: '').
723 * @param string $value (default: '').
724 * @return void
725 */
726 public function add_component( $component = '', $keyword = '', $value = '' ) {
727 if ( ! $keyword ) {
728 $keyword = $this->last_keyword;
729 switch ( $component ) {
730 case 'VEVENT':
731 $value = $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] . $value;
732 break;
733 case 'VTODO':
734 $value = $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] . $value;
735 break;
736 }
737 }
738
739 /*
740 * Some events have a specific timezone set in their start/end date,
741 * and it may or may not be different than the calendar timzeone.
742 * Valid formats include:
743 * DTSTART;TZID=Pacific Standard Time:20141219T180000
744 * DTEND;TZID=Pacific Standard Time:20141219T200000
745 * EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
746 * EXDATE;VALUE=DATE:2015050
747 * EXDATE;TZID=America/New_York:20150424T170000
748 * EXDATE;TZID=Pacific Standard Time:20120615T140000,20120629T140000,20120706T140000
749 */
750
751 // Always store EXDATE as an array.
752 if ( stristr( $keyword, 'EXDATE' ) ) {
753 $value = explode( ',', $value );
754 }
755
756 // Adjust DTSTART, DTEND, and EXDATE according to their TZID if set.
757 if ( strpos( $keyword, ';' ) && ( stristr( $keyword, 'DTSTART' ) || stristr( $keyword, 'DTEND' ) || stristr( $keyword, 'EXDATE' ) || stristr( $keyword, 'RECURRENCE-ID' ) ) ) {
758 $keyword = explode( ';', $keyword );
759
760 $tzid = false;
761 if ( 2 === count( $keyword ) ) {
762 $tparam = $keyword[1];
763
764 if ( strpos( $tparam, 'TZID' ) !== false ) {
765 $tzid = $this->timezone_from_string( str_replace( 'TZID=', '', $tparam ) );
766 }
767 }
768
769 // Normalize all times to default UTC.
770 if ( $tzid ) {
771 $adjusted_times = array();
772 foreach ( (array) $value as $v ) {
773 try {
774 $adjusted_time = new DateTime( $v, $tzid );
775 $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
776 $adjusted_times[] = $adjusted_time->format( 'Ymd\THis' );
777 } catch ( Exception $e ) {
778 // Invalid argument to DateTime.
779 return;
780 }
781 }
782 $value = $adjusted_times;
783 }
784
785 // Format for adding to event.
786 $keyword = $keyword[0];
787 if ( 'EXDATE' !== $keyword ) {
788 $value = implode( (array) $value );
789 }
790 }
791
792 foreach ( (array) $value as $v ) {
793 switch ( $component ) {
794 case 'VTODO':
795 if ( 'EXDATE' === $keyword ) {
796 $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ][] = $v;
797 } else {
798 $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] = $v;
799 }
800 break;
801 case 'VEVENT':
802 if ( 'EXDATE' === $keyword ) {
803 $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ][] = $v;
804 } else {
805 $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] = $v;
806 }
807 break;
808 default:
809 $this->cal[ $component ][ $keyword ] = $v;
810 break;
811 }
812 }
813 $this->last_keyword = $keyword;
814 }
815
816 /**
817 * Escape strings with wp_kses, allow links
818 *
819 * @param string $string (default: '') The string to escape.
820 * @return string
821 */
822 public function escape( $string = '' ) {
823 // Unfold content lines per RFC 5545.
824 $string = str_replace( "\n\t", '', $string );
825 $string = str_replace( "\n ", '', $string );
826
827 $allowed_html = array(
828 'a' => array(
829 'href' => array(),
830 'title' => array(),
831 ),
832 );
833
834 $allowed_tags = '';
835 foreach ( array_keys( $allowed_html ) as $tag ) {
836 $allowed_tags .= "<{$tag}>";
837 }
838
839 // Running strip_tags() first with allowed tags to get rid of remaining gallery markup, etc
840 // because wp_kses() would only htmlentity'fy that. Then still running wp_kses(), for extra
841 // safety and good measure.
842 return wp_kses( strip_tags( $string, $allowed_tags ), $allowed_html );
843 }
844
845 /**
846 * Render the events
847 *
848 * @param string $url (default: '') URL of the iCal feed.
849 * @param array $args Event options.
850 *
851 * @return mixed bool|string false on failure, rendered HTML string on success.
852 */
853 public function render( $url = '', $args = array() ) {
854
855 $args = wp_parse_args(
856 $args,
857 array(
858 'context' => 'widget',
859 'number' => 5,
860 )
861 );
862
863 $events = $this->get_events( $url, $args['number'] );
864 $events = $this->apply_timezone_offset( $events );
865
866 if ( empty( $events ) ) {
867 return false;
868 }
869
870 ob_start();
871
872 if ( 'widget' === $args['context'] ) : ?>
873 <ul class="upcoming-events">
874 <?php foreach ( $events as $event ) : ?>
875 <li>
876 <strong class="event-summary">
877 <?php
878 echo $this->escape( stripslashes( $event['SUMMARY'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
879 ?>
880 </strong>
881 <span class="event-when"><?php echo esc_html( $this->formatted_date( $event ) ); ?></span>
882 <?php if ( ! empty( $event['LOCATION'] ) ) : ?>
883 <span class="event-location">
884 <?php
885 echo $this->escape( stripslashes( $event['LOCATION'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
886 ?>
887 </span>
888 <?php endif; ?>
889 <?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?>
890 <span class="event-description">
891 <?php
892 echo wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
893 ?>
894 </span>
895 <?php endif; ?>
896 </li>
897 <?php endforeach; ?>
898 </ul>
899 <?php
900 endif;
901
902 if ( 'shortcode' === $args['context'] ) :
903 ?>
904 <table class="upcoming-events">
905 <thead>
906 <tr>
907 <th><?php esc_html_e( 'Location', 'jetpack' ); ?></th>
908 <th><?php esc_html_e( 'When', 'jetpack' ); ?></th>
909 <th><?php esc_html_e( 'Summary', 'jetpack' ); ?></th>
910 <th><?php esc_html_e( 'Description', 'jetpack' ); ?></th>
911 </tr>
912 </thead>
913 <tbody>
914 <?php foreach ( $events as $event ) : ?>
915 <tr>
916 <td>
917 <?php
918 echo empty( $event['LOCATION'] )
919 ? '&nbsp;'
920 : $this->escape( stripslashes( $event['LOCATION'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
921 ?>
922 </td>
923 <td><?php echo esc_html( $this->formatted_date( $event ) ); ?></td>
924 <td>
925 <?php
926 echo empty( $event['SUMMARY'] )
927 ? '&nbsp;'
928 : $this->escape( stripslashes( $event['SUMMARY'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
929 ?>
930 </td>
931 <td>
932 <?php
933 echo empty( $event['DESCRIPTION'] )
934 ? '&nbsp;'
935 : wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
936 ?>
937 </td>
938 </tr>
939 <?php endforeach; ?>
940 </tbody>
941 </table>
942 <?php
943 endif;
944
945 $rendered = ob_get_clean();
946
947 if ( empty( $rendered ) ) {
948 return false;
949 }
950
951 return $rendered;
952 }
953
954 /**
955 * Return a localized string with information about the event's date and time,
956 * or starting date and end date.
957 *
958 * @param array $event Info about the event.
959 *
960 * @return string
961 */
962 public function formatted_date( $event ) {
963 $date_format = get_option( 'date_format' );
964 $time_format = get_option( 'time_format' );
965 $start = strtotime( $event['DTSTART'] );
966 $end = isset( $event['DTEND'] ) ? strtotime( $event['DTEND'] ) : false;
967
968 $all_day = ( 8 === strlen( $event['DTSTART'] ) );
969
970 if ( ! $all_day && $this->timezone ) {
971 try {
972 $timezone_offset = $this->timezone->getOffset( new DateTime( $event['DTSTART'] ) );
973 $start += $timezone_offset;
974
975 if ( $end ) {
976 $end += $timezone_offset;
977 }
978 } catch ( Exception $e ) {
979 // Invalid argument to DateTime.
980 return '';
981 }
982 }
983 $single_day = $end ? ( $end - $start ) <= DAY_IN_SECONDS : true;
984
985 /* translators: Date and time */
986 $date_with_time = __( '%1$s at %2$s', 'jetpack' );
987 /* translators: Two dates with a separator */
988 $two_dates = __( '%1$s &ndash; %2$s', 'jetpack' );
989
990 // we'll always have the start date. Maybe with time.
991 if ( $all_day ) {
992 $date = date_i18n( $date_format, $start );
993 } else {
994 $date = sprintf(
995 $date_with_time,
996 date_i18n( $date_format, $start ),
997 date_i18n( $time_format, $start )
998 );
999 }
1000
1001 // single day, timed.
1002 if ( $single_day && ! $all_day && false !== $end ) {
1003 $date = sprintf( $two_dates, $date, date_i18n( $time_format, $end ) );
1004 }
1005
1006 // multi-day.
1007 if ( ! $single_day ) {
1008
1009 if ( $all_day ) {
1010 // DTEND for multi-day events represents "until", not "including", so subtract one minute.
1011 $end_date = date_i18n( $date_format, $end - 60 );
1012 } else {
1013 $end_date = sprintf( $date_with_time, date_i18n( $date_format, $end ), date_i18n( $time_format, $end ) );
1014 }
1015
1016 $date = sprintf( $two_dates, $date, $end_date );
1017
1018 }
1019
1020 return $date;
1021 }
1022
1023 /**
1024 * Sort list of events by event date.
1025 *
1026 * @param array $list List of events.
1027 *
1028 * @return array
1029 */
1030 protected function sort_by_recent( $list ) {
1031 $dates = array();
1032 $sorted_list = array();
1033
1034 foreach ( $list as $key => $row ) {
1035 $date = $row['DTSTART'];
1036 // pad some time onto an all day date.
1037 if ( 8 === strlen( $date ) ) {
1038 $date .= 'T000000Z';
1039 }
1040 $dates[ $key ] = $date;
1041 }
1042 asort( $dates );
1043 foreach ( $dates as $key => $value ) {
1044 $sorted_list[ $key ] = $list[ $key ];
1045 }
1046 unset( $list );
1047 return $sorted_list;
1048 }
1049
1050 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date
1051 }
1052
1053 /**
1054 * Wrapper function for iCalendarReader->get_events()
1055 *
1056 * @param string $url (default: '').
1057 * @param int $count Number of events to fetch.
1058 * @return array
1059 */
1060 function icalendar_get_events( $url = '', $count = 5 ) {
1061 /*
1062 * Find your calendar's address
1063 * https://support.google.com/calendar/bin/answer.py?hl=en&answer=37103
1064 */
1065 return ( new iCalendarReader() )->get_events( $url, $count );
1066 }
1067
1068 /**
1069 * Wrapper function for iCalendarReader->render()
1070 *
1071 * @param string $url (default: '').
1072 * @param array $args Options when rendering events.
1073 *
1074 * @return mixed bool|string false on failure, rendered HTML string on success.
1075 */
1076 function icalendar_render_events( $url = '', $args = array() ) {
1077 return ( new iCalendarReader() )->render( $url, $args );
1078 }
1079