PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 13.7.2
Jetpack – WP Security, Backup, Speed, & Growth v13.7.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 2 years ago core-api 2 years ago debugger 2 years ago markdown 2 years ago class-jetpack-ai-helper.php 2 years ago class-jetpack-currencies.php 2 years ago class-jetpack-google-drive-helper.php 2 years ago class-jetpack-instagram-gallery-helper.php 2 years ago class-jetpack-mapbox-helper.php 3 years ago class-jetpack-podcast-feed-locator.php 2 years ago class-jetpack-podcast-helper.php 2 years ago class-jetpack-recommendations.php 2 years ago class-jetpack-top-posts-helper.php 2 years ago class.color.php 2 years ago class.core-rest-api-endpoints.php 2 years ago class.jetpack-automatic-install-skin.php 2 years ago class.jetpack-iframe-embed.php 3 years ago class.jetpack-keyring-service-helper.php 2 years ago class.jetpack-password-checker.php 2 years ago class.jetpack-search-performance-logger.php 4 years ago class.media-extractor.php 2 years ago class.media-summary.php 2 years ago class.media.php 2 years ago components.php 3 years ago debugger.php 2 years ago functions.wp-notify.php 2 years ago icalendar-reader.php 2 years ago markdown.php 3 years ago plans.php 4 years ago plugins.php 4 years ago tonesque.php 2 years ago widgets.php 2 years ago
icalendar-reader.php
1088 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 && preg_match( '/^(-?\d)([A-Z]{2})/', $rrule_array['BYDAY'], $matches )
346 ) {
347 $day_number = $matches[1];
348 $week_day = $matches[2];
349
350 $day_cardinals = array(
351 -3 => 'third to last',
352 -2 => 'second to last',
353 -1 => 'last',
354 1 => 'first',
355 2 => 'second',
356 3 => 'third',
357 4 => 'fourth',
358 5 => 'fifth',
359 );
360 $weekdays = array(
361 'SU' => 'Sunday',
362 'MO' => 'Monday',
363 'TU' => 'Tuesday',
364 'WE' => 'Wednesday',
365 'TH' => 'Thursday',
366 'FR' => 'Friday',
367 'SA' => 'Saturday',
368 );
369
370 $day_cardinal = $day_cardinals[ $day_number ] ?? '';
371 $weekday = $weekdays[ $week_day ] ?? '';
372 $event_date_desc = "$day_cardinal $weekday of ";
373 } else {
374 $event_date_desc = date( 'd ', strtotime( $event['DTSTART'] ) );
375 }
376
377 // Interval only.
378 if ( $interval > 1 ) {
379 $catchup = 0;
380 $maybe = strtotime( $event['DTSTART'] );
381 while ( $maybe < $current ) {
382 $maybe = strtotime( '+ ' . ( $interval * $catchup ) . ' months', strtotime( $event['DTSTART'] ) );
383 ++$catchup;
384 }
385 $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'] ) );
386 } else {
387 $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . date( 'F Y', $current ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
388 }
389
390 // Add one interval if necessary.
391 if ( strtotime( $recurring_event_date_start ) < $current ) {
392 if ( $interval > 1 ) {
393 $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'] ) );
394 } else {
395 try {
396 $adjustment = new DateTime( date( 'Y-m-d', $current ) );
397 $adjustment->modify( 'first day of next month' );
398 $recurring_event_date_start = date( 'Ymd', strtotime( $event_date_desc . $adjustment->format( 'F Y' ) ) ) . date( '\THis', strtotime( $event['DTSTART'] ) );
399 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
400 // Invalid argument to DateTime.
401 }
402 }
403 }
404 }
405 break;
406
407 case 'YEARLY':
408 $frequency = 'year';
409 $echo_limit = 1;
410
411 if ( $date_from_ics >= $current ) {
412 $recurring_event_date_start = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) );
413 } else {
414 $recurring_event_date_start = date( 'Y', $current ) . date( 'md\THis', strtotime( $event['DTSTART'] ) );
415 if ( strtotime( $recurring_event_date_start ) < $current ) {
416 try {
417 $next = new DateTime( date( 'Y-m-d', $current ) );
418 $next->modify( 'first day of next year' );
419 $recurring_event_date_start = $next->format( 'Y' ) . date( 'md\THis', strtotime( $event['DTSTART'] ) );
420 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
421 // Invalid argument to DateTime.
422 }
423 }
424 }
425 break;
426
427 default:
428 $frequency = false;
429 }
430
431 if ( false !== $frequency && ! $noop ) {
432 $count_counter = 1;
433
434 // If no COUNT limit, go to 10.
435 if ( empty( $rrule_count ) ) {
436 $rrule_count = 10;
437 }
438
439 // Set up EXDATE handling for the event.
440 $exdates = ( isset( $event['EXDATE'] ) ) ? $event['EXDATE'] : array();
441
442 for ( $i = 1; $i <= $echo_limit; $i++ ) {
443
444 // Weeks need a daily loop and must check for inclusion in BYDAYS.
445 if ( 'week' === $frequency ) {
446 $byday_event_date_start = strtotime( $recurring_event_date_start );
447
448 foreach ( $weekdays as $day ) {
449
450 $event_start_timestamp = $byday_event_date_start;
451 $start_time = date( 'His', $event_start_timestamp );
452 $event_end_timestamp = $event_start_timestamp + $duration;
453 $end_time = date( 'His', $event_end_timestamp );
454 if ( 8 === strlen( $event['DTSTART'] ) ) {
455 $exdate_compare = date( 'Ymd', $event_start_timestamp );
456 } else {
457 $exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
458 }
459
460 if (
461 in_array( $day, $bydays, true )
462 && $event_end_timestamp > $current
463 && $event_start_timestamp < $until
464 && $count_counter <= $rrule_count
465 && $event_start_timestamp >= $date_from_ics
466 && ! in_array( $exdate_compare, $exdates, true )
467 ) {
468 if ( 8 === strlen( $event['DTSTART'] ) ) {
469 $event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
470 $event['DTEND'] = date( 'Ymd', $event_end_timestamp );
471 } else {
472 $event['DTSTART'] = date( 'Ymd\THis', $event_start_timestamp );
473 $event['DTEND'] = date( 'Ymd\THis', $event_end_timestamp );
474 }
475 if ( $this->timezone->getName() && 8 !== strlen( $event['DTSTART'] ) ) {
476 try {
477 $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
478 $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
479 $event['DTSTART'] = $adjusted_time->format( 'Ymd\THis' );
480
481 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
482 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
483 // Invalid argument to DateTime.
484 }
485 }
486 $upcoming[] = $event;
487 ++$count_counter;
488 }
489
490 // Move forward one day.
491 $byday_event_date_start = strtotime( date( 'Ymd\T', strtotime( '+ 1 day', $event_start_timestamp ) ) . $start_time );
492 }
493
494 // Restore first event timestamp.
495 $event_start_timestamp = strtotime( $recurring_event_date_start );
496
497 } else {
498
499 $event_start_timestamp = strtotime( $recurring_event_date_start );
500 $start_time = date( 'His', $event_start_timestamp );
501 $event_end_timestamp = $event_start_timestamp + $duration;
502 $end_time = date( 'His', $event_end_timestamp );
503 if ( 8 === strlen( $event['DTSTART'] ) ) {
504 $exdate_compare = date( 'Ymd', $event_start_timestamp );
505 } else {
506 $exdate_compare = date( 'Ymd\THis', $event_start_timestamp );
507 }
508
509 if (
510 $event_end_timestamp > $current
511 && $event_start_timestamp < $until
512 && $count_counter <= $rrule_count
513 && $event_start_timestamp >= $date_from_ics
514 && ! in_array( $exdate_compare, $exdates, true )
515 ) {
516 if ( 8 === strlen( $event['DTSTART'] ) ) {
517 $event['DTSTART'] = date( 'Ymd', $event_start_timestamp );
518 $event['DTEND'] = date( 'Ymd', $event_end_timestamp );
519 } else {
520 $event['DTSTART'] = date( 'Ymd\T', $event_start_timestamp ) . $start_time;
521 $event['DTEND'] = date( 'Ymd\T', $event_end_timestamp ) . $end_time;
522 }
523 if ( $this->timezone->getName() && 8 !== strlen( $event['DTSTART'] ) ) {
524 try {
525 $adjusted_time = new DateTime( $event['DTSTART'], new DateTimeZone( $this->timezone->getName() ) );
526 $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
527 $event['DTSTART'] = $adjusted_time->format( 'Ymd\THis' );
528
529 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
530 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
531 // Invalid argument to DateTime.
532 }
533 }
534 $upcoming[] = $event;
535 ++$count_counter;
536 }
537 }
538
539 // Set up next interval and reset $event['DTSTART'] and $event['DTEND'], keeping timestamps intact.
540 $next_start_timestamp = strtotime( "+ {$interval} {$frequency}s", $event_start_timestamp );
541 if ( 8 === strlen( $event['DTSTART'] ) ) {
542 $event['DTSTART'] = date( 'Ymd', $next_start_timestamp );
543 $event['DTEND'] = date( 'Ymd', strtotime( $event['DTSTART'] ) + $duration );
544 } else {
545 $event['DTSTART'] = date( 'Ymd\THis', $next_start_timestamp );
546 $event['DTEND'] = date( 'Ymd\THis', strtotime( $event['DTSTART'] ) + $duration );
547 }
548
549 // Move recurring event date forward.
550 $recurring_event_date_start = $event['DTSTART'];
551 }
552 $set_recurring_events[] = $uid;
553
554 }
555 } elseif ( strtotime( isset( $event['DTEND'] ) ? $event['DTEND'] : $event['DTSTART'] ) >= $current ) { // Process normal events.
556 $upcoming[] = $event;
557 }
558 }
559 return $upcoming;
560 }
561
562 /**
563 * Parse events from an iCalendar feed
564 *
565 * @param string $url (default: '').
566 * @return array | false on failure
567 */
568 public function parse( $url = '' ) {
569 $cache_group = 'icalendar_reader_parse';
570 $disable_get_key = 'disable:' . md5( $url );
571
572 // Check to see if previous attempts have failed.
573 if ( false !== wp_cache_get( $disable_get_key, $cache_group ) ) {
574 return false;
575 }
576
577 // rewrite webcal: URI scheme to HTTP.
578 $url = preg_replace( '/^webcal/', 'http', $url );
579 // try to fetch.
580 $r = wp_safe_remote_get(
581 $url,
582 array(
583 'timeout' => 3,
584 'sslverify' => false,
585 )
586 );
587 if ( 200 !== wp_remote_retrieve_response_code( $r ) ) {
588 // We were unable to fetch any content, so don't try again for another 60 seconds.
589 wp_cache_set( $disable_get_key, 1, $cache_group, 60 );
590 return false;
591 }
592
593 $body = wp_remote_retrieve_body( $r );
594 if ( empty( $body ) ) {
595 return false;
596 }
597
598 $body = str_replace( "\r\n", "\n", $body );
599 $lines = preg_split( "/\n(?=[A-Z])/", $body );
600
601 if ( empty( $lines ) ) {
602 return false;
603 }
604
605 if ( false === stristr( $lines[0], 'BEGIN:VCALENDAR' ) ) {
606 return false;
607 }
608
609 $type = '';
610 foreach ( $lines as $line ) {
611 $add = $this->key_value_from_string( $line );
612 if ( ! $add ) {
613 $this->add_component( $type, false, $line );
614 continue;
615 }
616 list( $keyword, $value ) = $add;
617
618 switch ( $keyword ) {
619 case 'BEGIN':
620 case 'END':
621 switch ( $line ) {
622 case 'BEGIN:VTODO':
623 ++$this->todo_count;
624 $type = 'VTODO';
625 break;
626 case 'BEGIN:VEVENT':
627 ++$this->event_count;
628 $type = 'VEVENT';
629 break;
630 case 'BEGIN:VCALENDAR':
631 case 'BEGIN:DAYLIGHT':
632 case 'BEGIN:VTIMEZONE':
633 case 'BEGIN:STANDARD':
634 $type = $value;
635 break;
636 case 'END:VTODO':
637 case 'END:VEVENT':
638 case 'END:VCALENDAR':
639 case 'END:DAYLIGHT':
640 case 'END:VTIMEZONE':
641 case 'END:STANDARD':
642 $type = 'VCALENDAR';
643 break;
644 }
645 break;
646 case 'TZID':
647 if (
648 'VTIMEZONE' === $type
649 && ! $this->timezone
650 ) {
651 $this->timezone = $this->timezone_from_string( $value );
652 }
653 break;
654 case 'X-WR-TIMEZONE':
655 if ( ! $this->timezone ) {
656 $this->timezone = $this->timezone_from_string( $value );
657 }
658 break;
659 default:
660 $this->add_component( $type, $keyword, $value );
661 break;
662 }
663 }
664
665 // Filter for RECURRENCE-IDs.
666 $recurrences = array();
667 if ( array_key_exists( 'VEVENT', $this->cal ) ) {
668 foreach ( $this->cal['VEVENT'] as $event ) {
669 if ( isset( $event['RECURRENCE-ID'] ) ) {
670 $recurrences[] = $event;
671 }
672 }
673 foreach ( $recurrences as $recurrence ) {
674 $count_vevent = count( $this->cal['VEVENT'] );
675 for ( $i = 0; $i < $count_vevent; $i++ ) {
676 if (
677 $this->cal['VEVENT'][ $i ]['UID'] === $recurrence['UID']
678 && ! isset( $this->cal['VEVENT'][ $i ]['RECURRENCE-ID'] )
679 ) {
680 $this->cal['VEVENT'][ $i ]['EXDATE'][] = $recurrence['RECURRENCE-ID'];
681 break;
682 }
683 }
684 }
685 }
686
687 return $this->cal;
688 }
689
690 /**
691 * Parse key:value from a string
692 *
693 * @param string $text (default: '').
694 * @return array
695 */
696 public function key_value_from_string( $text = '' ) {
697 preg_match( '/([^:]+)(;[^:]+)?[:]([\w\W]*)/', $text, $matches );
698
699 if ( 0 === count( $matches ) ) {
700 return false;
701 }
702
703 return array( $matches[1], $matches[3] );
704 }
705
706 /**
707 * Convert a timezone name into a timezone object.
708 *
709 * @param string $text Timezone name. Example: America/Chicago.
710 * @return object|null A DateTimeZone object if the conversion was successful.
711 */
712 private function timezone_from_string( $text ) {
713 try {
714 $timezone = new DateTimeZone( $text );
715 } catch ( Exception $e ) {
716 $blog_timezone = get_option( 'timezone_string' );
717 if ( ! $blog_timezone ) {
718 $blog_timezone = 'Etc/UTC';
719 }
720
721 $timezone = new DateTimeZone( $blog_timezone );
722 }
723
724 return $timezone;
725 }
726
727 /**
728 * Add a component to the calendar array
729 *
730 * @param string $component (default: '').
731 * @param bool|string $keyword (default: '').
732 * @param string $value (default: '').
733 * @return void
734 */
735 public function add_component( $component = '', $keyword = '', $value = '' ) {
736 if ( ! $keyword ) {
737 $keyword = $this->last_keyword;
738 switch ( $component ) {
739 case 'VEVENT':
740 $value = $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] . $value;
741 break;
742 case 'VTODO':
743 $value = $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] . $value;
744 break;
745 }
746 }
747
748 /*
749 * Some events have a specific timezone set in their start/end date,
750 * and it may or may not be different than the calendar timzeone.
751 * Valid formats include:
752 * DTSTART;TZID=Pacific Standard Time:20141219T180000
753 * DTEND;TZID=Pacific Standard Time:20141219T200000
754 * EXDATE:19960402T010000Z,19960403T010000Z,19960404T010000Z
755 * EXDATE;VALUE=DATE:2015050
756 * EXDATE;TZID=America/New_York:20150424T170000
757 * EXDATE;TZID=Pacific Standard Time:20120615T140000,20120629T140000,20120706T140000
758 */
759
760 // Always store EXDATE as an array.
761 if ( stristr( $keyword, 'EXDATE' ) ) {
762 $value = explode( ',', $value );
763 }
764
765 // Adjust DTSTART, DTEND, and EXDATE according to their TZID if set.
766 if ( strpos( $keyword, ';' ) && ( stristr( $keyword, 'DTSTART' ) || stristr( $keyword, 'DTEND' ) || stristr( $keyword, 'EXDATE' ) || stristr( $keyword, 'RECURRENCE-ID' ) ) ) {
767 $keyword = explode( ';', $keyword );
768
769 $tzid = false;
770 if ( 2 === count( $keyword ) ) {
771 $tparam = $keyword[1];
772
773 if ( str_contains( $tparam, 'TZID' ) ) {
774 $tzid = $this->timezone_from_string( str_replace( 'TZID=', '', $tparam ) );
775 }
776 }
777
778 // Normalize all times to default UTC.
779 if ( $tzid ) {
780 $adjusted_times = array();
781 foreach ( (array) $value as $v ) {
782 try {
783 $adjusted_time = new DateTime( $v, $tzid );
784 $adjusted_time->setTimeZone( new DateTimeZone( 'UTC' ) );
785 $adjusted_times[] = $adjusted_time->format( 'Ymd\THis' );
786 } catch ( Exception $e ) {
787 // Invalid argument to DateTime.
788 return;
789 }
790 }
791 $value = $adjusted_times;
792 }
793
794 // Format for adding to event.
795 $keyword = $keyword[0];
796 if ( 'EXDATE' !== $keyword ) {
797 $value = implode( (array) $value );
798 }
799 }
800
801 foreach ( (array) $value as $v ) {
802 switch ( $component ) {
803 case 'VTODO':
804 if ( 'EXDATE' === $keyword ) {
805 $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ][] = $v;
806 } else {
807 $this->cal[ $component ][ $this->todo_count - 1 ][ $keyword ] = $v;
808 }
809 break;
810 case 'VEVENT':
811 if ( 'EXDATE' === $keyword ) {
812 $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ][] = $v;
813 } else {
814 $this->cal[ $component ][ $this->event_count - 1 ][ $keyword ] = $v;
815 }
816 break;
817 default:
818 $this->cal[ $component ][ $keyword ] = $v;
819 break;
820 }
821 }
822 $this->last_keyword = $keyword;
823 }
824
825 /**
826 * Escape strings with wp_kses, allow links
827 *
828 * @param string $string (default: '') The string to escape.
829 * @return string
830 */
831 public function escape( $string = '' ) {
832 // Unfold content lines per RFC 5545.
833 $string = str_replace( "\n\t", '', $string );
834 $string = str_replace( "\n ", '', $string );
835
836 $allowed_html = array(
837 'a' => array(
838 'href' => array(),
839 'title' => array(),
840 ),
841 );
842
843 $allowed_tags = '';
844 foreach ( array_keys( $allowed_html ) as $tag ) {
845 $allowed_tags .= "<{$tag}>";
846 }
847
848 // Running strip_tags() first with allowed tags to get rid of remaining gallery markup, etc
849 // because wp_kses() would only htmlentity'fy that. Then still running wp_kses(), for extra
850 // safety and good measure.
851 return wp_kses( strip_tags( $string, $allowed_tags ), $allowed_html );
852 }
853
854 /**
855 * Render the events
856 *
857 * @param string $url (default: '') URL of the iCal feed.
858 * @param array $args Event options.
859 *
860 * @return mixed bool|string false on failure, rendered HTML string on success.
861 */
862 public function render( $url = '', $args = array() ) {
863
864 $args = wp_parse_args(
865 $args,
866 array(
867 'context' => 'widget',
868 'number' => 5,
869 )
870 );
871
872 $events = $this->get_events( $url, $args['number'] );
873 $events = $this->apply_timezone_offset( $events );
874
875 if ( empty( $events ) ) {
876 return false;
877 }
878
879 ob_start();
880
881 if ( 'widget' === $args['context'] ) : ?>
882 <ul class="upcoming-events">
883 <?php foreach ( $events as $event ) : ?>
884 <li>
885 <strong class="event-summary">
886 <?php
887 echo $this->escape( stripslashes( $event['SUMMARY'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
888 ?>
889 </strong>
890 <span class="event-when"><?php echo esc_html( $this->formatted_date( $event ) ); ?></span>
891 <?php if ( ! empty( $event['LOCATION'] ) ) : ?>
892 <span class="event-location">
893 <?php
894 echo $this->escape( stripslashes( $event['LOCATION'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
895 ?>
896 </span>
897 <?php endif; ?>
898 <?php if ( ! empty( $event['DESCRIPTION'] ) ) : ?>
899 <span class="event-description">
900 <?php
901 echo wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
902 ?>
903 </span>
904 <?php endif; ?>
905 </li>
906 <?php endforeach; ?>
907 </ul>
908 <?php
909 endif;
910
911 if ( 'shortcode' === $args['context'] ) :
912 ?>
913 <table class="upcoming-events">
914 <thead>
915 <tr>
916 <th><?php esc_html_e( 'Location', 'jetpack' ); ?></th>
917 <th><?php esc_html_e( 'When', 'jetpack' ); ?></th>
918 <th><?php esc_html_e( 'Summary', 'jetpack' ); ?></th>
919 <th><?php esc_html_e( 'Description', 'jetpack' ); ?></th>
920 </tr>
921 </thead>
922 <tbody>
923 <?php foreach ( $events as $event ) : ?>
924 <tr>
925 <td>
926 <?php
927 echo empty( $event['LOCATION'] )
928 ? '&nbsp;'
929 : $this->escape( stripslashes( $event['LOCATION'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
930 ?>
931 </td>
932 <td><?php echo esc_html( $this->formatted_date( $event ) ); ?></td>
933 <td>
934 <?php
935 echo empty( $event['SUMMARY'] )
936 ? '&nbsp;'
937 : $this->escape( stripslashes( $event['SUMMARY'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
938 ?>
939 </td>
940 <td>
941 <?php
942 echo empty( $event['DESCRIPTION'] )
943 ? '&nbsp;'
944 : wp_trim_words( $this->escape( stripcslashes( $event['DESCRIPTION'] ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- this method is built to escape.
945 ?>
946 </td>
947 </tr>
948 <?php endforeach; ?>
949 </tbody>
950 </table>
951 <?php
952 endif;
953
954 $rendered = ob_get_clean();
955
956 if ( empty( $rendered ) ) {
957 return false;
958 }
959
960 return $rendered;
961 }
962
963 /**
964 * Return a localized string with information about the event's date and time,
965 * or starting date and end date.
966 *
967 * @param array $event Info about the event.
968 *
969 * @return string
970 */
971 public function formatted_date( $event ) {
972 $date_format = get_option( 'date_format' );
973 $time_format = get_option( 'time_format' );
974 $start = strtotime( $event['DTSTART'] );
975 $end = isset( $event['DTEND'] ) ? strtotime( $event['DTEND'] ) : false;
976
977 $all_day = ( 8 === strlen( $event['DTSTART'] ) );
978
979 if ( ! $all_day && $this->timezone ) {
980 try {
981 $timezone_offset = $this->timezone->getOffset( new DateTime( $event['DTSTART'] ) );
982 $start += $timezone_offset;
983
984 if ( $end ) {
985 $end += $timezone_offset;
986 }
987 } catch ( Exception $e ) {
988 // Invalid argument to DateTime.
989 return '';
990 }
991 }
992 $single_day = $end ? ( $end - $start ) <= DAY_IN_SECONDS : true;
993
994 /* translators: Date and time */
995 $date_with_time = __( '%1$s at %2$s', 'jetpack' );
996 /* translators: Two dates with a separator */
997 $two_dates = __( '%1$s &ndash; %2$s', 'jetpack' );
998
999 // we'll always have the start date. Maybe with time.
1000 if ( $all_day ) {
1001 $date = date_i18n( $date_format, $start );
1002 } else {
1003 $date = sprintf(
1004 $date_with_time,
1005 date_i18n( $date_format, $start ),
1006 date_i18n( $time_format, $start )
1007 );
1008 }
1009
1010 // single day, timed.
1011 if ( $single_day && ! $all_day && false !== $end ) {
1012 $date = sprintf( $two_dates, $date, date_i18n( $time_format, $end ) );
1013 }
1014
1015 // multi-day.
1016 if ( ! $single_day ) {
1017
1018 if ( $all_day ) {
1019 // DTEND for multi-day events represents "until", not "including", so subtract one minute.
1020 $end_date = date_i18n( $date_format, $end - 60 );
1021 } else {
1022 $end_date = sprintf( $date_with_time, date_i18n( $date_format, $end ), date_i18n( $time_format, $end ) );
1023 }
1024
1025 $date = sprintf( $two_dates, $date, $end_date );
1026
1027 }
1028
1029 return $date;
1030 }
1031
1032 /**
1033 * Sort list of events by event date.
1034 *
1035 * @param array $list List of events.
1036 *
1037 * @return array
1038 */
1039 protected function sort_by_recent( $list ) {
1040 $dates = array();
1041 $sorted_list = array();
1042
1043 foreach ( $list as $key => $row ) {
1044 $date = $row['DTSTART'];
1045 // pad some time onto an all day date.
1046 if ( 8 === strlen( $date ) ) {
1047 $date .= 'T000000Z';
1048 }
1049 $dates[ $key ] = $date;
1050 }
1051 asort( $dates );
1052 foreach ( $dates as $key => $value ) {
1053 $sorted_list[ $key ] = $list[ $key ];
1054 }
1055 unset( $list );
1056 return $sorted_list;
1057 }
1058
1059 // phpcs:enable WordPress.DateTime.RestrictedFunctions.date_date
1060 }
1061
1062 /**
1063 * Wrapper function for iCalendarReader->get_events()
1064 *
1065 * @param string $url (default: '').
1066 * @param int $count Number of events to fetch.
1067 * @return array
1068 */
1069 function icalendar_get_events( $url = '', $count = 5 ) {
1070 /*
1071 * Find your calendar's address
1072 * https://support.google.com/calendar/bin/answer.py?hl=en&answer=37103
1073 */
1074 return ( new iCalendarReader() )->get_events( $url, $count );
1075 }
1076
1077 /**
1078 * Wrapper function for iCalendarReader->render()
1079 *
1080 * @param string $url (default: '').
1081 * @param array $args Options when rendering events.
1082 *
1083 * @return mixed bool|string false on failure, rendered HTML string on success.
1084 */
1085 function icalendar_render_events( $url = '', $args = array() ) {
1086 return ( new iCalendarReader() )->render( $url, $args );
1087 }
1088