| 1 |
<?php |
| 2 |
/** |
| 3 |
* @version 1.0 |
| 4 |
* @description Booking Calendar integration - Export |
| 5 |
* |
| 6 |
* @author wpdevelop |
| 7 |
* @link https://oplugins.com/ |
| 8 |
* @email [email protected] |
| 9 |
* |
| 10 |
* @modified 2017-06-28 |
| 11 |
*/ |
| 12 |
|
| 13 |
if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly |
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| 18 |
// E X P O R T |
| 19 |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| 20 |
|
| 21 |
/** Define ICS Feeds names |
| 22 |
* |
| 23 |
* wpbm-ics - Example of Feed Name. |
| 24 |
* |
| 25 |
* Have to work correctly immediately: http://beta/?feed=wpbm-ics |
| 26 |
* Nice looking link: http://beta/feed/wpbm-ics -- require to update permalink structure at WordPress > Settings > Permalink Settings page by clicking on "Save changes" button. |
| 27 |
* Finally, to display your feed, you’ll first need to flush your WordPress rewrite rules. |
| 28 |
* The easiest way to do this is by logging in to the WordPress admin, and clicking Settings -> Permalinks. |
| 29 |
* Once here, just click Save Changes, which will flush the rewrite rules. |
| 30 |
* |
| 31 |
* Otherwsie need to run ONLY once this: |
| 32 |
* |
| 33 |
* global $wp_rewrite; |
| 34 |
* $wp_rewrite->flush_rules(); |
| 35 |
* |
| 36 |
*/ |
| 37 |
function wpbm_make_export_ics_feeds(){ |
| 38 |
|
| 39 |
if ( function_exists( 'wp_parse_url' ) ) |
| 40 |
$my_parsed_url = wp_parse_url( $_SERVER[ 'REQUEST_URI' ] ); |
| 41 |
else |
| 42 |
$my_parsed_url = @parse_url( $_SERVER[ 'REQUEST_URI' ] ); |
| 43 |
|
| 44 |
if ( false === $my_parsed_url ) { // seriously malformed URLs, parse_url() may return FALSE. |
| 45 |
return; |
| 46 |
} |
| 47 |
|
| 48 |
$my_parsed_url_path = trim( $my_parsed_url[ 'path' ] ); |
| 49 |
$my_parsed_url_path = trim( $my_parsed_url_path, '/' ); |
| 50 |
|
| 51 |
//FixIn: 2.0.5.4 |
| 52 |
// check internal subfolder of WP, like http://server.com/my-website/ ... link ... |
| 53 |
$server_url_sufix = ''; |
| 54 |
if ( function_exists( 'wp_parse_url' ) ) |
| 55 |
$wp_home_server_url = wp_parse_url( home_url() ); |
| 56 |
else |
| 57 |
$wp_home_server_url = @parse_url( home_url() ); |
| 58 |
|
| 59 |
if ( ( false !== $wp_home_server_url ) && (! empty( $wp_home_server_url[ 'path' ] ) ) ){ // seriously malformed URLs, parse_url() may return FALSE. |
| 60 |
$server_url_sufix = trim( $wp_home_server_url[ 'path' ] ); // [path] => /my-website |
| 61 |
$server_url_sufix = trim( $server_url_sufix, '/' ); // my-website |
| 62 |
if ( ! empty( $server_url_sufix ) ) { |
| 63 |
$check_sufix = substr( $my_parsed_url_path, 0, strlen( $server_url_sufix ) ); |
| 64 |
if ( $check_sufix === $server_url_sufix ) { |
| 65 |
$my_parsed_url_path = substr( $my_parsed_url_path, strlen( $server_url_sufix ) ); |
| 66 |
$my_parsed_url_path = trim( $my_parsed_url_path, '/' ); |
| 67 |
} |
| 68 |
} |
| 69 |
} |
| 70 |
//End FixIn: 2.0.5.4 |
| 71 |
|
| 72 |
|
| 73 |
|
| 74 |
// Get booking resources and Export Feed URLS |
| 75 |
$is_continue = false; |
| 76 |
if ( function_exists( 'wpbc_br_cache' ) ) { |
| 77 |
|
| 78 |
$resources_cache = wpbc_br_cache(); // Get booking resources from cache |
| 79 |
$resource_list = $resources_cache->get_resources(); |
| 80 |
|
| 81 |
foreach ( $resource_list as $res_id => $res_arr) { |
| 82 |
|
| 83 |
if ( ! empty( $res_arr[ 'export' ] ) ) { |
| 84 |
$resource_feed_url = $res_arr[ 'export' ]; |
| 85 |
} else { |
| 86 |
$resource_feed_url = ''; |
| 87 |
} |
| 88 |
|
| 89 |
$resource_feed_url = trim( $resource_feed_url, '/' ); |
| 90 |
if ( ! empty( $resource_feed_url ) ) { |
| 91 |
// if ( 0 === strpos( $my_parsed_url_path, $resource_feed_url ) ) { // First coocurence of $download_url_path in URL, like 'wpbm-ics-export' in 'http://beta/wpbm-ics-export/my-feed/' |
| 92 |
if ( $my_parsed_url_path === $resource_feed_url ) { |
| 93 |
$is_continue = true; |
| 94 |
break; |
| 95 |
} |
| 96 |
} |
| 97 |
} |
| 98 |
} else if ( function_exists( 'get_bk_option' ) ) { |
| 99 |
|
| 100 |
//TODO: cehck for Free version. |
| 101 |
$res_id = 1; |
| 102 |
$resource_feed_url = get_bk_option( 'booking_resource_export_ics_url' ); |
| 103 |
|
| 104 |
$resource_feed_url = trim( $resource_feed_url, '/' ); |
| 105 |
|
| 106 |
if ( ! empty( $resource_feed_url ) ) { |
| 107 |
if ( $my_parsed_url_path === $resource_feed_url ) { |
| 108 |
$is_continue = true; |
| 109 |
} |
| 110 |
} |
| 111 |
|
| 112 |
} else { |
| 113 |
return; // No Booking Calendar active, so ust opening some other page |
| 114 |
} |
| 115 |
|
| 116 |
|
| 117 |
if ( true === $is_continue) { |
| 118 |
|
| 119 |
$download_url_path = $resource_feed_url; |
| 120 |
|
| 121 |
$ics_export = wpbm_export_ics_feed__wpbm_ics( array( 'wh_booking_type' => $res_id ) ); |
| 122 |
|
| 123 |
if ( headers_sent() ){ |
| 124 |
die( 'headers_sent_before_download' ); |
| 125 |
} |
| 126 |
|
| 127 |
//////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| 128 |
// Prepare system before downloading set time limits, server output options |
| 129 |
//////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| 130 |
$disabled = explode( ',', ini_get( 'disable_functions' ) ); |
| 131 |
$is_func_disabled = in_array( 'set_time_limit', $disabled ); |
| 132 |
if ( ! $is_func_disabled && ! ini_get( 'safe_mode' ) ) { |
| 133 |
@set_time_limit( 0 ); |
| 134 |
} |
| 135 |
//FixIn: 2.0.18.2 |
| 136 |
// if ( function_exists( 'get_magic_quotes_runtime' ) && get_magic_quotes_runtime() && version_compare( phpversion(), '5.4', '<' ) ) { |
| 137 |
// set_magic_quotes_runtime( 0 ); |
| 138 |
// } |
| 139 |
|
| 140 |
@session_write_close(); |
| 141 |
if ( function_exists( 'apache_setenv' ) ) { |
| 142 |
@apache_setenv( 'no-gzip', 1 ); |
| 143 |
} |
| 144 |
@ini_set( 'zlib.output_compression', 'Off' ); |
| 145 |
|
| 146 |
@ob_end_clean(); // In case, if somewhere opeded output buffer, may be required for working fpassthru with large files |
| 147 |
|
| 148 |
|
| 149 |
//////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| 150 |
// Set Headers before file download |
| 151 |
//////////////////////////////////////////////////////////////////////////////////////////////////////////// |
| 152 |
$file = array(); |
| 153 |
$file['content_type'] = 'text/calendar; charset=UTF-8'; |
| 154 |
$file['name'] = 'wpbm.ics'; |
| 155 |
|
| 156 |
if ( function_exists( 'mb_detect_encoding' ) ) //FixIn: 2.0.5.3 |
| 157 |
$text_encoding = mb_detect_encoding( $ics_export ); |
| 158 |
else |
| 159 |
$text_encoding = ''; //Unknown |
| 160 |
|
| 161 |
//debuge( mb_detect_encoding( $ics_export ) );die; |
| 162 |
|
| 163 |
if ( ( is_array( $text_encoding ) ) |
| 164 |
&& ( ! in_array( 'UTF-8', $text_encoding ) ) |
| 165 |
){ |
| 166 |
$file['size'] = wpbm_get_bytes_from_str( $ics_export ); |
| 167 |
} else { |
| 168 |
$file['size'] = 0; // UTF-8 encoding, so size alculated incorrectly, probabaly |
| 169 |
} |
| 170 |
|
| 171 |
nocache_headers(); |
| 172 |
header( "Robots: none" . "\n" ); |
| 173 |
@header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 200 OK' . "\n" ); |
| 174 |
header( "Content-Type: " . $file['content_type'] . "\n" ); |
| 175 |
header( "Content-Description: File Transfer" . "\n" ); |
| 176 |
header( "Content-Disposition: inline; filename=\"" . $file['name'] . "\"" . "\n" ); |
| 177 |
header( "Content-Transfer-Encoding: binary" . "\n" ); |
| 178 |
|
| 179 |
if ( (int) $file['size'] > 0 ) |
| 180 |
header( "Content-Length: " . $file['size'] . "\n" ); |
| 181 |
|
| 182 |
|
| 183 |
echo $ics_export; |
| 184 |
|
| 185 |
exit; |
| 186 |
} |
| 187 |
// Unknown error, or just opening some other page |
| 188 |
|
| 189 |
|
| 190 |
} |
| 191 |
add_action( 'template_redirect', 'wpbm_make_export_ics_feeds' ); |
| 192 |
|
| 193 |
|
| 194 |
// --------------------------------------------------------------------------------------------------------------------- |
| 195 |
// Helpers for ICS export: |
| 196 |
// --------------------------------------------------------------------------------------------------------------------- |
| 197 |
|
| 198 |
/** |
| 199 |
* Escape ICS text per RFC 5545: backslash, comma, semicolon; newlines -> \n |
| 200 |
* |
| 201 |
* @param string $text |
| 202 |
* |
| 203 |
* @return string |
| 204 |
*/ |
| 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. |
| 210 |
|
| 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 booking dates have been limited to the |
| 401 |
* resources authorized for an iCalendar feed. It follows the same |
| 402 |
* range-compression rules as the 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 |
* Get one resource ID from the iCalendar export query. |
| 469 |
* |
| 470 |
* The scope expansion must only run for a feed URL that requested one resource. |
| 471 |
* Existing internal callers may pass a comma-separated resource list, which is |
| 472 |
* left untouched and must not be reinterpreted as one parent-resource feed. |
| 473 |
* |
| 474 |
* @param mixed $resource_query Resource selector received by the exporter. |
| 475 |
* |
| 476 |
* @return int Positive resource ID, or zero when the query is not one ID. |
| 477 |
*/ |
| 478 |
function wpbc_ics_export_get_requested_resource_id( $resource_query ) { |
| 479 |
|
| 480 |
if ( ! is_scalar( $resource_query ) ) { |
| 481 |
return 0; |
| 482 |
} |
| 483 |
|
| 484 |
$resource_query = trim( (string) $resource_query ); |
| 485 |
|
| 486 |
if ( ( '' === $resource_query ) || ( 1 !== preg_match( '/^[0-9]+$/', $resource_query ) ) ) { |
| 487 |
return 0; |
| 488 |
} |
| 489 |
|
| 490 |
return absint( $resource_query ); |
| 491 |
} |
| 492 |
|
| 493 |
|
| 494 |
/** |
| 495 |
* Resolve the resource IDs authorized for one iCalendar feed. |
| 496 |
* |
| 497 |
* Parent feeds always include the requested parent. When the opt-in setting is |
| 498 |
* enabled, direct capacity children owned by the same MultiUser account are |
| 499 |
* added. Descendants are not traversed because Booking Calendar capacity uses |
| 500 |
* a direct parent-child relationship. |
| 501 |
* |
| 502 |
* @param int $resource_id Resource requested by the iCalendar feed URL. |
| 503 |
* @param bool $include_children Whether direct child resources may be included. |
| 504 |
* |
| 505 |
* @return array { |
| 506 |
* Resolved export scope. |
| 507 |
* |
| 508 |
* @type bool $is_parent Whether the requested resource has direct children. |
| 509 |
* @type array $resource_ids Requested resource ID and any authorized child IDs. |
| 510 |
* } |
| 511 |
*/ |
| 512 |
function wpbc_ics_export_get_resource_scope( $resource_id, $include_children = false ) { |
| 513 |
|
| 514 |
$resource_id = absint( $resource_id ); |
| 515 |
$scope = array( |
| 516 |
'is_parent' => false, |
| 517 |
'resource_ids' => ( $resource_id > 0 ) ? array( $resource_id ) : array(), |
| 518 |
); |
| 519 |
|
| 520 |
if ( |
| 521 |
( $resource_id <= 0 ) |
| 522 |
|| ! class_exists( 'wpdev_bk_biz_l' ) |
| 523 |
|| ! function_exists( 'wpbc_br_cache' ) |
| 524 |
) { |
| 525 |
return $scope; |
| 526 |
} |
| 527 |
|
| 528 |
$resource_cache = wpbc_br_cache(); |
| 529 |
|
| 530 |
if ( ! is_object( $resource_cache ) || ! method_exists( $resource_cache, 'get_resources' ) ) { |
| 531 |
return $scope; |
| 532 |
} |
| 533 |
|
| 534 |
$resource_list = $resource_cache->get_resources(); |
| 535 |
|
| 536 |
if ( ! is_array( $resource_list ) ) { |
| 537 |
return $scope; |
| 538 |
} |
| 539 |
|
| 540 |
$parent_owner_id = null; |
| 541 |
|
| 542 |
foreach ( $resource_list as $resource_key => $resource_params ) { |
| 543 |
if ( ! is_array( $resource_params ) ) { |
| 544 |
continue; |
| 545 |
} |
| 546 |
|
| 547 |
$current_resource_id = isset( $resource_params['id'] ) |
| 548 |
? absint( $resource_params['id'] ) |
| 549 |
: absint( $resource_key ); |
| 550 |
|
| 551 |
if ( $resource_id === $current_resource_id ) { |
| 552 |
$parent_owner_id = isset( $resource_params['users'] ) ? absint( $resource_params['users'] ) : null; |
| 553 |
break; |
| 554 |
} |
| 555 |
} |
| 556 |
|
| 557 |
foreach ( $resource_list as $resource_key => $resource_params ) { |
| 558 |
if ( ! is_array( $resource_params ) ) { |
| 559 |
continue; |
| 560 |
} |
| 561 |
|
| 562 |
$parent_resource_id = isset( $resource_params['parent'] ) ? absint( $resource_params['parent'] ) : 0; |
| 563 |
|
| 564 |
if ( $resource_id !== $parent_resource_id ) { |
| 565 |
continue; |
| 566 |
} |
| 567 |
|
| 568 |
$scope['is_parent'] = true; |
| 569 |
|
| 570 |
if ( ! $include_children ) { |
| 571 |
continue; |
| 572 |
} |
| 573 |
|
| 574 |
$child_owner_id = isset( $resource_params['users'] ) ? absint( $resource_params['users'] ) : null; |
| 575 |
|
| 576 |
if ( ( null !== $child_owner_id ) && ( null === $parent_owner_id || $parent_owner_id !== $child_owner_id ) ) { |
| 577 |
continue; |
| 578 |
} |
| 579 |
|
| 580 |
$child_resource_id = isset( $resource_params['id'] ) |
| 581 |
? absint( $resource_params['id'] ) |
| 582 |
: absint( $resource_key ); |
| 583 |
|
| 584 |
if ( $child_resource_id > 0 ) { |
| 585 |
$scope['resource_ids'][] = $child_resource_id; |
| 586 |
} |
| 587 |
} |
| 588 |
|
| 589 |
$scope['resource_ids'] = array_values( array_unique( array_map( 'absint', $scope['resource_ids'] ) ) ); |
| 590 |
|
| 591 |
return $scope; |
| 592 |
} |
| 593 |
|
| 594 |
|
| 595 |
/** |
| 596 |
* Limit bookings and booking dates to an iCalendar resource scope. |
| 597 |
* |
| 598 |
* A NULL booking-date type ID represents the resource stored in the main |
| 599 |
* booking row. An explicit type ID represents another capacity resource used |
| 600 |
* by that booking. Identical timestamps inside one booking are collapsed so a |
| 601 |
* booking occupying multiple capacity units still exports as one event; dates |
| 602 |
* from different booking IDs are never merged. |
| 603 |
* |
| 604 |
* @param array $bookings_arr Booking-list API result. |
| 605 |
* @param array $resource_ids Resource IDs allowed in the feed. |
| 606 |
* |
| 607 |
* @return array Filtered booking-list API result. |
| 608 |
*/ |
| 609 |
function wpbc_ics_export_filter_bookings_by_resource_scope( $bookings_arr, $resource_ids ) { |
| 610 |
|
| 611 |
$resource_ids = array_values( array_filter( array_unique( array_map( 'absint', (array) $resource_ids ) ) ) ); |
| 612 |
|
| 613 |
if ( empty( $resource_ids ) || empty( $bookings_arr['bookings'] ) || ! is_array( $bookings_arr['bookings'] ) ) { |
| 614 |
return $bookings_arr; |
| 615 |
} |
| 616 |
|
| 617 |
$allowed_resource_ids = array_fill_keys( $resource_ids, true ); |
| 618 |
|
| 619 |
foreach ( $bookings_arr['bookings'] as $booking_id => $booking_obj ) { |
| 620 |
$main_resource_id = isset( $booking_obj->booking_type ) ? absint( $booking_obj->booking_type ) : 0; |
| 621 |
$scoped_dates = array(); |
| 622 |
|
| 623 |
if ( empty( $booking_obj->dates ) || ! is_array( $booking_obj->dates ) ) { |
| 624 |
unset( $bookings_arr['bookings'][ $booking_id ] ); |
| 625 |
continue; |
| 626 |
} |
| 627 |
|
| 628 |
foreach ( $booking_obj->dates as $booking_date ) { |
| 629 |
$date_resource_id = ! empty( $booking_date->type_id ) |
| 630 |
? absint( $booking_date->type_id ) |
| 631 |
: $main_resource_id; |
| 632 |
|
| 633 |
if ( ! isset( $allowed_resource_ids[ $date_resource_id ] ) || ! isset( $booking_date->booking_date ) ) { |
| 634 |
continue; |
| 635 |
} |
| 636 |
|
| 637 |
$scoped_dates[ (string) $booking_date->booking_date ] = $booking_date; |
| 638 |
} |
| 639 |
|
| 640 |
if ( empty( $scoped_dates ) ) { |
| 641 |
unset( $bookings_arr['bookings'][ $booking_id ] ); |
| 642 |
continue; |
| 643 |
} |
| 644 |
|
| 645 |
$scoped_dates = array_values( $scoped_dates ); |
| 646 |
|
| 647 |
usort( |
| 648 |
$scoped_dates, |
| 649 |
static function ( $first_date, $second_date ) { |
| 650 |
return strcmp( $first_date->booking_date, $second_date->booking_date ); |
| 651 |
} |
| 652 |
); |
| 653 |
|
| 654 |
$short_dates = wpbc_ics_export_build_short_dates( $scoped_dates, $main_resource_id ); |
| 655 |
|
| 656 |
$booking_obj->dates = $scoped_dates; |
| 657 |
$booking_obj->dates_short = $short_dates['dates_short']; |
| 658 |
$booking_obj->dates_short_id = $short_dates['dates_short_id']; |
| 659 |
} |
| 660 |
|
| 661 |
$bookings_arr['bookings_count'] = count( $bookings_arr['bookings'] ); |
| 662 |
|
| 663 |
return $bookings_arr; |
| 664 |
} |
| 665 |
|
| 666 |
|
| 667 |
/** |
| 668 |
* Limit a parent-resource iCalendar feed to dates occupying the parent itself. |
| 669 |
* |
| 670 |
* This public compatibility wrapper preserves the behavior introduced in |
| 671 |
* Booking Manager 2.1.20 for callers that use the original helper directly. |
| 672 |
* The exporter uses the generalized scope helper when child aggregation is |
| 673 |
* enabled. |
| 674 |
* |
| 675 |
* @param array $bookings_arr Booking-list API result. |
| 676 |
* @param int $resource_id Resource requested by the iCalendar feed URL. |
| 677 |
* |
| 678 |
* @return array Filtered booking-list API result. |
| 679 |
*/ |
| 680 |
function wpbc_ics_export_filter_parent_resource_bookings( $bookings_arr, $resource_id ) { |
| 681 |
|
| 682 |
$resource_scope = wpbc_ics_export_get_resource_scope( $resource_id, false ); |
| 683 |
|
| 684 |
if ( ! $resource_scope['is_parent'] ) { |
| 685 |
return $bookings_arr; |
| 686 |
} |
| 687 |
|
| 688 |
return wpbc_ics_export_filter_bookings_by_resource_scope( $bookings_arr, $resource_scope['resource_ids'] ); |
| 689 |
} |
| 690 |
|
| 691 |
|
| 692 |
/** |
| 693 |
* Define export ICS here. |
| 694 |
* For testing: http://beta/?feed=wpbm-ics |
| 695 |
* |
| 696 |
* @param array $param - [ 'wh_booking_type' => '1', 'wh_trash' => '' ]. |
| 697 |
* |
| 698 |
* @return string |
| 699 |
*/ |
| 700 |
function wpbm_export_ics_feed__wpbm_ics( $param = array( 'wh_booking_type' => '1', 'wh_trash' => '' ) ) { |
| 701 |
|
| 702 |
// FixIn: 2.1.13.1 – timezone-safe, UTC stamps, OTA all‑day, single VTIMEZONE, no closures. |
| 703 |
|
| 704 |
if ( ! function_exists( 'wpbc_api_get_bookings_arr' ) ) { |
| 705 |
return ''; |
| 706 |
} |
| 707 |
|
| 708 |
// Resolve export timezone: option 'booking_gcal_timezone' if set, else WP site tz. |
| 709 |
$site_tz_obj = wp_timezone(); |
| 710 |
$tzid_opt_raw = (string) get_bk_option( 'booking_gcal_timezone' ); |
| 711 |
$tzid_opt = trim( $tzid_opt_raw ); // "" when “Default” is selected |
| 712 |
|
| 713 |
// Validate TZID (must be a known IANA zone). |
| 714 |
$tzid = ''; |
| 715 |
if ( $tzid_opt !== '' ) { |
| 716 |
try { |
| 717 |
// Will throw on invalid IDs (e.g., typos). |
| 718 |
$check = new DateTimeZone( $tzid_opt ); |
| 719 |
$tzid = $tzid_opt; // valid — we’ll emit TZID and VTIMEZONE. |
| 720 |
} |
| 721 |
catch ( Exception $e ) { |
| 722 |
$tzid = ''; // invalid — treat as Default (no TZID emitted). |
| 723 |
} |
| 724 |
} |
| 725 |
|
| 726 |
// Export timezone object (priority: booking_gcal_timezone > site tz). |
| 727 |
$export_tz = ( $tzid !== '' ) ? new DateTimeZone( $tzid ) : $site_tz_obj; |
| 728 |
|
| 729 |
// End date of getting bookings (cutoff +2 years) – done in export tz. |
| 730 |
$cutoff = ( new DateTimeImmutable( 'now', $export_tz ) )->modify( '+2 years' )->format( 'Y-m-d' ); |
| 731 |
$param['wh_booking_date2'] = $cutoff; |
| 732 |
|
| 733 |
// Export only approved bookings (FixIn: 2.0.11.1 / 8.5.2.3). |
| 734 |
$booking_is_ics_export_only_approved = get_bk_option( 'booking_is_ics_export_only_approved' ); |
| 735 |
$param['wh_approved'] = ( 'On' === $booking_is_ics_export_only_approved ) ? '1' : ''; |
| 736 |
|
| 737 |
// '' | 'imported' | 'plugin' (FixIn: 8.8.3.19 / 2.0.20.2). |
| 738 |
if ( 'imported' === get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) { |
| 739 |
$param['wh_sync_gid'] = 'imported'; |
| 740 |
} |
| 741 |
if ( 'plugin' === get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) { |
| 742 |
$param['wh_sync_gid'] = 'plugin'; |
| 743 |
} |
| 744 |
|
| 745 |
$_REQUEST['tab'] = 'vm_calendar'; // FixIn: 10.12.3.2 / 8.5.2.15 / 2.0.11.2. |
| 746 |
|
| 747 |
// Get array of bookings. |
| 748 |
$export_resource_id = isset( $param['wh_booking_type'] ) |
| 749 |
? wpbc_ics_export_get_requested_resource_id( $param['wh_booking_type'] ) |
| 750 |
: 0; |
| 751 |
$include_child_resources = ( 'On' === get_wpbm_option( 'wpbm_is_export_parent_with_children', 'Off' ) ); |
| 752 |
$resource_scope = wpbc_ics_export_get_resource_scope( $export_resource_id, $include_child_resources ); |
| 753 |
|
| 754 |
if ( $resource_scope['is_parent'] && ! empty( $resource_scope['resource_ids'] ) ) { |
| 755 |
$param['wh_booking_type'] = implode( ',', array_map( 'absint', $resource_scope['resource_ids'] ) ); |
| 756 |
} |
| 757 |
|
| 758 |
$bookings_arr = wpbc_api_get_bookings_arr( $param ); |
| 759 |
|
| 760 |
if ( $resource_scope['is_parent'] ) { |
| 761 |
$bookings_arr = wpbc_ics_export_filter_bookings_by_resource_scope( $bookings_arr, $resource_scope['resource_ids'] ); |
| 762 |
} |
| 763 |
|
| 764 |
ob_start(); |
| 765 |
|
| 766 |
// Create the iCal object. |
| 767 |
$icalobj = new ZCiCal(); |
| 768 |
|
| 769 |
// Calendar headers (library adds VERSION/PRODID; we add refresh hints). |
| 770 |
$datanode = new ZCiCalDataNode( 'REFRESH-INTERVAL;VALUE=DURATION:PT10M' ); // 10 minutes. |
| 771 |
$icalobj->curnode->data[ $datanode->getName() ] = $datanode; |
| 772 |
$datanode = new ZCiCalDataNode( 'X-PUBLISHED-TTL:PT10M' ); // 10 minutes. |
| 773 |
$icalobj->curnode->data[ $datanode->getName() ] = $datanode; |
| 774 |
|
| 775 |
$all_day_export = ( 'On' === get_wpbm_option( 'wpbm_is_export_only_full_days' ) ); // OTA toggle. |
| 776 |
|
| 777 |
// Host for UIDs (global uniqueness). |
| 778 |
$host = wp_parse_url( home_url(), PHP_URL_HOST ); |
| 779 |
if ( empty( $host ) ) { |
| 780 |
$host = 'example.org'; |
| 781 |
} |
| 782 |
if ( 'localhost' === $host ) { |
| 783 |
$host = 'localhost.localdomain'; |
| 784 |
} |
| 785 |
|
| 786 |
// Track VTIMEZONE additions per TZID. |
| 787 |
$__wpbc_added_vtimezones = array(); |
| 788 |
|
| 789 |
foreach ( $bookings_arr['bookings'] as $bk_id => $bk_arr ) { |
| 790 |
|
| 791 |
// Create the master event within the iCal object. |
| 792 |
$eventobj = new ZCiCalNode( 'VEVENT', $icalobj->curnode ); |
| 793 |
|
| 794 |
// --------------------------------------------------------------------------------------------- |
| 795 |
// SUMMARY |
| 796 |
// --------------------------------------------------------------------------------------------- |
| 797 |
if ( function_exists( 'get_title_for_showing_in_day' ) ) { |
| 798 |
$what_show_in_day_template = get_bk_option( 'booking_default_title_in_day_for_calendar_view_mode' ); |
| 799 |
$title = get_title_for_showing_in_day( $bk_id, $bookings_arr['bookings'], $what_show_in_day_template ); |
| 800 |
} else { |
| 801 |
$title = ( isset( $bk_arr->form_data['name'] ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' ); |
| 802 |
$title .= ( isset( $bk_arr->form_data['firstname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['firstname'] ) |
| 803 |
: '' ); |
| 804 |
$title .= ( isset( $bk_arr->form_data['secondname'] ) |
| 805 |
? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' ); |
| 806 |
$title .= ( isset( $bk_arr->form_data['lastname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['lastname'] ) |
| 807 |
: '' ); |
| 808 |
} |
| 809 |
|
| 810 |
$is_hide_details = get_wpbm_option( 'wpbm_is_hide_details' ); |
| 811 |
if ( 'On' === $is_hide_details ) { |
| 812 |
$title = ''; |
| 813 |
} |
| 814 |
$title = ltrim( (string) $title ); // optional: avoid leading space. |
| 815 |
$title_ics = wpbc_ics_escape_text( wpbm_esc_to_plain_text( $title ) ); |
| 816 |
$eventobj->addNode( new ZCiCalDataNode( 'SUMMARY:' . $title_ics ) ); |
| 817 |
|
| 818 |
// --------------------------------------------------------------------------------------------- |
| 819 |
// DESCRIPTION |
| 820 |
// --------------------------------------------------------------------------------------------- |
| 821 |
$description_raw = ( 'On' === $is_hide_details ) ? '' : $bk_arr->form_show; |
| 822 |
$description_raw = ltrim( (string) $description_raw ); // remove leading whitespace/newlines. |
| 823 |
$description_ics = wpbc_ics_escape_text( wpbm_esc_to_plain_text( $description_raw ) ); |
| 824 |
$description_ics = preg_replace('/\s+/', ' ', $description_ics); |
| 825 |
$eventobj->addNode( new ZCiCalDataNode( 'DESCRIPTION:' . $description_ics ) ); |
| 826 |
// --------------------------------------------------------------------------------------------- |
| 827 |
// START & END (master span from dates_short) |
| 828 |
// --------------------------------------------------------------------------------------------- |
| 829 |
$event_start = $bk_arr->dates_short[0]; |
| 830 |
if ( ( count( $bk_arr->dates_short ) > 1 ) && ( '-' === $bk_arr->dates_short[1] ) ) { |
| 831 |
$event_end = $bk_arr->dates_short[2]; |
| 832 |
$event_end = wpbm_get_next_date_if_it_full_date( $event_end ); // make end exclusive for YYYY-mm-dd |
| 833 |
$period_start_index = 3; |
| 834 |
} else { // comma or single day. |
| 835 |
$event_end = $bk_arr->dates_short[0]; |
| 836 |
$event_end = wpbm_get_next_date_if_it_full_date( $event_end ); |
| 837 |
$period_start_index = 1; |
| 838 |
} |
| 839 |
|
| 840 |
if ( $all_day_export ) { |
| 841 |
// OTA all-day mode (end exclusive). |
| 842 |
$event_start = substr( $event_start, 0, 10 ); // Y-m-d |
| 843 |
$event_end = substr( $event_end, 0, 10 ); // Y-m-d |
| 844 |
if ( $event_start === $event_end ) { |
| 845 |
$event_end = wpbc_add_days_ymd( $event_end, 1, $export_tz ); |
| 846 |
} |
| 847 |
} else { |
| 848 |
// Timed mode: normalize seconds to :00 for clean slots. |
| 849 |
if ( strlen( $event_start ) > 10 ) { |
| 850 |
$event_start = wpbc_normalize_sec_sql( $event_start, $export_tz ); |
| 851 |
} |
| 852 |
if ( strlen( $event_end ) > 10 ) { |
| 853 |
$event_end = wpbc_normalize_sec_sql( $event_end, $export_tz ); |
| 854 |
} |
| 855 |
} |
| 856 |
|
| 857 |
// Add VTIMEZONE once (timed mode with TZID). |
| 858 |
if ( ! $all_day_export && ! empty( $tzid ) ) { |
| 859 |
wpbc_add_vtimezone_once( $icalobj, $tzid, substr( $event_start, 0, 4 ), substr( $event_end, 0, 4 ), $__wpbc_added_vtimezones ); |
| 860 |
} |
| 861 |
|
| 862 |
// DTSTART / DTEND for the master VEVENT (coerced so DTEND > DTSTART). |
| 863 |
if ( $all_day_export ) { |
| 864 |
list( $event_start, $event_end ) = wpbc_ics_coerce_order( $event_start, $event_end, $export_tz, true ); |
| 865 |
$eventobj->addNode( new ZCiCalDataNode( 'DTSTART;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $event_start ) ) ); |
| 866 |
$eventobj->addNode( new ZCiCalDataNode( 'DTEND;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $event_end ) ) ); |
| 867 |
} else { |
| 868 |
list( $event_start_fixed, $event_end_fixed ) = wpbc_ics_coerce_order( $event_start, $event_end, $export_tz, false ); |
| 869 |
if ( ! empty( $tzid ) ) { |
| 870 |
$eventobj->addNode( new ZCiCalDataNode( 'DTSTART;TZID=' . $tzid . ':' . wpbc_to_ics_local( $event_start_fixed, $export_tz ) ) ); |
| 871 |
$eventobj->addNode( new ZCiCalDataNode( 'DTEND;TZID=' . $tzid . ':' . wpbc_to_ics_local( $event_end_fixed, $export_tz ) ) ); |
| 872 |
} else { |
| 873 |
$eventobj->addNode( new ZCiCalDataNode( 'DTSTART:' . wpbc_to_ics_utc( $event_start_fixed, $export_tz ) ) ); |
| 874 |
$eventobj->addNode( new ZCiCalDataNode( 'DTEND:' . wpbc_to_ics_utc( $event_end_fixed, $export_tz ) ) ); |
| 875 |
} |
| 876 |
} |
| 877 |
|
| 878 |
// --------------------------------------------------------------------------------------------- |
| 879 |
// STATUS / TRANSP |
| 880 |
// --------------------------------------------------------------------------------------------- |
| 881 |
$ics_status = 'TENTATIVE'; |
| 882 |
if ( ! empty( $bk_arr->trash ) ) { |
| 883 |
$ics_status = 'CANCELLED'; |
| 884 |
} elseif ( ! empty( $bk_arr->dates[0]->approved ) ) { |
| 885 |
$ics_status = 'CONFIRMED'; |
| 886 |
} |
| 887 |
$eventobj->addNode( new ZCiCalDataNode( 'STATUS:' . $ics_status ) ); |
| 888 |
$eventobj->addNode( new ZCiCalDataNode( 'TRANSP:OPAQUE' ) ); |
| 889 |
|
| 890 |
// --------------------------------------------------------------------------------------------- |
| 891 |
// Expand extra dates/ranges into standalone VEVENTs (better than RDATE/RRULE for OTA/clients) |
| 892 |
// --------------------------------------------------------------------------------------------- |
| 893 |
$segments = array(); // each: ['start_sql' => 'Y-m-d[ H:i:s]', 'end_sql' => 'Y-m-d[ H:i:s]'|null] |
| 894 |
$prev = ''; |
| 895 |
$current = null; |
| 896 |
|
| 897 |
foreach ( $bk_arr->dates_short as $i => $tok ) { |
| 898 |
if ( $i < $period_start_index ) { |
| 899 |
continue; |
| 900 |
} |
| 901 |
if ( ',' !== $tok && '-' !== $tok ) { |
| 902 |
if ( '-' === $prev ) { |
| 903 |
$tok_norm = wpbm_get_next_date_if_it_full_date( $tok ); // range end (exclusive if Y-m-d) |
| 904 |
} else { |
| 905 |
$tok_norm = wpbm_get_only_date_if_it_full_date( $tok ); // start or single |
| 906 |
} |
| 907 |
if ( '-' === $prev ) { |
| 908 |
if ( $current ) { |
| 909 |
$current['end_sql'] = $tok_norm; |
| 910 |
} |
| 911 |
} else { |
| 912 |
$current = array( 'start_sql' => $tok_norm, 'end_sql' => null ); |
| 913 |
} |
| 914 |
} elseif ( ',' === $tok ) { |
| 915 |
if ( $current ) { |
| 916 |
$segments[] = $current; |
| 917 |
$current = null; |
| 918 |
} |
| 919 |
} |
| 920 |
$prev = $tok; |
| 921 |
} |
| 922 |
if ( $current ) { |
| 923 |
$segments[] = $current; |
| 924 |
} |
| 925 |
|
| 926 |
if ( ! empty( $segments ) ) { |
| 927 |
// Duration basis for timed events. |
| 928 |
$master_duration = 0; |
| 929 |
if ( ! $all_day_export ) { |
| 930 |
$basis_start = isset( $event_start_fixed ) ? $event_start_fixed : $event_start; |
| 931 |
$basis_end = isset( $event_end_fixed ) ? $event_end_fixed : $event_end; |
| 932 |
$dts = DateTime::createFromFormat( 'Y-m-d H:i:s', $basis_start, $export_tz ); |
| 933 |
$dte = DateTime::createFromFormat( 'Y-m-d H:i:s', $basis_end, $export_tz ); |
| 934 |
$master_duration = ( $dts && $dte && $dte > $dts ) ? ( $dte->getTimestamp() - $dts->getTimestamp() ) |
| 935 |
: 3600; |
| 936 |
} |
| 937 |
|
| 938 |
$seg_idx = 0; |
| 939 |
foreach ( $segments as $s ) { |
| 940 |
++ $seg_idx; |
| 941 |
$ev = new ZCiCalNode( 'VEVENT', $icalobj->curnode ); |
| 942 |
|
| 943 |
if ( $all_day_export ) { |
| 944 |
$start_d = substr( $s['start_sql'], 0, 10 ); |
| 945 |
$end_d = ! empty( $s['end_sql'] ) ? substr( $s['end_sql'], 0, 10 ) : $start_d; |
| 946 |
list( $start_d, $end_d ) = wpbc_ics_coerce_order( $start_d, $end_d, $export_tz, true ); |
| 947 |
|
| 948 |
$ev->addNode( new ZCiCalDataNode( 'DTSTART;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $start_d ) ) ); |
| 949 |
$ev->addNode( new ZCiCalDataNode( 'DTEND;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $end_d ) ) ); |
| 950 |
|
| 951 |
} else { |
| 952 |
$start_sql = ( strlen( $s['start_sql'] ) > 10 ) |
| 953 |
? wpbc_normalize_sec_sql( $s['start_sql'], $export_tz ) : $s['start_sql']; |
| 954 |
if ( ! empty( $s['end_sql'] ) ) { |
| 955 |
$end_sql = ( strlen( $s['end_sql'] ) > 10 ) |
| 956 |
? wpbc_normalize_sec_sql( $s['end_sql'], $export_tz ) : $s['end_sql']; |
| 957 |
} else { |
| 958 |
$dt = DateTime::createFromFormat( 'Y-m-d H:i:s', $start_sql, $export_tz ); |
| 959 |
if ( $dt ) { |
| 960 |
$dt->modify( '+' . $master_duration . ' seconds' ); |
| 961 |
$end_sql = $dt->format( 'Y-m-d H:i:s' ); |
| 962 |
} else { |
| 963 |
$end_sql = $start_sql; |
| 964 |
} |
| 965 |
} |
| 966 |
|
| 967 |
// Ensure DTEND > DTSTART for each segment. |
| 968 |
list( $start_sql, $end_sql ) = wpbc_ics_coerce_order( $start_sql, $end_sql, $export_tz, false ); |
| 969 |
|
| 970 |
if ( ! empty( $tzid ) ) { |
| 971 |
$ev->addNode( new ZCiCalDataNode( 'DTSTART;TZID=' . $tzid . ':' . wpbc_to_ics_local( $start_sql, $export_tz ) ) ); |
| 972 |
$ev->addNode( new ZCiCalDataNode( 'DTEND;TZID=' . $tzid . ':' . wpbc_to_ics_local( $end_sql, $export_tz ) ) ); |
| 973 |
} else { |
| 974 |
$ev->addNode( new ZCiCalDataNode( 'DTSTART:' . wpbc_to_ics_utc( $start_sql, $export_tz ) ) ); |
| 975 |
$ev->addNode( new ZCiCalDataNode( 'DTEND:' . wpbc_to_ics_utc( $end_sql, $export_tz ) ) ); |
| 976 |
} |
| 977 |
} |
| 978 |
|
| 979 |
// Copy common fields. |
| 980 |
$ev->addNode( new ZCiCalDataNode( 'SUMMARY:' . $title_ics ) ); |
| 981 |
$ev->addNode( new ZCiCalDataNode( 'DESCRIPTION:' . $description_ics ) ); |
| 982 |
$ev->addNode( new ZCiCalDataNode( 'STATUS:' . $ics_status ) ); |
| 983 |
$ev->addNode( new ZCiCalDataNode( 'TRANSP:OPAQUE' ) ); |
| 984 |
|
| 985 |
// Unique UID per extra VEVENT. |
| 986 |
$uid_extra = $bk_id . '-' . $seg_idx . '@' . $host; |
| 987 |
$ev->addNode( new ZCiCalDataNode( 'UID:' . $uid_extra ) ); |
| 988 |
|
| 989 |
// Timestamps in UTC Z. |
| 990 |
$ev->addNode( new ZCiCalDataNode( 'DTSTAMP:' . wpbc_to_ics_utc( null, $export_tz ) ) ); |
| 991 |
$ev->addNode( new ZCiCalDataNode( 'CREATED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) ); |
| 992 |
$ev->addNode( new ZCiCalDataNode( 'LAST-MODIFIED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) ); |
| 993 |
} |
| 994 |
} |
| 995 |
|
| 996 |
// --------------------------------------------------------------------------------------------- |
| 997 |
// UID (master) |
| 998 |
// --------------------------------------------------------------------------------------------- |
| 999 |
$uid = $bk_id . '@' . $host; // stable UID not tied to start time. |
| 1000 |
$eventobj->addNode( new ZCiCalDataNode( 'UID:' . $uid ) ); |
| 1001 |
|
| 1002 |
// DTSTAMP / CREATED / LAST-MODIFIED in UTC Z. |
| 1003 |
$eventobj->addNode( new ZCiCalDataNode( 'DTSTAMP:' . wpbc_to_ics_utc( null, $export_tz ) ) ); |
| 1004 |
$eventobj->addNode( new ZCiCalDataNode( 'CREATED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) ); |
| 1005 |
$eventobj->addNode( new ZCiCalDataNode( 'LAST-MODIFIED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) ); |
| 1006 |
|
| 1007 |
// --------------------------------------------------------------------------------------------- |
| 1008 |
// ATTENDEE (optional) |
| 1009 |
// --------------------------------------------------------------------------------------------- |
| 1010 |
if ( isset( $bk_arr->form_data['email'] ) && ( 'On' !== $is_hide_details ) ) { |
| 1011 |
$cn_title = ( isset( $bk_arr->form_data['name'] ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' ); |
| 1012 |
$cn_title .= ( isset( $bk_arr->form_data['firstname'] ) |
| 1013 |
? ' ' . esc_textarea( $bk_arr->form_data['firstname'] ) : '' ); |
| 1014 |
$cn_title .= ( isset( $bk_arr->form_data['secondname'] ) |
| 1015 |
? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' ); |
| 1016 |
$cn_title .= ( isset( $bk_arr->form_data['lastname'] ) |
| 1017 |
? ' ' . esc_textarea( $bk_arr->form_data['lastname'] ) : '' ); |
| 1018 |
|
| 1019 |
$cn_clean = trim( wpbm_esc_to_plain_text( $cn_title ) ); |
| 1020 |
$cn_clean = addcslashes( $cn_clean, "\\,;\"" ); // escape per RFC |
| 1021 |
$attendee_email = $bk_arr->form_data['email']; |
| 1022 |
$eventobj->addNode( new ZCiCalDataNode( 'ATTENDEE;CN="' . $cn_clean . '":MAILTO:' . $attendee_email ) ); |
| 1023 |
} |
| 1024 |
} |
| 1025 |
|
| 1026 |
// Output iCalendar text. |
| 1027 |
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped |
| 1028 |
echo $icalobj->export(); |
| 1029 |
|
| 1030 |
$ics_export = ob_get_contents(); |
| 1031 |
$ics_export = trim( $ics_export ); |
| 1032 |
ob_end_clean(); |
| 1033 |
|
| 1034 |
return $ics_export; |
| 1035 |
} |
| 1036 |
|
| 1037 |
|
| 1038 |
|
| 1039 |
/** Get Next day in MySQL format without time, if current day its Full day - ending with 00:00:00 |
| 1040 |
* |
| 1041 |
* @param string $mysql_date - 2017-07-12 00:00:00 |
| 1042 |
* @return string - 2017-07-13 |
| 1043 |
*/ |
| 1044 |
function wpbm_get_next_date_if_it_full_date( $mysql_date ) { |
| 1045 |
|
| 1046 |
// Check if this date ending with 00:00:00 its means that it full day, |
| 1047 |
// 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 |
| 1048 |
if ( ( strlen( $mysql_date ) > 10 ) // , like 2017-07-12 00:00:00 |
| 1049 |
&& ( substr( $mysql_date, 11 ) == '00:00:00' ) |
| 1050 |
) { |
| 1051 |
|
| 1052 |
$mysql_date = date_i18n( "Y-m-d 00:00:00", strtotime( $mysql_date ) ); |
| 1053 |
$mysql_date = strtotime( $mysql_date ); |
| 1054 |
$mysql_date = date_i18n( "Y-m-d", strtotime( '+1 day', $mysql_date ) ); |
| 1055 |
} |
| 1056 |
|
| 1057 |
return $mysql_date; |
| 1058 |
} |
| 1059 |
|
| 1060 |
|
| 1061 |
/** Get day in MySQL format without time, if current day its Full day - ending with 00:00:00 |
| 1062 |
* |
| 1063 |
* @param string $mysql_date - 2017-07-12 00:00:00 |
| 1064 |
* @return string - 2017-07-12 |
| 1065 |
*/ |
| 1066 |
function wpbm_get_only_date_if_it_full_date( $mysql_date ) { |
| 1067 |
|
| 1068 |
if ( ( strlen( $mysql_date ) > 10 ) // , like 2017-07-12 00:00:00 |
| 1069 |
&& ( substr( $mysql_date, 11) == '00:00:00' ) |
| 1070 |
) { |
| 1071 |
$mysql_date = substr( $mysql_date, 0, 10 ); |
| 1072 |
} |
| 1073 |
|
| 1074 |
return $mysql_date; |
| 1075 |
} |
| 1076 |
|