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 +647 -198 2.0.20 → 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,251 +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' ) )
211 + return $text;
212 +}
213 +
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' ) );
229 +
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' );
327 + } else {
328 + $de = DateTime::createFromFormat( 'Y-m-d H:i:s', $ee, $tz );
329 + if ( ! $de ) { $de = new DateTime( $ee, $tz ); }
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 +}
337 +
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;
358 + }
359 + if ( isset( $added_ref[ $tzid ] ) ) {
360 + return;
361 + }
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 ) );
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' ) );
376 +
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' ) );
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 + }
389 +
390 + $added_ref[ $tzid ] = true;
391 +}
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 ) {
415 +
416 + $dates_short = array();
417 + $dates_short_id = array();
418 + $last_date = '';
419 + $last_date_id = '';
420 + $last_token = '';
421 +
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 );
427 +
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 + }
443 +
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 + }
450 +
451 + $last_date = $current_date;
452 + $last_date_id = $current_date_resource_id;
453 + }
454 +
455 + if ( ( '' !== $last_date ) && ( $last_token !== $last_date ) ) {
456 + $dates_short[] = $last_date;
457 + $dates_short_id[] = $last_date_id;
458 + }
459 +
460 + return array(
461 + 'dates_short' => $dates_short,
462 + 'dates_short_id' => $dates_short_id,
463 + );
464 +}
465 +
466 +
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 ) {
485 +
486 + $resource_id = intval( $resource_id );
487 +
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 + }
496 +
497 + $resource_list = wpbc_br_cache()->get_resources();
498 + $is_parent_resource = false;
499 +
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' ) ) {
201 570 return '';
571 + }
202 572
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;
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
211 577
212 - // Export only approved bookings //FixIn: 2.0.11.1 //FixIn: 8.5.2.3
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).
213 599 $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';
216 - } else {
217 - $param['wh_approved'] = '';
218 - }
600 + $param['wh_approved'] = ( 'On' === $booking_is_ics_export_only_approved ) ? '1' : '';
219 601
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' ) ) {
602 + // '' | 'imported' | 'plugin' (FixIn: 8.8.3.19 / 2.0.20.2).
603 + if ( 'imported' === get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) {
222 604 $param['wh_sync_gid'] = 'imported';
223 605 }
224 - if ( 'plugin' == get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) {
606 + if ( 'plugin' === get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) {
225 607 $param['wh_sync_gid'] = 'plugin';
226 608 }
227 609
610 + $_REQUEST['tab'] = 'vm_calendar'; // FixIn: 10.12.3.2 / 8.5.2.15 / 2.0.11.2.
228 611
229 -
230 - $_REQUEST['view_mode']= 'vm_calendar'; //FixIn: 8.5.2.15 2.0.11.2
231 612 // Get array of bookings.
232 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 );
233 616
617 + ob_start();
234 618
235 - ob_start();
236 -
237 - // create the ical object
619 + // Create the iCal object.
238 620 $icalobj = new ZCiCal();
239 -
240 621
622 + // Calendar headers (library adds VERSION/PRODID; we add refresh hints).
623 + $datanode = new ZCiCalDataNode( 'REFRESH-INTERVAL;VALUE=DURATION:PT10M' ); // 10 minutes.
624 + $icalobj->curnode->data[ $datanode->getName() ] = $datanode;
625 + $datanode = new ZCiCalDataNode( 'X-PUBLISHED-TTL:PT10M' ); // 10 minutes.
626 + $icalobj->curnode->data[ $datanode->getName() ] = $datanode;
627 +
628 + $all_day_export = ( 'On' === get_wpbm_option( 'wpbm_is_export_only_full_days' ) ); // OTA toggle.
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 + }
638 +
639 + // Track VTIMEZONE additions per TZID.
640 + $__wpbc_added_vtimezones = array();
641 +
241 642 foreach ( $bookings_arr['bookings'] as $bk_id => $bk_arr ) {
242 643
243 - // create the event within the ical object
644 + // Create the master event within the iCal object.
244 645 $eventobj = new ZCiCalNode( 'VEVENT', $icalobj->curnode );
245 646
246 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
247 - // SUMMARY [Title]
248 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
249 -
250 - if ( function_exists( 'get_title_for_showing_in_day' ) ) { // Get title of booking pipiline in "Calendar Overview" page
251 -
252 - if ( true ) { // admin
253 - $what_show_in_day_template = get_bk_option( 'booking_default_title_in_day_for_calendar_view_mode' );
254 - } else { // front-end
255 - $what_show_in_day_template = get_bk_option( 'booking_default_title_in_day_for_timeline_front_end' );
256 - }
257 - //$title = esc_textarea( get_title_for_showing_in_day( $bk_id, $bookings_arr['bookings'], $what_show_in_day_template ) );
258 - $title = get_title_for_showing_in_day( $bk_id, $bookings_arr['bookings'], $what_show_in_day_template ); //FixIn: 2.0.1.6.1
259 -
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 );
260 653 } else {
261 -
262 - $title = ( ( isset( $bk_arr->form_data['name'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' )
263 - . ( ( isset( $bk_arr->form_data['firstname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['firstname'] ) : '' )
264 - . ( ( isset( $bk_arr->form_data['secondname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' )
265 - . ( ( 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 + : '' );
266 661 }
267 662
268 - // Insert the name of booking resource into title of .ics event
269 - //$title = $bk_arr->form_data['_all_fields_']['resource_title']->title . ' - ' . $title;
270 -
271 - $is_hide_details = get_wpbm_option( 'wpbm_is_hide_details' );
272 - 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 ) {
273 665 $title = '';
274 666 }
275 - $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 ) );
276 670
277 -//$title = substr($title, 0, 5);
278 - $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 + }
279 692
280 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
281 - // DESCRIPTION
282 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
283 - $description = $bk_arr->form_show;
284 -// if ( ( ! empty($bk_arr->dates) ) && ( $bk_arr->dates[0]->approved ) ){
285 -// $description .= '<br>Status: Approved';
286 -// } else {
287 -// $description .= '<br>Status: Pending';
288 -// }
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 + }
289 709
290 - //$description = html_entity_decode( sanitize_text_field( $description ) );
291 - //$description = trim( wp_specialchars_decode( esc_html( stripslashes( $description ) ), ENT_QUOTES ) );
292 - if ( 'On' === $is_hide_details ) { //FixIn: 2.0.12.3
293 - $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 );
294 713 }
295 - $description = wpbm_esc_to_plain_text( $description );
296 714
297 -//debuge($description . '!');die;
298 -//$description = substr($description, 0, 1);
299 - $description = preg_replace( array( "/\n/" ), array( '' ), $description );
300 -//debuge( ZCiCal::formatContent( $description ) );die;
301 - $eventobj->addNode( new ZCiCalDataNode( 'DESCRIPTION:' . ZCiCal::formatContent( $description ) ) );
302 -
303 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
304 - // START & END DATES
305 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
306 - $event_start = $bk_arr->dates_short[ 0 ];
307 - if ( ( count($bk_arr->dates_short) > 1 ) && ( '-' === $bk_arr->dates_short[ 1 ] ) ) {
308 -
309 - $event_end = $bk_arr->dates_short[ 2 ];
310 -
311 - $event_end = wpbm_get_next_date_if_it_full_date( $event_end );
312 -
313 - $period_start_index = 3;
314 - } else { // comma
315 - $event_end = $bk_arr->dates_short[ 0 ];
316 -
317 - $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 + }
318 730
319 - $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';
320 739 }
321 -
322 - // add start date
323 - $eventobj->addNode( new ZCiCalDataNode( "DTSTART:" . ZCiCal::fromSqlDateTime( $event_start ) ) );
740 + $eventobj->addNode( new ZCiCalDataNode( 'STATUS:' . $ics_status ) );
741 + $eventobj->addNode( new ZCiCalDataNode( 'TRANSP:OPAQUE' ) );
324 742
325 - // add end date
326 - $eventobj->addNode( new ZCiCalDataNode( "DTEND:" . ZCiCal::fromSqlDateTime( $event_end ) ) );
327 -
328 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
329 - // RDATE - Rule for other different dates.
330 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
331 - //
332 - // RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,
333 - // 19960404T010000Z/PT3H
334 - //
335 - $rdate = array();
336 - $previos_value = '';
337 - foreach ( $bk_arr->dates_short as $short_ind => $bk_date ) {
338 -
339 - 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 ) {
340 752 continue;
341 -
342 - if ( ( $bk_date !== ',' ) && ( $bk_date !== '-' ) ) {
343 -
344 - if ( $previos_value == '-' ) {
345 - $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)
346 757 } else {
347 - $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
348 759 }
349 -
350 - $rdate[]= ZCiCal::fromSqlDateTime( $bk_date );
351 -
352 - } else if ( ( $bk_date === '-' ) && ( count($rdate) > 0 ) ) {
353 -
354 - $rdate[] = '/'; // PERIOD
355 -
356 - } else if ( ( $bk_date === ',' ) && ( count($rdate) > 0 ) ) {
357 -
358 - $rdate[] = ','; // Other date
359 -
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 + }
360 772 }
361 - $previos_value = $bk_date;
773 + $prev = $tok;
362 774 }
363 - if ( ! empty( $rdate ) ) {
364 - $rdate = implode( '', $rdate );
365 - // add RDATE
366 - $eventobj->addNode( new ZCiCalDataNode( "RDATE;VALUE=PERIOD:" . $rdate ) );
367 -
775 + if ( $current ) {
776 + $segments[] = $current;
368 777 }
369 778
370 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
371 - // Timezone
372 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
373 - /* Timezone must be a supported PHP timezone (see http://php.net/manual/en/timezones.php )
374 - * Note: multi-word timezones must use underscore "_" separator
375 - * Example: $tzid = "America/New_York";
376 - */
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 + }
377 790
378 - $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 );
379 795
380 - if ( ! empty( $tzid ) ) {
381 - // Add timezone data
382 - ZCTimeZoneHelper::getTZNode( substr( $event_start, 0, 4 ), substr( $event_end, 0, 4 ), $tzid, $icalobj->curnode ); //FixIn: 2.0.3.3
383 - }
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 );
384 800
385 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
386 - // UID
387 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
388 - $uid = $event_start . '_' . $bk_id . "@demo.icalendar.org";
389 - $eventobj->addNode( new ZCiCalDataNode( "UID:" . $uid ) );
390 -
391 - // DTSTAMP is a required item in VEVENT
392 - $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 ) ) );
393 803
394 - $eventobj->addNode( new ZCiCalDataNode( "CREATED:" . ZCiCal::fromSqlDateTime( $bk_arr->modification_date ) ) );
395 - $eventobj->addNode( new ZCiCalDataNode( "LAST-MODIFIED:" . ZCiCal::fromSqlDateTime( $bk_arr->modification_date ) ) );
396 -
397 -
398 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
399 - // STATUS:CONFIRMED ( "TENTATIVE" / "CONFIRMED" / "CANCELLED" ) // This property defines the overall status for EVENT
400 - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
401 -
402 - $ics_status = 'TENTATIVE';
403 - if ( ! ( empty( $bk_arr->trash ) ) ) {
404 - $ics_status = 'CANCELLED';
405 - } elseif ( ! ( empty( $bk_arr->dates[0]->approved ) ) ) {
406 - $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 + }
407 847 }
408 - $eventobj->addNode( new ZCiCalDataNode( "STATUS:" . $ics_status ) ); //FixIn: 2.0.11.3 //FixIn: 2.0.12.2
409 848
410 - //FixIn: 2.0.20.1
411 - //ATTENDEE;SENT-BY=MAILTO:[email protected];CN=John Smith:MAILTO:[email protected] // More here https://www.kanzaki.com/docs/ical/attendee.html
412 - if ( isset( $bk_arr->form_data['email'] ) ) {
849 + // ---------------------------------------------------------------------------------------------
850 + // UID (master)
851 + // ---------------------------------------------------------------------------------------------
852 + $uid = $bk_id . '@' . $host; // stable UID not tied to start time.
853 + $eventobj->addNode( new ZCiCalDataNode( 'UID:' . $uid ) );
413 854
414 - $cn_title = ( ( isset( $bk_arr->form_data['name'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' )
415 - . ( ( isset( $bk_arr->form_data['firstname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['firstname'] ) : '' )
416 - . ( ( isset( $bk_arr->form_data['secondname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' )
417 - . ( ( isset( $bk_arr->form_data['lastname'] ) ) ? ' ' . esc_textarea( $bk_arr->form_data['lastname'] ) : '' );
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 ) ) );
418 859
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'] ) : '' );
871 +
872 + $cn_clean = trim( wpbm_esc_to_plain_text( $cn_title ) );
873 + $cn_clean = addcslashes( $cn_clean, "\\,;\"" ); // escape per RFC
419 874 $attendee_email = $bk_arr->form_data['email'];
420 -
421 - $eventobj->addNode( new ZCiCalDataNode( "ATTENDEE;CN=" . $cn_title . ":MAILTO:" . $attendee_email ) );
875 + $eventobj->addNode( new ZCiCalDataNode( 'ATTENDEE;CN="' . $cn_clean . '":MAILTO:' . $attendee_email ) );
422 876 }
423 -
424 - // RRULE:FREQ=WEEKLY;COUNT=3;BYDAY=WE,SA
425 - // LOCATION:South Australia\, Australia
426 - // SEQUENCE:1 // defines the revision sequence number of the calendar component within a sequence of revisions
427 - // TRANSP:OPAQUE // This property defines whether or not an event is transparent to busy time searches ;Default value is OPAQUE
428 - // OPAQUE - Blocks or opaque on busy time searches.
429 - // TRANSPARENT - Transparent on busy time searches.
430 -
431 877 }
432 878
433 - echo $icalobj->export(); // write iCalendar feed to stdout
879 + // Output iCalendar text.
880 + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
881 + echo $icalobj->export();
434 882
435 883 $ics_export = ob_get_contents();
436 -
884 + $ics_export = trim( $ics_export );
437 885 ob_end_clean();
438 886
439 887 return $ics_export;
440 888 }
@@ -439,32 +887,33 @@
439 887 return $ics_export;
440 888 }
441 889
442 890
891 +
443 892 /** Get Next day in MySQL format without time, if current day its Full day - ending with 00:00:00
444 - *
893 + *
445 894 * @param string $mysql_date - 2017-07-12 00:00:00
446 895 * @return string - 2017-07-13
447 896 */
448 897 function wpbm_get_next_date_if_it_full_date( $mysql_date ) {
449 -
450 - // 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,
451 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
452 901 if ( ( strlen( $mysql_date ) > 10 ) // , like 2017-07-12 00:00:00
453 - && ( substr( $mysql_date, 11 ) == '00:00:00' )
902 + && ( substr( $mysql_date, 11 ) == '00:00:00' )
454 903 ) {
455 904
456 905 $mysql_date = date_i18n( "Y-m-d 00:00:00", strtotime( $mysql_date ) );
457 906 $mysql_date = strtotime( $mysql_date );
458 - $mysql_date = date_i18n( "Y-m-d", strtotime( '+1 day', $mysql_date ) );
459 - }
907 + $mysql_date = date_i18n( "Y-m-d", strtotime( '+1 day', $mysql_date ) );
908 + }
460 909
461 - return $mysql_date;
910 + return $mysql_date;
462 911 }
463 912
464 913
465 914 /** Get day in MySQL format without time, if current day its Full day - ending with 00:00:00
466 - *
915 + *
467 916 * @param string $mysql_date - 2017-07-12 00:00:00
468 917 * @return string - 2017-07-12
469 918 */
470 919 function wpbm_get_only_date_if_it_full_date( $mysql_date ) {
@@ -469,11 +918,11 @@
469 918 */
470 919 function wpbm_get_only_date_if_it_full_date( $mysql_date ) {
471 920
472 921 if ( ( strlen( $mysql_date ) > 10 ) // , like 2017-07-12 00:00:00
473 - && ( substr( $mysql_date, 11) == '00:00:00' )
474 - ) {
922 + && ( substr( $mysql_date, 11) == '00:00:00' )
923 + ) {
475 924 $mysql_date = substr( $mysql_date, 0, 10 );
476 925 }
477 926
478 927 return $mysql_date;
479 -}
928 +}