PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
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 1 week ago core-api 2 days ago debugger 1 month ago markdown 7 months ago class-jetpack-ai-helper.php 4 months ago class-jetpack-application-password-extras.php 3 months ago class-jetpack-blog-stats-helper.php 7 months ago class-jetpack-currencies.php 1 year ago class-jetpack-instagram-gallery-helper.php 1 month ago class-jetpack-mapbox-helper.php 3 years ago class-jetpack-newsletter-category-helper.php 7 months ago class-jetpack-podcast-feed-locator.php 1 month ago class-jetpack-podcast-helper.php 5 months ago class-jetpack-recommendations.php 1 month ago class-jetpack-spinner.php 2 months ago class-jetpack-top-posts-helper.php 1 week ago class.color.php 1 year ago class.core-rest-api-endpoints.php 1 week ago class.jetpack-automatic-install-skin.php 2 years ago class.jetpack-iframe-embed.php 3 years ago class.jetpack-password-checker.php 7 months ago class.jetpack-search-performance-logger.php 4 years ago class.media-extractor.php 1 month ago class.media-summary.php 1 week ago class.media.php 7 months ago components.php 7 months ago debugger.php 1 month ago icalendar-reader.php 1 month ago markdown.php 7 months ago plans.php 4 years ago plugins.php 4 years ago tonesque.php 7 months ago widgets.php 3 months ago
icalendar-reader.php
1094 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 * Last iCalendar keyword parsed.
50 *
51 * @var string
52 */
53 public $last_keyword;
54
55 /**
56 * Class constructor
57 *
58 * @return void
59 */
60 public function __construct() {}
61
62 /**
63 * Return an array of events
64 *
65 * @param string $url (default: '') URL of the iCal feed.
66 * @param int $count Count the number of events.
67 *
68 * @return array | false on failure
69 */
70 public function get_events( $url = '', $count = 5 ) {
71 $count = (int) $count;
72 $transient_id = 'icalendar_vcal_' . md5( $url ) . '_' . $count;
73
74 $vcal = get_transient( $transient_id );
75 if ( ! empty( $vcal ) ) {
76 if ( isset( $vcal['TIMEZONE'] ) ) {
77 $this->timezone = $this->timezone_from_string( $vcal['TIMEZONE'] );
78 }
79
80 if ( isset( $vcal['VEVENT'] ) ) {
81 $vevent = $vcal['VEVENT'];
82
83 if ( $count > 0 ) {
84 $vevent = array_slice( $vevent, 0, $count );
85 }
86
87 $this->cal['VEVENT'] = $vevent;
88
89 return $this->cal['VEVENT'];
90 }
91 }
92
93 if ( ! $this->parse( $url ) ) {
94 return false;
95 }
96
97 $vcal = array();
98
99 if ( $this->timezone ) {
100 $vcal['TIMEZONE'] = $this->timezone->getName();
101 } else {
102 $this->timezone = $this->timezone_from_string( '' );
103 }
104
105 if ( ! empty( $this->cal['VEVENT'] ) ) {
106 $vevent = $this->cal['VEVENT'];
107
108 // check for recurring events.
109 // $vevent = $this->add_recurring_events( $vevent );.
110
111 // remove before caching - no sense in hanging onto the past.
112 $vevent = $this->filter_past_and_recurring_events( $vevent );
113
114 // order by soonest start date.
115 $vevent = $this->sort_by_recent( $vevent );
116
117 $vcal['VEVENT'] = $vevent;
118 }
119
120 set_transient( $transient_id, $vcal, HOUR_IN_SECONDS );
121
122 if ( ! isset( $vcal['VEVENT'] ) ) {
123 return false;
124 }
125
126 if ( $count > 0 ) {
127 return array_slice( $vcal['VEVENT'], 0, $count );
128 }
129
130 return $vcal['VEVENT'];
131 }
132
133 /**
134 * Adjust event's time based on site's timezone.
135 *
136 * @param array $events Array of events.
137 *
138 * @return array
139 */
140 public function apply_timezone_offset( $events ) {
141 if ( ! $events ) {
142 return $events;
143 }
144
145 // get timezone offset from the timezone name.
146 $timezone = wp_timezone();
147
148 $offsetted_events = array();
149
150 foreach ( $events as $event ) {
151 // Don't handle all-day events.
152 if ( 8 < strlen( $event['DTSTART'] ) ) {
153 $start_time = preg_replace( '/Z$/', '', $event['DTSTART'] );
154 $start_time = new DateTime( $start_time, $this->timezone );
155 $start_time->setTimeZone( $timezone );
156 $event['DTSTART'] = $start_time->format( 'YmdHis\Z' );
157 if ( isset( $event['DTEND'] ) ) {
158 $end_time = preg_replace( '/Z$/', '', $event['DTEND'] );
159 $end_time = new DateTime( $end_time, $this->timezone );
160 $end_time->setTimeZone( $timezone );
161 $event['DTEND'] = $end_time->format( 'YmdHis\Z' );
162 }
163 }
164
165 $offsetted_events[] = $event;
166 }
167
168 return $offsetted_events;
169 }
170
171 /**
172 * Reorganize events into an array of events with standardized data.
173 *
174 * @param array $events Array of events.
175 *
176 * @return array
177 */
178 protected function filter_past_and_recurring_events( $events ) {
179 $upcoming = array();
180 $set_recurring_events = array();
181 /**
182 * This filter allows any time to be passed in for testing or changing timezones, etc...
183 *
184 * @module widgets
185 *
186 * @since 3.4.0
187 *
188 * @param object time() A time object.
189 */
190 $current = apply_filters( 'ical_get_current_time', time() );
191
192 foreach ( $events as $event ) {
193
194 $date_from_ics = strtotime( $event['DTSTART'] );
195 if ( isset( $event['DTEND'] ) ) {
196 $duration = strtotime( $event['DTEND'] ) - strtotime( $event['DTSTART'] );
197 } else {
198 $duration = 0;
199 }
200
201 if ( isset( $event['RRULE'] ) && $this->timezone->getName() && 8 !== strlen( $event['DTSTART'] ) ) {
202 try {
203 $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( 'UTC' ) );
204 $adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
205 $event['DTSTART'] = $adjusted_time->format( 'Ymd\THis' );
206 $date_from_ics = strtotime( $event['DTSTART'] );
207
208 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
209 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
210 // Invalid argument to DateTime.
211 }
212
213 if ( isset( $event['EXDATE'] ) ) {
214 $exdates = array();
215 foreach ( (array) $event['EXDATE'] as $exdate ) {
216 try {
217 $adjusted_time = new DateTime( $exdate, new DateTimeZone( 'UTC' ) );
218 $adjusted_time->setTimeZone( new DateTimeZone( $this->timezone->getName() ) );
219 if ( 8 === strlen( $event['DTSTART'] ) ) {
220 $exdates[] = $adjusted_time->format( 'Ymd' );
221 } else {
222 $exdates[] = $adjusted_time->format( 'Ymd\THis' );
223 }
224 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
225 // Invalid argument to DateTime.
226 }
227 }
228 $event['EXDATE'] = $exdates;
229 } else {
230 $event['EXDATE'] = array();
231 }
232 }
233
234 if ( ! isset( $event['DTSTART'] ) ) {
235 continue;
236 }
237
238 // Process events with RRULE before other events.
239 $rrule = $event['RRULE'] ?? false;
240 $uid = $event['UID'] ?? false;
241
242 if ( $rrule && $uid && ! in_array( $uid, $set_recurring_events, true ) ) {
243
244 // Break down the RRULE into digestible chunks.
245 $rrule_array = array();
246
247 foreach ( explode( ';', $event['RRULE'] ) as $rline ) {
248 list( $rkey, $rvalue ) = explode( '=', $rline, 2 );
249 $rrule_array[ $rkey ] = $rvalue;
250 }
251
252 $interval = $rrule_array['INTERVAL'] ?? 1;
253 $rrule_count = $rrule_array['COUNT'] ?? 0;
254 $until = ( isset( $rrule_array['UNTIL'] ) ) ? strtotime( $rrule_array['UNTIL'] ) : strtotime( '+1 year', $current );
255
256 // Used to bound event checks.
257 $echo_limit = 10;
258 $noop = false;
259
260 // Set bydays for the event.
261 $weekdays = array( 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA' );
262 $bydays = $weekdays;
263
264 // Calculate a recent start date for incrementing depending on the frequency and interval.
265 switch ( $rrule_array['FREQ'] ) {
266
267 case 'DAILY':
268 $frequency = 'day';
269 $echo_limit = 10;
270
271 if ( $date_from_ics >= $current ) {
272 $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
273 } else {
274 // Interval and count.
275 $catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * DAY_IN_SECONDS ) );
276 if ( $rrule_count && $catchup > 0 ) {
277 if ( $catchup < $rrule_count ) {
278 $rrule_count -= $catchup;
279 $recurring_event_date_start = date(
280 'Ymd',
281 strtotime(
282 '+ ' . ( $interval * $catchup ) . ' days',
283 strtotime( $event['DTSTART'] )
284 )
285 ) . date(
286 '\THis',
287 strtotime( $event['DTSTART'] )
288 );
289 } else {
290 $noop = true;
291 }
292 } else {
293 $recurring_event_date_start = date(
294 'Ymd',
295 strtotime(
296 '+ ' . ( $interval * $catchup ) . ' days',
297 strtotime( $event['DTSTART'] )
298 )
299 ) . date(
300 '\THis',
301 strtotime( $event['DTSTART'] )
302 );
303 }
304 }
305 break;
306
307 case 'WEEKLY':
308 $frequency = 'week';
309 $echo_limit = 4;
310
311 // BYDAY exception to current date.
312 $day = false;
313 if ( ! isset( $rrule_array['BYDAY'] ) ) {
314 $rrule_array['BYDAY'] = strtoupper( substr( date( 'D', strtotime( $event['DTSTART'] ) ), 0, 2 ) );
315 $day = $rrule_array['BYDAY'];
316 }
317 $bydays = explode( ',', $rrule_array['BYDAY'] );
318
319 if ( $date_from_ics >= $current ) {
320 $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
321 } else {
322 // Interval and count.
323 $catchup = floor( ( $current - strtotime( $event['DTSTART'] ) ) / ( $interval * WEEK_IN_SECONDS ) );
324 if ( $rrule_count && $catchup > 0 ) {
325 if ( ( $catchup * count( $bydays ) ) < $rrule_count ) {
326 $rrule_count -= $catchup * count( $bydays ); // Estimate current event count.
327 $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
328 } else {
329 $noop = true;
330 }
331 } else {
332 $recurring_event_date_start = date( 'Ymd', strtotime( '+ ' . ( $interval * $catchup ) . ' weeks', strtotime( $event['DTSTART'] ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
333 }
334 }
335
336 // Set to Sunday start.
337 if ( ! $noop && 'SU' !== strtoupper( substr( date( 'D', strtotime( $recurring_event_date_start ) ), 0, 2 ) ) ) {
338 $recurring_event_date_start = date( 'Ymd', strtotime( 'last Sunday', strtotime( $recurring_event_date_start ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
339 }
340 break;
341
342 case 'MONTHLY':
343 $frequency = 'month';
344 $echo_limit = 1;
345
346 if ( $date_from_ics >= $current ) {
347 $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
348 } else {
349 // Describe the date in the month.
350 if ( isset( $rrule_array['BYDAY'] )
351 && preg_match( '/^(-?\d)([A-Z]{2})/', $rrule_array['BYDAY'], $matches )
352 ) {
353 $day_number = $matches[1];
354 $week_day = $matches[2];
355
356 $day_cardinals = array(
357 -3 => 'third to last',
358 -2 => 'second to last',
359 -1 => 'last',
360 1 => 'first',
361 2 => 'second',
362 3 => 'third',
363 4 => 'fourth',
364 5 => 'fifth',
365 );
366 $weekdays = array(
367 'SU' => 'Sunday',
368 'MO' => 'Monday',
369 'TU' => 'Tuesday',
370 'WE' => 'Wednesday',
371 'TH' => 'Thursday',
372 'FR' => 'Friday',
373 'SA' => 'Saturday',
374 );
375
376 $day_cardinal = $day_cardinals[ $day_number ] ?? '';
377 $weekday = $weekdays[ $week_day ] ?? '';
378 $event_date_desc = "$day_cardinal $weekday of ";
379 } else {
380 $event_date_desc = date( 'd ', strtotime( $event['DTSTART'] ) );
381 }
382
383 // Interval only.
384 if ( $interval > 1 ) {
385 $catchup = 0;
386 $maybe = strtotime( $event['DTSTART'] );
387 while ( $maybe < $current ) {
388 $maybe = strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) );
389 ++$catchup;
390 }
391 $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'] ) );
392 } else {
393 $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', $current ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
394 }
395
396 // Add one interval if necessary.
397 if ( strtotime( $recurring_event_date_start ) < $current ) {
398 if ( $interval > 1 ) {
399 $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'] ) );
400 } else {
401 try {
402 $adjustment = new DateTime( date( 'Y-m-d', $current ) );
403 $adjustment->modify( 'first day of next month' );
404 $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . $adjustment->format( 'F Y' ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
405 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
406 // Invalid argument to DateTime.
407 }
408 }
409 }
410 }
411 break;
412
413 case 'YEARLY':
414 $frequency = 'year';
415 $echo_limit = 1;
416
417 if ( $date_from_ics >= $current ) {
418 $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
419 } else {
420 $recurring_event_date_start = date( 'Y', $current ) . date( 'md\THis', strtotime( $event['DTSTART'] ) );
421 if ( strtotime( $recurring_event_date_start ) < $current ) {
422 try {
423 $next = new DateTime( date( 'Y-m-d', $current ) );
424 $next->modify( 'first day of next year' );
425 $recurring_event_date_start = $next->format( 'Y' ) . date( 'md\THis', strtotime( $event['DTSTART'] ) );
426 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
427 // Invalid argument to DateTime.
428 }
429 }
430 }
431 break;
432
433 default:
434 $frequency = false;
435 }
436
437 if ( false !== $frequency && ! $noop ) {
438 $count_counter = 1;
439
440 // If no COUNT limit, go to 10.
441 if ( empty( $rrule_count ) ) {
442 $rrule_count = 10;
443 }
444
445 // Set up EXDATE handling for the event.
446 $exdates = $event['EXDATE'] ?? array();
447
448 for ( $i = 1; $i <= $echo_limit; $i++ ) {
449
450 // Weeks need a daily loop and must check for inclusion in BYDAYS.
451 if ( 'week' === $frequency ) {
452 $byday_event_date_start = strtotime( $recurring_event_date_start );
453
454 foreach ( $weekdays as $day ) {
455
456 $event_start_timestamp = $byday_event_date_start;
457 $start_time = date( 'His', $event_start_timestamp );
458 $event_end_timestamp = $event_start_timestamp + $duration;
459 $end_time = date( 'His', $event_end_timestamp );
460 if ( 8 === strlen( $event['DTSTART'] ) ) {
461 $exdate_compare = date( 'Ymd', $event_start_timestamp );
462 } else {
463 $exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
464 }
465
466 if (
467 in_array( $day, $bydays, true )
468 && $event_end_timestamp > $current
469 && $event_start_timestamp < $until
470 && $count_counter <= $rrule_count
471 && $event_start_timestamp >= $date_from_ics
472 && ! in_array( $exdate_compare, $exdates, true )
473 ) {
474 if ( 8 === strlen( $event['DTSTART'] ) ) {
475 $event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
476 $event['DTEND'] = date( 'Ymd', $event_end_timestamp );
477 } else {
478 $event['DTSTART'] = date( 'Ymd\THis', $event_start_timestamp );
479 $event['DTEND'] = date( 'Ymd\THis', $event_end_timestamp );
480 }
481 if ( $this->timezone->getName() && 8 !== strlen( $event['DTSTART'] ) ) {
482 try {
483 $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
484 $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
485 $event['DTSTART'] = $adjusted_time->format( 'Ymd\THis' );
486
487 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
488 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
489 // Invalid argument to DateTime.
490 }
491 }
492 $upcoming[] = $event;
493 ++$count_counter;
494 }
495
496 // Move forward one day.
497 $byday_event_date_start = strtotime( date( 'Ymd\T', strtotime( '+ 1 day', $event_start_timestamp ) ) . $start_time );
498 }
499
500 // Restore first event timestamp.
501 $event_start_timestamp = strtotime( $recurring_event_date_start );
502
503 } else {
504
505 $event_start_timestamp = strtotime( $recurring_event_date_start );
506 $start_time = date( 'His', $event_start_timestamp );
507 $event_end_timestamp = $event_start_timestamp + $duration;
508 $end_time = date( 'His', $event_end_timestamp );
509 if ( 8 === strlen( $event['DTSTART'] ) ) {
510 $exdate_compare = date( 'Ymd', $event_start_timestamp );
511 } else {
512 $exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
513 }
514
515 if (
516 $event_end_timestamp > $current
517 && $event_start_timestamp < $until
518 && $count_counter <= $rrule_count
519 && $event_start_timestamp >= $date_from_ics
520 && ! in_array( $exdate_compare, $exdates, true )
521 ) {
522 if ( 8 === strlen( $event['DTSTART'] ) ) {
523 $event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
524 $event['DTEND'] = date( 'Ymd', $event_end_timestamp );
525 } else {
526 $event['DTSTART'] = date( 'Ymd\T', $event_start_timestamp ) . $start_time;
527 $event['DTEND'] = date( 'Ymd\T', $event_end_timestamp ) . $end_time;
528 }
529 if ( $this->timezone->getName() && 8 !== strlen( $event['DTSTART'] ) ) {
530 try {
531 $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
532 $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
533 $event['DTSTART'] = $adjusted_time->format( 'Ymd\THis' );
534
535 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
536 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
537 // Invalid argument to DateTime.
538 }
539 }
540 $upcoming[] = $event;
541 ++$count_counter;
542 }
543 }
544
545 // Set up next interval and reset $event['DTSTART'] and $event['DTEND'], keeping timestamps intact.
546 $next_start_timestamp = strtotime( "+ {$interval} {$frequency}s", $event_start_timestamp );
547 if ( 8 === strlen( $event['DTSTART'] ) ) {
548 $event['DTSTART'] = date( 'Ymd', $next_start_timestamp );
549 $event['DTEND'] = date( 'Ymd', strtotime( $event['DTSTART'] ) + $duration );
550 } else {
551 $event['DTSTART'] = date( 'Ymd\THis', $next_start_timestamp );
552 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
553 }
554
555 // Move recurring event date forward.
556 $recurring_event_date_start = $event['DTSTART'];
557 }
558 $set_recurring_events[] = $uid;
559
560 }
561 } elseif ( strtotime( $event['DTEND'] ?? $event['DTSTART'] ) >= $current ) { // Process normal events.
562 $upcoming[] = $event;
563 }
564 }
565 return $upcoming;
566 }
567
568 /**
569 * Parse events from an iCalendar feed
570 *
571 * @param string $url (default: '').
572 * @return array | false on failure
573 */
574 public function parse( $url = '' ) {
575 $cache_group = 'icalendar_reader_parse';
576 $disable_get_key = 'disable:' . md5( $url );
577
578 // Check to see if previous attempts have failed.
579 if ( false !== wp_cache_get( $disable_get_key, $cache_group ) ) {
580 return false;
581 }
582
583 // rewrite webcal: URI scheme to HTTP.
584 $url = preg_replace( '/^webcal/', 'http', $url );
585 // try to fetch.
586 $r = wp_safe_remote_get(
587 $url,
588 array(
589 'timeout' => 3,
590 'sslverify' => false,
591 )
592 );
593 if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
594 // We were unable to fetch any content, so don't try again for another 60 seconds.
595 wp_cache_set( $disable_get_key, 1, $cache_group, 60 );
596 return false;
597 }
598
599 $body = wp_remote_retrieve_body( $r );
600 if ( empty( $body ) ) {
601 return false;
602 }
603
604 $body = str_replace( "\r\n", "\n", $body );
605 $lines = preg_split( "/\n(?=[A-Z])/", $body );
606
607 if ( empty( $lines ) ) {
608 return false;
609 }
610
611 if ( false === stristr( $lines[0], 'BEGIN:VCALENDAR' ) ) {
612 return false;
613 }
614
615 $type = '';
616 foreach ( $lines as $line ) {
617 $add = $this->key_value_from_string( $line );
618 if ( ! $add ) {
619 $this->add_component( $type, false, $line );
620 continue;
621 }
622 list( $keyword, $value ) = $add;
623
624 switch ( $keyword ) {
625 case 'BEGIN':
626 case 'END':
627 switch ( $line ) {
628 case 'BEGIN:VTODO':
629 ++$this->todo_count;
630 $type = 'VTODO';
631 break;
632 case 'BEGIN:VEVENT':
633 ++$this->event_count;
634 $type = 'VEVENT';
635 break;
636 case 'BEGIN:VCALENDAR':
637 case 'BEGIN:DAYLIGHT':
638 case 'BEGIN:VTIMEZONE':
639 case 'BEGIN:STANDARD':
640 $type = $value;
641 break;
642 case 'END:VTODO':
643 case 'END:VEVENT':
644 case 'END:VCALENDAR':
645 case 'END:DAYLIGHT':
646 case 'END:VTIMEZONE':
647 case 'END:STANDARD':
648 $type = 'VCALENDAR';
649 break;
650 }
651 break;
652 case 'TZID':
653 if (
654 'VTIMEZONE' === $type
655 && ! $this->timezone
656 ) {
657 $this->timezone = $this->timezone_from_string( $value );
658 }
659 break;
660 case 'X-WR-TIMEZONE':
661 if ( ! $this->timezone ) {
662 $this->timezone = $this->timezone_from_string( $value );
663 }
664 break;
665 default:
666 $this->add_component( $type, $keyword, $value );
667 break;
668 }
669 }
670
671 // Filter for RECURRENCE-IDs.
672 $recurrences = array();
673 if ( array_key_exists( 'VEVENT', $this->cal ) ) {
674 foreach ( $this->cal['VEVENT'] as $event ) {
675 if ( isset( $event['RECURRENCE-ID'] ) ) {
676 $recurrences[] = $event;
677 }
678 }
679 foreach ( $recurrences as $recurrence ) {
680 $count_vevent = count( $this->cal['VEVENT'] );
681 for ( $i = 0; $i < $count_vevent; $i++ ) {
682 if (
683 $this->cal['VEVENT'][ $i ]['UID'] === $recurrence['UID']
684 && ! isset( $this->cal['VEVENT'][ $i ]['RECURRENCE-ID'] )
685 ) {
686 $this->cal['VEVENT'][ $i ]['EXDATE'][] = $recurrence['RECURRENCE-ID'];
687 break;
688 }
689 }
690 }
691 }
692
693 return $this->cal;
694 }
695
696 /**
697 * Parse key:value from a string
698 *
699 * @param string $text (default: '').
700 * @return array
701 */
702 public function key_value_from_string( $text = '' ) {
703 preg_match( '/([^:]+)(;[^:]+)?[:]([\w\W]*)/', $text, $matches );
704
705 if ( 0 === count( $matches ) ) {
706 return false;
707 }
708
709 return array( $matches[1], $matches[3] );
710 }
711
712 /**
713 * Convert a timezone name into a timezone object.
714 *
715 * @param string $text Timezone name. Example: America/Chicago.
716 * @return object|null A DateTimeZone object if the conversion was successful.
717 */
718 private function timezone_from_string( $text ) {
719 try {
720 $timezone = new DateTimeZone( $text );
721 } catch ( Exception $e ) {
722 $blog_timezone = get_option( 'timezone_string' );
723 if ( ! $blog_timezone ) {
724 $blog_timezone = 'Etc/UTC';
725 }
726
727 $timezone = new DateTimeZone( $blog_timezone );
728 }
729
730 return $timezone;
731 }
732
733 /**
734 * Add a component to the calendar array
735 *
736 * @param string $component (default: '').
737 * @param bool|string $keyword (default: '').
738 * @param string $value (default: '').
739 * @return void
740 */
741 public function add_component( $component = '', $keyword = '', $value = '' ) {
742 if ( ! $keyword ) {
743 $keyword = $this->last_keyword;
744 switch ( $component ) {
745 case 'VEVENT':
746 $value = $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] . $value;
747 break;
748 case 'VTODO':
749 $value = $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] . $value;
750 break;
751 }
752 }
753
754 /*
755 * Some events have a specific timezone set in their start/end date,
756 * and it may or may not be different than the calendar timzeone.
757 * Valid formats include:
758 * DTSTART;TZID=Pacific Standard Time:20141219T180000
759 * DTEND;TZID=Pacific Standard Time:20141219T200000
760 * EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
761 * EXDATE;VALUE=DATE:2015050
762 * EXDATE;TZID=America/New_York:20150424T170000
763 * EXDATE;TZID=Pacific Standard Time:20120615T140000,20120629T140000,20120706T140000
764 */
765
766 // Always store EXDATE as an array.
767 if ( stristr( $keyword, 'EXDATE' ) ) {
768 $value = explode( ',', $value );
769 }
770
771 // Adjust DTSTART, DTEND, and EXDATE according to their TZID if set.
772 if ( strpos( $keyword, ';' ) && ( stristr( $keyword, 'DTSTART' ) || stristr( $keyword, 'DTEND' ) || stristr( $keyword, 'EXDATE' ) || stristr( $keyword, 'RECURRENCE-ID' ) ) ) {
773 $keyword = explode( ';', $keyword );
774
775 $tzid = false;
776 if ( 2 === count( $keyword ) ) {
777 $tparam = $keyword[1];
778
779 if ( str_contains( $tparam, 'TZID' ) ) {
780 $tzid = $this->timezone_from_string( str_replace( 'TZID=', '', $tparam ) );
781 }
782 }
783
784 // Normalize all times to default UTC.
785 if ( $tzid ) {
786 $adjusted_times = array();
787 foreach ( (array) $value as $v ) {
788 try {
789 $adjusted_time = new DateTime( $v, $tzid );
790 $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
791 $adjusted_times[] = $adjusted_time->format( 'Ymd\THis' );
792 } catch ( Exception $e ) {
793 // Invalid argument to DateTime.
794 return;
795 }
796 }
797 $value = $adjusted_times;
798 }
799
800 // Format for adding to event.
801 $keyword = $keyword[0];
802 if ( 'EXDATE' !== $keyword ) {
803 $value = implode( (array) $value );
804 }
805 }
806
807 foreach ( (array) $value as $v ) {
808 switch ( $component ) {
809 case 'VTODO':
810 if ( 'EXDATE' === $keyword ) {
811 $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ][] = $v;
812 } else {
813 $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] = $v;
814 }
815 break;
816 case 'VEVENT':
817 if ( 'EXDATE' === $keyword ) {
818 $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ][] = $v;
819 } else {
820 $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] = $v;
821 }
822 break;
823 default:
824 $this->cal[ $component ][ $keyword ] = $v;
825 break;
826 }
827 }
828 $this->last_keyword = $keyword;
829 }
830
831 /**
832 * Escape strings with wp_kses, allow links
833 *
834 * @param string $string (default: '') The string to escape.
835 * @return string
836 */
837 public function escape( $string = '' ) {
838 // Unfold content lines per RFC 5545.
839 $string = str_replace( "\n\t", '', $string );
840 $string = str_replace( "\n ", '', $string );
841
842 $allowed_html = array(
843 'a' => array(
844 'href' => array(),
845 'title' => array(),
846 ),
847 );
848
849 $allowed_tags = '';
850 foreach ( array_keys( $allowed_html ) as $tag ) {
851 $allowed_tags .= "<{$tag}>";
852 }
853
854 // Running strip_tags() first with allowed tags to get rid of remaining gallery markup, etc
855 // because wp_kses() would only htmlentity'fy that. Then still running wp_kses(), for extra
856 // safety and good measure.
857 return wp_kses( strip_tags( $string, $allowed_tags ), $allowed_html );
858 }
859
860 /**
861 * Render the events
862 *
863 * @param string $url (default: '') URL of the iCal feed.
864 * @param array $args Event options.
865 *
866 * @return mixed bool|string false on failure, rendered HTML string on success.
867 */
868 public function render( $url = '', $args = array() ) {
869
870 $args = wp_parse_args(
871 $args,
872 array(
873 'context' => 'widget',
874 'number' => 5,
875 )
876 );
877
878 $events = $this->get_events( $url, $args['number'] );
879 $events = $this->apply_timezone_offset( $events );
880
881 if ( empty( $events ) ) {
882 return false;
883 }
884
885 ob_start();
886
887 if ( 'widget' === $args['context'] ) : ?>
888 <ul class="upcoming-events">
889 <?php foreach ( $events as $event ) : ?>
890 <li>
891 <strong class="event-summary">
892 <?php
893 echo $this->escape( stripslashes( $event['SUMMARY'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
894 ?>
895 </strong>
896 <span class="event-when"><?php echo esc_html( $this->formatted_date( $event ) ); ?></span>
897 <?php if ( ! empty( $event['LOCATION'] ) ) : ?>
898 <span class="event-location">
899 <?php
900 echo $this->escape( stripslashes( $event['LOCATION'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
901 ?>
902 </span>
903 <?php endif; ?>
904 <?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?>
905 <span class="event-description">
906 <?php
907 echo wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
908 ?>
909 </span>
910 <?php endif; ?>
911 </li>
912 <?php endforeach; ?>
913 </ul>
914 <?php
915 endif;
916
917 if ( 'shortcode' === $args['context'] ) :
918 ?>
919 <table class="upcoming-events">
920 <thead>
921 <tr>
922 <th><?php esc_html_e( 'Location', 'jetpack' ); ?></th>
923 <th><?php esc_html_e( 'When', 'jetpack' ); ?></th>
924 <th><?php esc_html_e( 'Summary', 'jetpack' ); ?></th>
925 <th><?php esc_html_e( 'Description', 'jetpack' ); ?></th>
926 </tr>
927 </thead>
928 <tbody>
929 <?php foreach ( $events as $event ) : ?>
930 <tr>
931 <td>
932 <?php
933 echo empty( $event['LOCATION'] )
934 ? '&nbsp;'
935 : $this->escape( stripslashes( $event['LOCATION'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
936 ?>
937 </td>
938 <td><?php echo esc_html( $this->formatted_date( $event ) ); ?></td>
939 <td>
940 <?php
941 echo empty( $event['SUMMARY'] )
942 ? '&nbsp;'
943 : $this->escape( stripslashes( $event['SUMMARY'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
944 ?>
945 </td>
946 <td>
947 <?php
948 echo empty( $event['DESCRIPTION'] )
949 ? '&nbsp;'
950 : wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
951 ?>
952 </td>
953 </tr>
954 <?php endforeach; ?>
955 </tbody>
956 </table>
957 <?php
958 endif;
959
960 $rendered = ob_get_clean();
961
962 if ( empty( $rendered ) ) {
963 return false;
964 }
965
966 return $rendered;
967 }
968
969 /**
970 * Return a localized string with information about the event's date and time,
971 * or starting date and end date.
972 *
973 * @param array $event Info about the event.
974 *
975 * @return string
976 */
977 public function formatted_date( $event ) {
978 $date_format = get_option( 'date_format' );
979 $time_format = get_option( 'time_format' );
980 $start = strtotime( $event['DTSTART'] );
981 $end = isset( $event['DTEND'] ) ? strtotime( $event['DTEND'] ) : false;
982
983 $all_day = ( 8 === strlen( $event['DTSTART'] ) );
984
985 if ( ! $all_day && $this->timezone ) {
986 try {
987 $timezone_offset = $this->timezone->getOffset( new DateTime( $event['DTSTART'] ) );
988 $start += $timezone_offset;
989
990 if ( $end ) {
991 $end += $timezone_offset;
992 }
993 } catch ( Exception $e ) {
994 // Invalid argument to DateTime.
995 return '';
996 }
997 }
998 $single_day = $end ? ( $end - $start ) <= DAY_IN_SECONDS : true;
999
1000 /* translators: Date and time */
1001 $date_with_time = __( '%1$s at %2$s', 'jetpack' );
1002 /* translators: Two dates with a separator */
1003 $two_dates = __( '%1$s &ndash; %2$s', 'jetpack' );
1004
1005 // we'll always have the start date. Maybe with time.
1006 if ( $all_day ) {
1007 $date = date_i18n( $date_format, $start );
1008 } else {
1009 $date = sprintf(
1010 $date_with_time,
1011 date_i18n( $date_format, $start ),
1012 date_i18n( $time_format, $start )
1013 );
1014 }
1015
1016 // single day, timed.
1017 if ( $single_day && ! $all_day && false !== $end ) {
1018 $date = sprintf( $two_dates, $date, date_i18n( $time_format, $end ) );
1019 }
1020
1021 // multi-day.
1022 if ( ! $single_day ) {
1023
1024 if ( $all_day ) {
1025 // DTEND for multi-day events represents "until", not "including", so subtract one minute.
1026 $end_date = date_i18n( $date_format, $end - 60 );
1027 } else {
1028 $end_date = sprintf( $date_with_time, date_i18n( $date_format, $end ), date_i18n( $time_format, $end ) );
1029 }
1030
1031 $date = sprintf( $two_dates, $date, $end_date );
1032
1033 }
1034
1035 return $date;
1036 }
1037
1038 /**
1039 * Sort list of events by event date.
1040 *
1041 * @param array $list List of events.
1042 *
1043 * @return array
1044 */
1045 protected function sort_by_recent( $list ) {
1046 $dates = array();
1047 $sorted_list = array();
1048
1049 foreach ( $list as $key => $row ) {
1050 $date = $row['DTSTART'];
1051 // pad some time onto an all day date.
1052 if ( 8 === strlen( $date ) ) {
1053 $date .= 'T000000Z';
1054 }
1055 $dates[ $key ] = $date;
1056 }
1057 asort( $dates );
1058 foreach ( $dates as $key => $value ) {
1059 $sorted_list[ $key ] = $list[ $key ];
1060 }
1061 unset( $list );
1062 return $sorted_list;
1063 }
1064
1065 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date
1066 }
1067
1068 /**
1069 * Wrapper function for iCalendarReader->get_events()
1070 *
1071 * @param string $url (default: '').
1072 * @param int $count Number of events to fetch.
1073 * @return array
1074 */
1075 function icalendar_get_events( $url = '', $count = 5 ) {
1076 /*
1077 * Find your calendar's address
1078 * https://support.google.com/calendar/bin/answer.py?hl=en&answer=37103
1079 */
1080 return ( new iCalendarReader() )->get_events( $url, $count );
1081 }
1082
1083 /**
1084 * Wrapper function for iCalendarReader->render()
1085 *
1086 * @param string $url (default: '').
1087 * @param array $args Options when rendering events.
1088 *
1089 * @return mixed bool|string false on failure, rendered HTML string on success.
1090 */
1091 function icalendar_render_events( $url = '', $args = array() ) {
1092 return ( new iCalendarReader() )->render( $url, $args );
1093 }
1094