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