PluginProbe
Booking Manager – Sync WP Booking Calendar – Import Events, Export Bookings to ICS Calendar / 2.1.21
Booking Manager – Sync WP Booking Calendar – Import Events, Export Bookings to ICS Calendar v2.1.21
2.1.22 2.1.21 2.1.20 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 trunk 1.1 2.0 2.0.1 2.0.10.2 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.17 2.0.18 2.0.2 2.0.20 2.0.21 All 56 releases
← All changes | core/wpbc/wpbm-bc-export.php +632 -242 2.1.9 → 2.1.21 View file →
@@ -149,9 +149,9 @@
149 149 ////////////////////////////////////////////////////////////////////////////////////////////////////////////
150 150 // Set Headers before file download
151 151 ////////////////////////////////////////////////////////////////////////////////////////////////////////////
152 152 $file = array();
153 - $file['content_type'] = 'text/calendar';
153 + $file['content_type'] = 'text/calendar; charset=UTF-8';
154 154 $file['name'] = 'wpbm.ics';
155 155
156 156 if ( function_exists( 'mb_detect_encoding' ) ) //FixIn: 2.0.5.3
157 157 $text_encoding = mb_detect_encoding( $ics_export );
@@ -172,9 +172,9 @@
172 172 header( "Robots: none" . "\n" );
173 173 @header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 200 OK' . "\n" );
174 174 header( "Content-Type: " . $file['content_type'] . "\n" );
175 175 header( "Content-Description: File Transfer" . "\n" );
176 - header( "Content-Disposition: attachment; filename=\"" . $file['name'] . "\"" . "\n" );
176 + header( "Content-Disposition: inline; filename=\"" . $file['name'] . "\"" . "\n" );
177 177 header( "Content-Transfer-Encoding: binary" . "\n" );
178 178
179 179 if ( (int) $file['size'] > 0 )
180 180 header( "Content-Length: " . $file['size'] . "\n" );
@@ -190,310 +190,699 @@
190 190 }
191 191 add_action( 'template_redirect', 'wpbm_make_export_ics_feeds' );
192 192
193 193
194 +// ---------------------------------------------------------------------------------------------------------------------
195 +// Helpers for ICS export:
196 +// ---------------------------------------------------------------------------------------------------------------------
194 197
195 -/** Define export ICS here
196 - * For testing: http://beta/?feed=wpbm-ics
198 +/**
199 + * Escape ICS text per RFC 5545: backslash, comma, semicolon; newlines -> \n
200 + *
201 + * @param string $text
202 + *
203 + * @return string
197 204 */
198 -function wpbm_export_ics_feed__wpbm_ics( $param = array( 'wh_booking_type' => '1', 'wh_trash' => '' ) ) { //FixIn: 2.0.2.3
205 +function wpbc_ics_escape_text( $text ) {
206 + $text = (string) $text;
207 + $text = str_replace( array( "\\", ";", "," ), array( "\\\\", "\;", "\," ), $text );
208 + $text = str_replace( array( "\r\n", "\r" ), "\n", $text );
209 + $text = str_replace( "\n", "\\n", $text ); // single backslash-n.
199 210
200 - if ( ! function_exists( 'wpbc_api_get_bookings_arr' ) )
201 - return '';
211 + return $text;
212 +}
202 213
203 -// // Start date of getting bookings
204 -// $real_date = strtotime( '-1 year' );
205 -// $wh_booking_date = date_i18n( "Y-m-d", $real_date );
206 -// $param['wh_booking_date'] = $wh_booking_date;
207 - // End date of getting bookings
208 - $real_date = strtotime( '+2 years' ); //FixIn: 2.0.7.1
209 - $wh_booking_date2 = date_i18n( "Y-m-d", $real_date );
210 - $param['wh_booking_date2' ] = $wh_booking_date2;
214 +/**
215 + * Format SQL local datetime ("Y-m-d H:i:s") as UTC Z iCal (YYYYMMDDTHHMMSSZ).
216 + *
217 + * @param string|null $sql_dt Local SQL datetime or null for "now"
218 + * @param DateTimeZone $source_tz
219 + *
220 + * @return string
221 + */
222 +function wpbc_to_ics_utc( $sql_dt, DateTimeZone $source_tz ) {
223 + $dt = $sql_dt ? DateTime::createFromFormat( 'Y-m-d H:i:s', $sql_dt, $source_tz )
224 + : new DateTime( 'now', $source_tz );
225 + if ( ! $dt ) {
226 + $dt = new DateTime( 'now', $source_tz );
227 + }
228 + $dt->setTimezone( new DateTimeZone( 'UTC' ) );
211 229
212 - // Export only approved bookings //FixIn: 2.0.11.1 //FixIn: 8.5.2.3
213 - $booking_is_ics_export_only_approved = get_bk_option( 'booking_is_ics_export_only_approved' );
214 - if ( 'On' == $booking_is_ics_export_only_approved ) {
215 - $param['wh_approved'] = '1';
230 + return $dt->format( 'Ymd\THis\Z' );
231 +}
232 +
233 +/**
234 + * Format SQL local datetime as local (no Z) in iCal basic (YYYYMMDDTHHMMSS).
235 + *
236 + * @param string $sql_dt
237 + * @param DateTimeZone $tz
238 + *
239 + * @return string
240 + */
241 +function wpbc_to_ics_local( $sql_dt, DateTimeZone $tz ) {
242 + $dt = DateTime::createFromFormat( 'Y-m-d H:i:s', $sql_dt, $tz );
243 + if ( ! $dt ) {
244 + $dt = new DateTime( $sql_dt ?: 'now', $tz );
245 + }
246 +
247 + return $dt->format( 'Ymd\THis' );
248 +}
249 +
250 +/**
251 + * Normalize seconds to :00 for clean slots (avoid :01/:02 artifacts).
252 + *
253 + * @param string $sql_dt
254 + * @param DateTimeZone $tz
255 + *
256 + * @return string "Y-m-d H:i:00" on success, original input if parsing fails
257 + */
258 +function wpbc_normalize_sec_sql( $sql_dt, DateTimeZone $tz ) {
259 + $dt = DateTime::createFromFormat( 'Y-m-d H:i:s', $sql_dt, $tz );
260 + if ( ! $dt ) {
261 + return $sql_dt;
262 + }
263 +
264 + return $dt->format( 'Y-m-d H:i:00' );
265 +}
266 +
267 +/**
268 + * Add/subtract whole days to a Y-m-d in a specific tz (DST-safe), return Y-m-d.
269 + *
270 + * @param string $ymd "Y-m-d"
271 + * @param int $days can be negative
272 + * @param DateTimeZone $tz
273 + *
274 + * @return string "Y-m-d"
275 + */
276 +function wpbc_add_days_ymd( $ymd, $days, DateTimeZone $tz ) {
277 + $dt = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $ymd . ' 00:00:00', $tz );
278 + if ( ! $dt ) {
279 + $dt = new DateTimeImmutable( 'now', $tz );
280 + }
281 + $modifier = ( $days >= 0 ? '+' : '' ) . $days . ' days';
282 +
283 + return $dt->modify( $modifier )->format( 'Y-m-d' );
284 +}
285 +
286 +/**
287 + * Ensure DTEND is strictly after DTSTART (RFC 5545).
288 + *
289 + * - All-day mode: treat inputs as Y-m-d; if end <= start → end = start + 1 day (exclusive end).
290 + * - Timed mode : normalize to 'Y-m-d H:i:s'; if end <= start → end = start + 1 hour.
291 + *
292 + * @param string $start_in 'Y-m-d' or 'Y-m-d H:i:s'
293 + * @param string $end_in 'Y-m-d' or 'Y-m-d H:i:s' (can be empty)
294 + * @param DateTimeZone $tz
295 + * @param bool $all_day
296 + *
297 + * @return array [ $start_out, $end_out ] (all-day → Y-m-d; timed → 'Y-m-d H:i:00')
298 + */
299 +function wpbc_ics_coerce_order( $start_in, $end_in, DateTimeZone $tz, $all_day ) {
300 +
301 + // Helper for all-day date math.
302 + $_add_days = function( $ymd, $days ) use ( $tz ) {
303 + return wpbc_add_days_ymd( $ymd, $days, $tz );
304 + };
305 +
306 + if ( $all_day ) {
307 + $sd = substr( (string) $start_in, 0, 10 );
308 + $ed = substr( (string) ($end_in !== '' ? $end_in : $sd), 0, 10 );
309 + if ( $ed <= $sd ) {
310 + $ed = $_add_days( $sd, 1 );
311 + }
312 + return array( $sd, $ed );
313 + }
314 +
315 + // Timed mode: normalize to full SQL datetimes.
316 + $ss = ( strlen( (string) $start_in ) > 10 ) ? $start_in : ( substr( (string) $start_in, 0, 10 ) . ' 00:00:00' );
317 + $ee = ( $end_in !== '' )
318 + ? ( ( strlen( (string) $end_in ) > 10 ) ? $end_in : ( substr( (string) $end_in, 0, 10 ) . ' 00:00:00' ) )
319 + : '';
320 +
321 + $ds = DateTime::createFromFormat( 'Y-m-d H:i:s', $ss, $tz );
322 + if ( ! $ds ) { $ds = new DateTime( $ss ?: 'now', $tz ); }
323 +
324 + if ( $ee === '' ) {
325 + $de = clone $ds;
326 + $de->modify( '+1 hour' );
216 327 } else {
217 - $param['wh_approved'] = '';
328 + $de = DateTime::createFromFormat( 'Y-m-d H:i:s', $ee, $tz );
329 + if ( ! $de ) { $de = new DateTime( $ee, $tz ); }
218 330 }
331 + if ( $de <= $ds ) {
332 + $de = clone $ds;
333 + $de->modify( '+1 hour' );
334 + }
335 + return array( $ds->format( 'Y-m-d H:i:00' ), $de->format( 'Y-m-d H:i:00' ) );
336 +}
219 337
220 - //'' | 'imported' | 'plugin' //FixIn: 8.8.3.19 //FixIn: 2.0.20.2
221 - if ( 'imported' == get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) {
222 - $param['wh_sync_gid'] = 'imported';
338 +
339 +/**
340 + * Add a VTIMEZONE node exactly once per TZID.
341 + *
342 + * @param ZCiCal $icalobj
343 + * @param string $tzid
344 + * @param string|int $year_start "YYYY"
345 + * @param string|int $year_end "YYYY"
346 + * @param array $added_ref reference array to track added TZIDs
347 + *
348 + * @return void
349 + */
350 +/**
351 + * Add a VTIMEZONE node exactly once per TZID.
352 + * - For Europe/* zones: emit a compact RRULE-based CET/CEST block + X-LIC-LOCATION.
353 + * - For other zones: fall back to library helper (detailed transitions).
354 + */
355 +function wpbc_add_vtimezone_once( $icalobj, $tzid, $year_start, $year_end, array &$added_ref ) {
356 + if ( empty( $tzid ) ) {
357 + return;
223 358 }
224 - if ( 'plugin' == get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) {
225 - $param['wh_sync_gid'] = 'plugin';
359 + if ( isset( $added_ref[ $tzid ] ) ) {
360 + return;
226 361 }
227 362
363 + // Lightweight CET/CEST definition for Europe/* (covers Europe/Prague and peers).
364 + if ( strpos( $tzid, 'Europe/' ) === 0 ) {
365 + $vtz = new ZCiCalNode( 'VTIMEZONE', $icalobj->curnode );
366 + $vtz->addNode( new ZCiCalDataNode( 'TZID:' . $tzid ) );
367 + $vtz->addNode( new ZCiCalDataNode( 'X-LIC-LOCATION:' . $tzid ) );
228 368
369 + // STANDARD: last Sunday in October 03:00 -> CET (UTC+1).
370 + $std = new ZCiCalNode( 'STANDARD', $vtz );
371 + $std->addNode( new ZCiCalDataNode( 'DTSTART:19961027T030000' ) );
372 + $std->addNode( new ZCiCalDataNode( 'TZOFFSETFROM:+0200' ) );
373 + $std->addNode( new ZCiCalDataNode( 'TZOFFSETTO:+0100' ) );
374 + $std->addNode( new ZCiCalDataNode( 'TZNAME:CET' ) );
375 + $std->addNode( new ZCiCalDataNode( 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' ) );
229 376
230 - $_REQUEST['view_mode']= 'vm_calendar'; //FixIn: 8.5.2.15 2.0.11.2
231 - // Get array of bookings.
232 - $bookings_arr = wpbc_api_get_bookings_arr( $param );
377 + // DAYLIGHT: last Sunday in March 02:00 -> CEST (UTC+2).
378 + $dst = new ZCiCalNode( 'DAYLIGHT', $vtz );
379 + $dst->addNode( new ZCiCalDataNode( 'DTSTART:19960331T020000' ) );
380 + $dst->addNode( new ZCiCalDataNode( 'TZOFFSETFROM:+0100' ) );
381 + $dst->addNode( new ZCiCalDataNode( 'TZOFFSETTO:+0200' ) );
382 + $dst->addNode( new ZCiCalDataNode( 'TZNAME:CEST' ) );
383 + $dst->addNode( new ZCiCalDataNode( 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' ) );
233 384
385 + } else {
386 + // Fallback: detailed transitions for non‑Europe zones (safer across the globe).
387 + ZCTimeZoneHelper::getTZNode( (string) $year_start, (string) $year_end, $tzid, $icalobj->curnode );
388 + }
234 389
235 - ob_start();
236 -
237 - // create the ical object
238 - $icalobj = new ZCiCal();
390 + $added_ref[ $tzid ] = true;
391 +}
239 392
393 +// ---------------------------------------------------------------------------------------------------------------------
394 +// Export ICS here.
395 +// ---------------------------------------------------------------------------------------------------------------------
396 +// FixIn: 2.1.20.1
397 +/**
398 + * Build the shortened booking-date representation used by the iCalendar exporter.
399 + *
400 + * This rebuild is required after child-resource dates have been removed from a
401 + * parent-resource feed. It follows the same range-compression rules as the
402 + * Booking Calendar booking-list API.
403 + *
404 + * @param array $booking_dates Booking-date objects for one booking.
405 + * @param int $main_resource_id Resource ID stored on the main booking row.
406 + *
407 + * @return array {
408 + * Shortened dates and their effective booking-resource IDs.
409 + *
410 + * @type array $dates_short Date values and range separators.
411 + * @type array $dates_short_id Effective resource IDs matching the date values.
412 + * }
413 + */
414 +function wpbc_ics_export_build_short_dates( $booking_dates, $main_resource_id ) {
240 415
241 - // 5.7. REFRESH-INTERVAL Property: https://www.rfc-editor.org/rfc/rfc7986.html#section-5.7 https://icalendar.org/New-Properties-for-iCalendar-RFC-7986/5-7-refresh-interval-property.html
416 + $dates_short = array();
417 + $dates_short_id = array();
418 + $last_date = '';
419 + $last_date_id = '';
420 + $last_token = '';
242 421
243 - /**
244 - * 4.3.6 Duration https://www.rfc-editor.org/rfc/rfc2445.txt
422 + foreach ( $booking_dates as $booking_date ) {
423 + $current_date = $booking_date->booking_date;
424 + $current_date_resource_id = ! empty( $booking_date->type_id )
425 + ? intval( $booking_date->type_id )
426 + : intval( $main_resource_id );
245 427
246 - Value Name: DURATION
428 + if ( '' === $last_date ) {
429 + $dates_short[] = $current_date;
430 + $dates_short_id[] = $current_date_resource_id;
431 + $last_token = $current_date;
432 + } elseif ( wpbc_is_less_than_next_day( $current_date, $last_date, true ) ) {
433 + if ( '-' !== $last_token ) {
434 + $dates_short[] = '-';
435 + $dates_short_id[] = '';
436 + }
437 + $last_token = '-';
438 + } else {
439 + if ( $last_token !== $last_date ) {
440 + $dates_short[] = $last_date;
441 + $dates_short_id[] = $last_date_id;
442 + }
247 443
248 - Purpose: This value type is used to identify properties that contain
249 - a duration of time.
444 + $dates_short[] = ',';
445 + $dates_short_id[] = '';
446 + $dates_short[] = $current_date;
447 + $dates_short_id[] = $current_date_resource_id;
448 + $last_token = $current_date;
449 + }
250 450
251 - Formal Definition: The value type is defined by the following
252 - notation:
451 + $last_date = $current_date;
452 + $last_date_id = $current_date_resource_id;
453 + }
253 454
254 - dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
455 + if ( ( '' !== $last_date ) && ( $last_token !== $last_date ) ) {
456 + $dates_short[] = $last_date;
457 + $dates_short_id[] = $last_date_id;
458 + }
255 459
256 - dur-date = dur-day [dur-time]
257 - dur-time = "T" (dur-hour / dur-minute / dur-second)
258 - dur-week = 1*DIGIT "W"
259 - dur-hour = 1*DIGIT "H" [dur-minute]
260 - dur-minute = 1*DIGIT "M" [dur-second]
261 - dur-second = 1*DIGIT "S"
262 - dur-day = 1*DIGIT "D"
460 + return array(
461 + 'dates_short' => $dates_short,
462 + 'dates_short_id' => $dates_short_id,
463 + );
464 +}
263 465
264 - Description: If the property permits, multiple "duration" values are
265 - specified by a COMMA character (US-ASCII decimal 44) separated list
266 - of values. The format is expressed as the [ISO 8601] basic format for
267 - the duration of time. The format can represent durations in terms of
268 - weeks, days, hours, minutes, and seconds.
269 - No additional content value encoding (i.e., BACKSLASH character
270 - encoding) are defined for this value type.
271 466
272 - Example: A duration of 15 days, 5 hours and 20 seconds would be:
467 +/**
468 + * Limit a parent-resource iCalendar feed to dates occupying the parent itself.
469 + *
470 + * Business Large normally expands parent-resource booking-list queries to all
471 + * child resources. The iCalendar export uses that shared query, so this helper
472 + * removes child-resource dates after retrieval without changing Booking
473 + * Calendar listings, calendar views, search, or capacity allocation.
474 + *
475 + * A NULL booking-date type ID represents the resource stored in the main
476 + * booking row. An explicit type ID represents another capacity resource used
477 + * by that booking.
478 + *
479 + * @param array $bookings_arr Booking-list API result.
480 + * @param int $resource_id Resource requested by the iCalendar feed URL.
481 + *
482 + * @return array Filtered booking-list API result.
483 + */
484 +function wpbc_ics_export_filter_parent_resource_bookings( $bookings_arr, $resource_id ) {
273 485
274 - P15DT5H0M20S
486 + $resource_id = intval( $resource_id );
275 487
276 - A duration of 7 weeks would be:
488 + if (
489 + ( $resource_id <= 0 )
490 + || ! class_exists( 'wpdev_bk_biz_l' )
491 + || ! function_exists( 'wpbc_br_cache' )
492 + || empty( $bookings_arr['bookings'] )
493 + ) {
494 + return $bookings_arr;
495 + }
277 496
278 - P7W
279 - */
497 + $resource_list = wpbc_br_cache()->get_resources();
498 + $is_parent_resource = false;
280 499
281 - $datanode = new ZCiCalDataNode( "REFRESH-INTERVAL;VALUE=DURATION:PT10M" ); // PERIOD TIME 10 Minutes //FixIn: 2.0.30.1
282 - //$datanode = new ZCiCalDataNode( "REFRESH-INTERVAL;VALUE=DURATION:PT1H" ); // PERIOD TIME 1 HOUR
500 + foreach ( $resource_list as $resource_params ) {
501 + $parent_resource_id = isset( $resource_params['parent'] ) ? intval( $resource_params['parent'] ) : 0;
502 +
503 + if ( $resource_id === $parent_resource_id ) {
504 + $is_parent_resource = true;
505 + break;
506 + }
507 + }
508 +
509 + if ( ! $is_parent_resource ) {
510 + return $bookings_arr;
511 + }
512 +
513 + foreach ( $bookings_arr['bookings'] as $booking_id => $booking_obj ) {
514 + $main_resource_id = isset( $booking_obj->booking_type ) ? intval( $booking_obj->booking_type ) : 0;
515 + $parent_dates = array();
516 +
517 + if ( empty( $booking_obj->dates ) || ! is_array( $booking_obj->dates ) ) {
518 + unset( $bookings_arr['bookings'][ $booking_id ] );
519 + continue;
520 + }
521 +
522 + foreach ( $booking_obj->dates as $booking_date ) {
523 + $date_resource_id = ! empty( $booking_date->type_id )
524 + ? intval( $booking_date->type_id )
525 + : $main_resource_id;
526 +
527 + if ( $resource_id === $date_resource_id ) {
528 + $parent_dates[] = $booking_date;
529 + }
530 + }
531 +
532 + if ( empty( $parent_dates ) ) {
533 + unset( $bookings_arr['bookings'][ $booking_id ] );
534 + continue;
535 + }
536 +
537 + usort(
538 + $parent_dates,
539 + static function ( $first_date, $second_date ) {
540 + return strcmp( $first_date->booking_date, $second_date->booking_date );
541 + }
542 + );
543 +
544 + $short_dates = wpbc_ics_export_build_short_dates( $parent_dates, $main_resource_id );
545 +
546 + $booking_obj->dates = $parent_dates;
547 + $booking_obj->dates_short = $short_dates['dates_short'];
548 + $booking_obj->dates_short_id = $short_dates['dates_short_id'];
549 + }
550 +
551 + $bookings_arr['bookings_count'] = count( $bookings_arr['bookings'] );
552 +
553 + return $bookings_arr;
554 +}
555 +
556 +
557 +/**
558 + * Define export ICS here.
559 + * For testing: http://beta/?feed=wpbm-ics
560 + *
561 + * @param array $param - [ 'wh_booking_type' => '1', 'wh_trash' => '' ].
562 + *
563 + * @return string
564 + */
565 +function wpbm_export_ics_feed__wpbm_ics( $param = array( 'wh_booking_type' => '1', 'wh_trash' => '' ) ) {
566 +
567 + // FixIn: 2.1.13.1 – timezone-safe, UTC stamps, OTA all‑day, single VTIMEZONE, no closures.
568 +
569 + if ( ! function_exists( 'wpbc_api_get_bookings_arr' ) ) {
570 + return '';
571 + }
572 +
573 + // Resolve export timezone: option 'booking_gcal_timezone' if set, else WP site tz.
574 + $site_tz_obj = wp_timezone();
575 + $tzid_opt_raw = (string) get_bk_option( 'booking_gcal_timezone' );
576 + $tzid_opt = trim( $tzid_opt_raw ); // "" when “Default” is selected
577 +
578 + // Validate TZID (must be a known IANA zone).
579 + $tzid = '';
580 + if ( $tzid_opt !== '' ) {
581 + try {
582 + // Will throw on invalid IDs (e.g., typos).
583 + $check = new DateTimeZone( $tzid_opt );
584 + $tzid = $tzid_opt; // valid — we’ll emit TZID and VTIMEZONE.
585 + }
586 + catch ( Exception $e ) {
587 + $tzid = ''; // invalid — treat as Default (no TZID emitted).
588 + }
589 + }
590 +
591 + // Export timezone object (priority: booking_gcal_timezone > site tz).
592 + $export_tz = ( $tzid !== '' ) ? new DateTimeZone( $tzid ) : $site_tz_obj;
593 +
594 + // End date of getting bookings (cutoff +2 years) – done in export tz.
595 + $cutoff = ( new DateTimeImmutable( 'now', $export_tz ) )->modify( '+2 years' )->format( 'Y-m-d' );
596 + $param['wh_booking_date2'] = $cutoff;
597 +
598 + // Export only approved bookings (FixIn: 2.0.11.1 / 8.5.2.3).
599 + $booking_is_ics_export_only_approved = get_bk_option( 'booking_is_ics_export_only_approved' );
600 + $param['wh_approved'] = ( 'On' === $booking_is_ics_export_only_approved ) ? '1' : '';
601 +
602 + // '' | 'imported' | 'plugin' (FixIn: 8.8.3.19 / 2.0.20.2).
603 + if ( 'imported' === get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) {
604 + $param['wh_sync_gid'] = 'imported';
605 + }
606 + if ( 'plugin' === get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) {
607 + $param['wh_sync_gid'] = 'plugin';
608 + }
609 +
610 + $_REQUEST['tab'] = 'vm_calendar'; // FixIn: 10.12.3.2 / 8.5.2.15 / 2.0.11.2.
611 +
612 + // Get array of bookings.
613 + $bookings_arr = wpbc_api_get_bookings_arr( $param );
614 + $export_resource_id = isset( $param['wh_booking_type'] ) ? intval( $param['wh_booking_type'] ) : 0;
615 + $bookings_arr = wpbc_ics_export_filter_parent_resource_bookings( $bookings_arr, $export_resource_id );
616 +
617 + ob_start();
618 +
619 + // Create the iCal object.
620 + $icalobj = new ZCiCal();
621 +
622 + // Calendar headers (library adds VERSION/PRODID; we add refresh hints).
623 + $datanode = new ZCiCalDataNode( 'REFRESH-INTERVAL;VALUE=DURATION:PT10M' ); // 10 minutes.
283 624 $icalobj->curnode->data[ $datanode->getName() ] = $datanode;
625 + $datanode = new ZCiCalDataNode( 'X-PUBLISHED-TTL:PT10M' ); // 10 minutes.
626 + $icalobj->curnode->data[ $datanode->getName() ] = $datanode;
284 627
285 - // This property for MS Outlook: // https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcical/1fc7b244-ecd1-4d28-ac0c-2bb4df855a1f or
286 - $datanode = new ZCiCalDataNode( "X-PUBLISHED-TTL:PT10M" ); // PERIOD TIME 10 Minutes - Outlook
287 - $icalobj->curnode->data[ $datanode->getName() ] = $datanode;
628 + $all_day_export = ( 'On' === get_wpbm_option( 'wpbm_is_export_only_full_days' ) ); // OTA toggle.
288 629
630 + // Host for UIDs (global uniqueness).
631 + $host = wp_parse_url( home_url(), PHP_URL_HOST );
632 + if ( empty( $host ) ) {
633 + $host = 'example.org';
634 + }
635 + if ( 'localhost' === $host ) {
636 + $host = 'localhost.localdomain';
637 + }
289 638
290 - //TODO: Some interesting info about updates of events: https://stackoverflow.com/questions/65812300/publishing-frequently-changing-icalendar-via-methodpublish-in-combination-with
639 + // Track VTIMEZONE additions per TZID.
640 + $__wpbc_added_vtimezones = array();
291 641
292 642 foreach ( $bookings_arr['bookings'] as $bk_id => $bk_arr ) {
293 643
294 - // create the event within the ical object
644 + // Create the master event within the iCal object.
295 645 $eventobj = new ZCiCalNode( 'VEVENT', $icalobj->curnode );
296 646
297 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
298 - // SUMMARY [Title]
299 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
300 -
301 - if ( function_exists( 'get_title_for_showing_in_day' ) ) { // Get title of booking pipiline in "Calendar Overview" page
302 -
303 - if ( true ) { // admin
304 - $what_show_in_day_template = get_bk_option( 'booking_default_title_in_day_for_calendar_view_mode' );
305 - } else { // front-end
306 - $what_show_in_day_template = get_bk_option( 'booking_default_title_in_day_for_timeline_front_end' );
307 - }
308 - //$title = esc_textarea( get_title_for_showing_in_day( $bk_id, $bookings_arr['bookings'], $what_show_in_day_template ) );
309 - $title = get_title_for_showing_in_day( $bk_id, $bookings_arr['bookings'], $what_show_in_day_template ); //FixIn: 2.0.1.6.1
310 -
647 + // ---------------------------------------------------------------------------------------------
648 + // SUMMARY
649 + // ---------------------------------------------------------------------------------------------
650 + if ( function_exists( 'get_title_for_showing_in_day' ) ) {
651 + $what_show_in_day_template = get_bk_option( 'booking_default_title_in_day_for_calendar_view_mode' );
652 + $title = get_title_for_showing_in_day( $bk_id, $bookings_arr['bookings'], $what_show_in_day_template );
311 653 } else {
312 -
313 - $title = ( ( isset( $bk_arr->form_data['name'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' )
314 - . ( ( isset( $bk_arr->form_data['firstname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['firstname'] ) : '' )
315 - . ( ( isset( $bk_arr->form_data['secondname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' )
316 - . ( ( isset( $bk_arr->form_data['lastname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['lastname'] ) : '' );
654 + $title = ( isset( $bk_arr->form_data['name'] ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' );
655 + $title .= ( isset( $bk_arr->form_data['firstname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['firstname'] )
656 + : '' );
657 + $title .= ( isset( $bk_arr->form_data['secondname'] )
658 + ? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' );
659 + $title .= ( isset( $bk_arr->form_data['lastname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['lastname'] )
660 + : '' );
317 661 }
318 662
319 - // Insert the name of booking resource into title of .ics event
320 - //$title = $bk_arr->form_data['_all_fields_']['resource_title']->title . ' - ' . $title;
321 -
322 - $is_hide_details = get_wpbm_option( 'wpbm_is_hide_details' );
323 - if ( 'On' === $is_hide_details ) { //FixIn: 2.0.12.3
663 + $is_hide_details = get_wpbm_option( 'wpbm_is_hide_details' );
664 + if ( 'On' === $is_hide_details ) {
324 665 $title = '';
325 666 }
326 - $title = wpbm_esc_to_plain_text( $title ); //FixIn: 2.0.1.6.1
667 + $title = ltrim( (string) $title ); // optional: avoid leading space.
668 + $title_ics = wpbc_ics_escape_text( wpbm_esc_to_plain_text( $title ) );
669 + $eventobj->addNode( new ZCiCalDataNode( 'SUMMARY:' . $title_ics ) );
327 670
328 -//$title = substr($title, 0, 5);
329 - $eventobj->addNode( new ZCiCalDataNode( 'SUMMARY:' . trim( $title ) ) );
671 + // ---------------------------------------------------------------------------------------------
672 + // DESCRIPTION
673 + // ---------------------------------------------------------------------------------------------
674 + $description_raw = ( 'On' === $is_hide_details ) ? '' : $bk_arr->form_show;
675 + $description_raw = ltrim( (string) $description_raw ); // remove leading whitespace/newlines.
676 + $description_ics = wpbc_ics_escape_text( wpbm_esc_to_plain_text( $description_raw ) );
677 + $description_ics = preg_replace('/\s+/', ' ', $description_ics);
678 + $eventobj->addNode( new ZCiCalDataNode( 'DESCRIPTION:' . $description_ics ) );
679 + // ---------------------------------------------------------------------------------------------
680 + // START & END (master span from dates_short)
681 + // ---------------------------------------------------------------------------------------------
682 + $event_start = $bk_arr->dates_short[0];
683 + if ( ( count( $bk_arr->dates_short ) > 1 ) && ( '-' === $bk_arr->dates_short[1] ) ) {
684 + $event_end = $bk_arr->dates_short[2];
685 + $event_end = wpbm_get_next_date_if_it_full_date( $event_end ); // make end exclusive for YYYY-mm-dd
686 + $period_start_index = 3;
687 + } else { // comma or single day.
688 + $event_end = $bk_arr->dates_short[0];
689 + $event_end = wpbm_get_next_date_if_it_full_date( $event_end );
690 + $period_start_index = 1;
691 + }
330 692
331 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
332 - // DESCRIPTION
333 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
334 - $description = $bk_arr->form_show;
335 -// if ( ( ! empty($bk_arr->dates) ) && ( $bk_arr->dates[0]->approved ) ){
336 -// $description .= '<br>Status: Approved';
337 -// } else {
338 -// $description .= '<br>Status: Pending';
339 -// }
693 + if ( $all_day_export ) {
694 + // OTA all-day mode (end exclusive).
695 + $event_start = substr( $event_start, 0, 10 ); // Y-m-d
696 + $event_end = substr( $event_end, 0, 10 ); // Y-m-d
697 + if ( $event_start === $event_end ) {
698 + $event_end = wpbc_add_days_ymd( $event_end, 1, $export_tz );
699 + }
700 + } else {
701 + // Timed mode: normalize seconds to :00 for clean slots.
702 + if ( strlen( $event_start ) > 10 ) {
703 + $event_start = wpbc_normalize_sec_sql( $event_start, $export_tz );
704 + }
705 + if ( strlen( $event_end ) > 10 ) {
706 + $event_end = wpbc_normalize_sec_sql( $event_end, $export_tz );
707 + }
708 + }
340 709
341 - //$description = html_entity_decode( sanitize_text_field( $description ) );
342 - //$description = trim( wp_specialchars_decode( esc_html( stripslashes( $description ) ), ENT_QUOTES ) );
343 - if ( 'On' === $is_hide_details ) { //FixIn: 2.0.12.3
344 - $description = '';
710 + // Add VTIMEZONE once (timed mode with TZID).
711 + if ( ! $all_day_export && ! empty( $tzid ) ) {
712 + wpbc_add_vtimezone_once( $icalobj, $tzid, substr( $event_start, 0, 4 ), substr( $event_end, 0, 4 ), $__wpbc_added_vtimezones );
345 713 }
346 - $description = wpbm_esc_to_plain_text( $description );
347 714
348 -//debuge($description . '!');die;
349 -//$description = substr($description, 0, 1);
350 - $description = preg_replace( array( "/\n/" ), array( '' ), $description );
351 -//debuge( ZCiCal::formatContent( $description ) );die;
352 - $eventobj->addNode( new ZCiCalDataNode( 'DESCRIPTION:' . ZCiCal::formatContent( $description ) ) );
353 -
354 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
355 - // START & END DATES
356 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
357 - $event_start = $bk_arr->dates_short[ 0 ];
358 - if ( ( count($bk_arr->dates_short) > 1 ) && ( '-' === $bk_arr->dates_short[ 1 ] ) ) {
359 -
360 - $event_end = $bk_arr->dates_short[ 2 ];
361 -
362 - $event_end = wpbm_get_next_date_if_it_full_date( $event_end );
363 -
364 - $period_start_index = 3;
365 - } else { // comma
366 - $event_end = $bk_arr->dates_short[ 0 ];
367 -
368 - $event_end = wpbm_get_next_date_if_it_full_date( $event_end );
715 + // DTSTART / DTEND for the master VEVENT (coerced so DTEND > DTSTART).
716 + if ( $all_day_export ) {
717 + list( $event_start, $event_end ) = wpbc_ics_coerce_order( $event_start, $event_end, $export_tz, true );
718 + $eventobj->addNode( new ZCiCalDataNode( 'DTSTART;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $event_start ) ) );
719 + $eventobj->addNode( new ZCiCalDataNode( 'DTEND;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $event_end ) ) );
720 + } else {
721 + list( $event_start_fixed, $event_end_fixed ) = wpbc_ics_coerce_order( $event_start, $event_end, $export_tz, false );
722 + if ( ! empty( $tzid ) ) {
723 + $eventobj->addNode( new ZCiCalDataNode( 'DTSTART;TZID=' . $tzid . ':' . wpbc_to_ics_local( $event_start_fixed, $export_tz ) ) );
724 + $eventobj->addNode( new ZCiCalDataNode( 'DTEND;TZID=' . $tzid . ':' . wpbc_to_ics_local( $event_end_fixed, $export_tz ) ) );
725 + } else {
726 + $eventobj->addNode( new ZCiCalDataNode( 'DTSTART:' . wpbc_to_ics_utc( $event_start_fixed, $export_tz ) ) );
727 + $eventobj->addNode( new ZCiCalDataNode( 'DTEND:' . wpbc_to_ics_utc( $event_end_fixed, $export_tz ) ) );
728 + }
729 + }
369 730
370 - $period_start_index = 1;
731 + // ---------------------------------------------------------------------------------------------
732 + // STATUS / TRANSP
733 + // ---------------------------------------------------------------------------------------------
734 + $ics_status = 'TENTATIVE';
735 + if ( ! empty( $bk_arr->trash ) ) {
736 + $ics_status = 'CANCELLED';
737 + } elseif ( ! empty( $bk_arr->dates[0]->approved ) ) {
738 + $ics_status = 'CONFIRMED';
371 739 }
372 -
373 - // add start date
374 - $eventobj->addNode( new ZCiCalDataNode( "DTSTART:" . ZCiCal::fromSqlDateTime( $event_start ) ) );
740 + $eventobj->addNode( new ZCiCalDataNode( 'STATUS:' . $ics_status ) );
741 + $eventobj->addNode( new ZCiCalDataNode( 'TRANSP:OPAQUE' ) );
375 742
376 - // add end date
377 - $eventobj->addNode( new ZCiCalDataNode( "DTEND:" . ZCiCal::fromSqlDateTime( $event_end ) ) );
378 -
379 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
380 - // RDATE - Rule for other different dates.
381 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
382 - //
383 - // RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,
384 - // 19960404T010000Z/PT3H
385 - //
386 - $rdate = array();
387 - $previos_value = '';
388 - foreach ( $bk_arr->dates_short as $short_ind => $bk_date ) {
389 -
390 - if ( $short_ind < $period_start_index )
743 + // ---------------------------------------------------------------------------------------------
744 + // Expand extra dates/ranges into standalone VEVENTs (better than RDATE/RRULE for OTA/clients)
745 + // ---------------------------------------------------------------------------------------------
746 + $segments = array(); // each: ['start_sql' => 'Y-m-d[ H:i:s]', 'end_sql' => 'Y-m-d[ H:i:s]'|null]
747 + $prev = '';
748 + $current = null;
749 +
750 + foreach ( $bk_arr->dates_short as $i => $tok ) {
751 + if ( $i < $period_start_index ) {
391 752 continue;
392 -
393 - if ( ( $bk_date !== ',' ) && ( $bk_date !== '-' ) ) {
394 -
395 - if ( $previos_value == '-' ) {
396 - $bk_date = wpbm_get_next_date_if_it_full_date( $bk_date );
753 + }
754 + if ( ',' !== $tok && '-' !== $tok ) {
755 + if ( '-' === $prev ) {
756 + $tok_norm = wpbm_get_next_date_if_it_full_date( $tok ); // range end (exclusive if Y-m-d)
397 757 } else {
398 - $bk_date = wpbm_get_only_date_if_it_full_date( $bk_date );
758 + $tok_norm = wpbm_get_only_date_if_it_full_date( $tok ); // start or single
399 759 }
400 -
401 - $rdate[]= ZCiCal::fromSqlDateTime( $bk_date );
402 -
403 - } else if ( ( $bk_date === '-' ) && ( count($rdate) > 0 ) ) {
404 -
405 - $rdate[] = '/'; // PERIOD
406 -
407 - } else if ( ( $bk_date === ',' ) && ( count($rdate) > 0 ) ) {
408 -
409 - $rdate[] = ','; // Other date
410 -
760 + if ( '-' === $prev ) {
761 + if ( $current ) {
762 + $current['end_sql'] = $tok_norm;
763 + }
764 + } else {
765 + $current = array( 'start_sql' => $tok_norm, 'end_sql' => null );
766 + }
767 + } elseif ( ',' === $tok ) {
768 + if ( $current ) {
769 + $segments[] = $current;
770 + $current = null;
771 + }
411 772 }
412 - $previos_value = $bk_date;
773 + $prev = $tok;
413 774 }
414 - if ( ! empty( $rdate ) ) {
415 - $rdate = implode( '', $rdate );
416 - // add RDATE
417 - $eventobj->addNode( new ZCiCalDataNode( "RDATE;VALUE=PERIOD:" . $rdate ) );
418 -
775 + if ( $current ) {
776 + $segments[] = $current;
419 777 }
420 778
421 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
422 - // Timezone
423 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
424 - /* Timezone must be a supported PHP timezone (see http://php.net/manual/en/timezones.php )
425 - * Note: multi-word timezones must use underscore "_" separator
426 - * Example: $tzid = "America/New_York";
427 - */
779 + if ( ! empty( $segments ) ) {
780 + // Duration basis for timed events.
781 + $master_duration = 0;
782 + if ( ! $all_day_export ) {
783 + $basis_start = isset( $event_start_fixed ) ? $event_start_fixed : $event_start;
784 + $basis_end = isset( $event_end_fixed ) ? $event_end_fixed : $event_end;
785 + $dts = DateTime::createFromFormat( 'Y-m-d H:i:s', $basis_start, $export_tz );
786 + $dte = DateTime::createFromFormat( 'Y-m-d H:i:s', $basis_end, $export_tz );
787 + $master_duration = ( $dts && $dte && $dte > $dts ) ? ( $dte->getTimestamp() - $dts->getTimestamp() )
788 + : 3600;
789 + }
428 790
429 - $tzid = get_bk_option( 'booking_gcal_timezone' );
791 + $seg_idx = 0;
792 + foreach ( $segments as $s ) {
793 + ++ $seg_idx;
794 + $ev = new ZCiCalNode( 'VEVENT', $icalobj->curnode );
430 795
431 - if ( ! empty( $tzid ) ) {
432 - // Add timezone data
433 - ZCTimeZoneHelper::getTZNode( substr( $event_start, 0, 4 ), substr( $event_end, 0, 4 ), $tzid, $icalobj->curnode ); //FixIn: 2.0.3.3
434 - }
796 + if ( $all_day_export ) {
797 + $start_d = substr( $s['start_sql'], 0, 10 );
798 + $end_d = ! empty( $s['end_sql'] ) ? substr( $s['end_sql'], 0, 10 ) : $start_d;
799 + list( $start_d, $end_d ) = wpbc_ics_coerce_order( $start_d, $end_d, $export_tz, true );
435 800
436 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
437 - // UID
438 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
439 - $uid = $event_start . '_' . $bk_id . "@demo.icalendar.org";
440 - $uid = str_replace( array( '_', ' ' ), '-', $uid ); //FixIn: 2.1.7.1
441 - $eventobj->addNode( new ZCiCalDataNode( "UID:" . $uid ) );
442 -
443 - // DTSTAMP is a required item in VEVENT
444 - $eventobj->addNode( new ZCiCalDataNode( "DTSTAMP:" . ZCiCal::fromSqlDateTime() ) );
801 + $ev->addNode( new ZCiCalDataNode( 'DTSTART;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $start_d ) ) );
802 + $ev->addNode( new ZCiCalDataNode( 'DTEND;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $end_d ) ) );
445 803
446 - $eventobj->addNode( new ZCiCalDataNode( "CREATED:" . ZCiCal::fromSqlDateTime( $bk_arr->modification_date ) ) );
447 - $eventobj->addNode( new ZCiCalDataNode( "LAST-MODIFIED:" . ZCiCal::fromSqlDateTime( $bk_arr->modification_date ) ) );
448 -
449 -
450 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
451 - // STATUS:CONFIRMED ( "TENTATIVE" / "CONFIRMED" / "CANCELLED" ) // This property defines the overall status for EVENT
452 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
453 -
454 - $ics_status = 'TENTATIVE';
455 - if ( ! ( empty( $bk_arr->trash ) ) ) {
456 - $ics_status = 'CANCELLED';
457 - } elseif ( ! ( empty( $bk_arr->dates[0]->approved ) ) ) {
458 - $ics_status = 'CONFIRMED';
804 + } else {
805 + $start_sql = ( strlen( $s['start_sql'] ) > 10 )
806 + ? wpbc_normalize_sec_sql( $s['start_sql'], $export_tz ) : $s['start_sql'];
807 + if ( ! empty( $s['end_sql'] ) ) {
808 + $end_sql = ( strlen( $s['end_sql'] ) > 10 )
809 + ? wpbc_normalize_sec_sql( $s['end_sql'], $export_tz ) : $s['end_sql'];
810 + } else {
811 + $dt = DateTime::createFromFormat( 'Y-m-d H:i:s', $start_sql, $export_tz );
812 + if ( $dt ) {
813 + $dt->modify( '+' . $master_duration . ' seconds' );
814 + $end_sql = $dt->format( 'Y-m-d H:i:s' );
815 + } else {
816 + $end_sql = $start_sql;
817 + }
818 + }
819 +
820 + // Ensure DTEND > DTSTART for each segment.
821 + list( $start_sql, $end_sql ) = wpbc_ics_coerce_order( $start_sql, $end_sql, $export_tz, false );
822 +
823 + if ( ! empty( $tzid ) ) {
824 + $ev->addNode( new ZCiCalDataNode( 'DTSTART;TZID=' . $tzid . ':' . wpbc_to_ics_local( $start_sql, $export_tz ) ) );
825 + $ev->addNode( new ZCiCalDataNode( 'DTEND;TZID=' . $tzid . ':' . wpbc_to_ics_local( $end_sql, $export_tz ) ) );
826 + } else {
827 + $ev->addNode( new ZCiCalDataNode( 'DTSTART:' . wpbc_to_ics_utc( $start_sql, $export_tz ) ) );
828 + $ev->addNode( new ZCiCalDataNode( 'DTEND:' . wpbc_to_ics_utc( $end_sql, $export_tz ) ) );
829 + }
830 + }
831 +
832 + // Copy common fields.
833 + $ev->addNode( new ZCiCalDataNode( 'SUMMARY:' . $title_ics ) );
834 + $ev->addNode( new ZCiCalDataNode( 'DESCRIPTION:' . $description_ics ) );
835 + $ev->addNode( new ZCiCalDataNode( 'STATUS:' . $ics_status ) );
836 + $ev->addNode( new ZCiCalDataNode( 'TRANSP:OPAQUE' ) );
837 +
838 + // Unique UID per extra VEVENT.
839 + $uid_extra = $bk_id . '-' . $seg_idx . '@' . $host;
840 + $ev->addNode( new ZCiCalDataNode( 'UID:' . $uid_extra ) );
841 +
842 + // Timestamps in UTC Z.
843 + $ev->addNode( new ZCiCalDataNode( 'DTSTAMP:' . wpbc_to_ics_utc( null, $export_tz ) ) );
844 + $ev->addNode( new ZCiCalDataNode( 'CREATED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) );
845 + $ev->addNode( new ZCiCalDataNode( 'LAST-MODIFIED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) );
846 + }
459 847 }
460 - $eventobj->addNode( new ZCiCalDataNode( "STATUS:" . $ics_status ) ); //FixIn: 2.0.11.3 //FixIn: 2.0.12.2
461 848
462 - // Check more here: https://icalendar.org/iCalendar-RFC-5545/3-2-9-free-busy-time-type.html //FixIn: 2.1.9.1
463 - $ics_busy = 'BUSY'; // ("FREE" / "BUSY" / "BUSY-UNAVAILABLE" / "BUSY-TENTATIVE"
464 - $eventobj->addNode( new ZCiCalDataNode( "FBTYPE:" . $ics_busy ) );
849 + // ---------------------------------------------------------------------------------------------
850 + // UID (master)
851 + // ---------------------------------------------------------------------------------------------
852 + $uid = $bk_id . '@' . $host; // stable UID not tied to start time.
853 + $eventobj->addNode( new ZCiCalDataNode( 'UID:' . $uid ) );
465 854
466 - //FixIn: 2.0.20.1
467 - //ATTENDEE;SENT-BY=MAILTO:[email protected];CN=John Smith:MAILTO:[email protected] // More here https://www.kanzaki.com/docs/ical/attendee.html
468 - if ( isset( $bk_arr->form_data['email'] ) ) {
855 + // DTSTAMP / CREATED / LAST-MODIFIED in UTC Z.
856 + $eventobj->addNode( new ZCiCalDataNode( 'DTSTAMP:' . wpbc_to_ics_utc( null, $export_tz ) ) );
857 + $eventobj->addNode( new ZCiCalDataNode( 'CREATED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) );
858 + $eventobj->addNode( new ZCiCalDataNode( 'LAST-MODIFIED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) );
469 859
470 - $cn_title = ( ( isset( $bk_arr->form_data['name'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' )
471 - . ( ( isset( $bk_arr->form_data['firstname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['firstname'] ) : '' )
472 - . ( ( isset( $bk_arr->form_data['secondname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' )
473 - . ( ( isset( $bk_arr->form_data['lastname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['lastname'] ) : '' );
860 + // ---------------------------------------------------------------------------------------------
861 + // ATTENDEE (optional)
862 + // ---------------------------------------------------------------------------------------------
863 + if ( isset( $bk_arr->form_data['email'] ) && ( 'On' !== $is_hide_details ) ) {
864 + $cn_title = ( isset( $bk_arr->form_data['name'] ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' );
865 + $cn_title .= ( isset( $bk_arr->form_data['firstname'] )
866 + ? ' ' . esc_textarea( $bk_arr->form_data['firstname'] ) : '' );
867 + $cn_title .= ( isset( $bk_arr->form_data['secondname'] )
868 + ? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' );
869 + $cn_title .= ( isset( $bk_arr->form_data['lastname'] )
870 + ? ' ' . esc_textarea( $bk_arr->form_data['lastname'] ) : '' );
474 871
872 + $cn_clean = trim( wpbm_esc_to_plain_text( $cn_title ) );
873 + $cn_clean = addcslashes( $cn_clean, "\\,;\"" ); // escape per RFC
475 874 $attendee_email = $bk_arr->form_data['email'];
476 - if ( 'On' !== $is_hide_details ) {
477 - $eventobj->addNode( new ZCiCalDataNode( "ATTENDEE;CN=" . $cn_title . ":MAILTO:" . $attendee_email ) );
478 - }
875 + $eventobj->addNode( new ZCiCalDataNode( 'ATTENDEE;CN="' . $cn_clean . '":MAILTO:' . $attendee_email ) );
479 876 }
480 -
481 - // RRULE:FREQ=WEEKLY;COUNT=3;BYDAY=WE,SA
482 - // LOCATION:South Australia\, Australia
483 - // SEQUENCE:1 // defines the revision sequence number of the calendar component within a sequence of revisions
484 - // TRANSP:OPAQUE // This property defines whether or not an event is transparent to busy time searches ;Default value is OPAQUE
485 - // OPAQUE - Blocks or opaque on busy time searches.
486 - // TRANSPARENT - Transparent on busy time searches.
487 -
488 877 }
489 878
490 - echo $icalobj->export(); // write iCalendar feed to stdout
879 + // Output iCalendar text.
880 + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
881 + echo $icalobj->export();
491 882
492 883 $ics_export = ob_get_contents();
493 -
494 - $ics_export = trim( $ics_export ); //FixIn: 2.1.9.2
495 -
884 + $ics_export = trim( $ics_export );
496 885 ob_end_clean();
497 886
498 887 return $ics_export;
499 888 }
@@ -498,32 +887,33 @@
498 887 return $ics_export;
499 888 }
500 889
501 890
891 +
502 892 /** Get Next day in MySQL format without time, if current day its Full day - ending with 00:00:00
503 - *
893 + *
504 894 * @param string $mysql_date - 2017-07-12 00:00:00
505 895 * @return string - 2017-07-13
506 896 */
507 897 function wpbm_get_next_date_if_it_full_date( $mysql_date ) {
508 -
509 - // Check if this date ending with 00:00:00 its means that it full day,
898 +
899 + // Check if this date ending with 00:00:00 its means that it full day,
510 900 // so we need to add 24 hours for ending at 23:59:59 (basically 00:00:00 of next day) instead of at start of current day 00:00:00
511 901 if ( ( strlen( $mysql_date ) > 10 ) // , like 2017-07-12 00:00:00
512 - && ( substr( $mysql_date, 11 ) == '00:00:00' )
902 + && ( substr( $mysql_date, 11 ) == '00:00:00' )
513 903 ) {
514 904
515 905 $mysql_date = date_i18n( "Y-m-d 00:00:00", strtotime( $mysql_date ) );
516 906 $mysql_date = strtotime( $mysql_date );
517 - $mysql_date = date_i18n( "Y-m-d", strtotime( '+1 day', $mysql_date ) );
518 - }
907 + $mysql_date = date_i18n( "Y-m-d", strtotime( '+1 day', $mysql_date ) );
908 + }
519 909
520 - return $mysql_date;
910 + return $mysql_date;
521 911 }
522 912
523 913
524 914 /** Get day in MySQL format without time, if current day its Full day - ending with 00:00:00
525 - *
915 + *
526 916 * @param string $mysql_date - 2017-07-12 00:00:00
527 917 * @return string - 2017-07-12
528 918 */
529 919 function wpbm_get_only_date_if_it_full_date( $mysql_date ) {
@@ -528,11 +918,11 @@
528 918 */
529 919 function wpbm_get_only_date_if_it_full_date( $mysql_date ) {
530 920
531 921 if ( ( strlen( $mysql_date ) > 10 ) // , like 2017-07-12 00:00:00
532 - && ( substr( $mysql_date, 11) == '00:00:00' )
533 - ) {
922 + && ( substr( $mysql_date, 11) == '00:00:00' )
923 + ) {
534 924 $mysql_date = substr( $mysql_date, 0, 10 );
535 925 }
536 926
537 927 return $mysql_date;
538 -}
928 +}