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