Settings > Permalink Settings page by clicking on "Save changes" button. * Finally, to display your feed, you’ll first need to flush your WordPress rewrite rules. * The easiest way to do this is by logging in to the WordPress admin, and clicking Settings -> Permalinks. * Once here, just click Save Changes, which will flush the rewrite rules. * * Otherwsie need to run ONLY once this: * * global $wp_rewrite; * $wp_rewrite->flush_rules(); * */ function wpbm_make_export_ics_feeds(){ if ( function_exists( 'wp_parse_url' ) ) $my_parsed_url = wp_parse_url( $_SERVER[ 'REQUEST_URI' ] ); else $my_parsed_url = @parse_url( $_SERVER[ 'REQUEST_URI' ] ); if ( false === $my_parsed_url ) { // seriously malformed URLs, parse_url() may return FALSE. return; } $my_parsed_url_path = trim( $my_parsed_url[ 'path' ] ); $my_parsed_url_path = trim( $my_parsed_url_path, '/' ); //FixIn: 2.0.5.4 // check internal subfolder of WP, like http://server.com/my-website/ ... link ... $server_url_sufix = ''; if ( function_exists( 'wp_parse_url' ) ) $wp_home_server_url = wp_parse_url( home_url() ); else $wp_home_server_url = @parse_url( home_url() ); if ( ( false !== $wp_home_server_url ) && (! empty( $wp_home_server_url[ 'path' ] ) ) ){ // seriously malformed URLs, parse_url() may return FALSE. $server_url_sufix = trim( $wp_home_server_url[ 'path' ] ); // [path] => /my-website $server_url_sufix = trim( $server_url_sufix, '/' ); // my-website if ( ! empty( $server_url_sufix ) ) { $check_sufix = substr( $my_parsed_url_path, 0, strlen( $server_url_sufix ) ); if ( $check_sufix === $server_url_sufix ) { $my_parsed_url_path = substr( $my_parsed_url_path, strlen( $server_url_sufix ) ); $my_parsed_url_path = trim( $my_parsed_url_path, '/' ); } } } //End FixIn: 2.0.5.4 // Get booking resources and Export Feed URLS $is_continue = false; if ( function_exists( 'wpbc_br_cache' ) ) { $resources_cache = wpbc_br_cache(); // Get booking resources from cache $resource_list = $resources_cache->get_resources(); foreach ( $resource_list as $res_id => $res_arr) { if ( ! empty( $res_arr[ 'export' ] ) ) { $resource_feed_url = $res_arr[ 'export' ]; } else { $resource_feed_url = ''; } $resource_feed_url = trim( $resource_feed_url, '/' ); if ( ! empty( $resource_feed_url ) ) { // 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/' if ( $my_parsed_url_path === $resource_feed_url ) { $is_continue = true; break; } } } } else if ( function_exists( 'get_bk_option' ) ) { //TODO: cehck for Free version. $res_id = 1; $resource_feed_url = get_bk_option( 'booking_resource_export_ics_url' ); $resource_feed_url = trim( $resource_feed_url, '/' ); if ( ! empty( $resource_feed_url ) ) { if ( $my_parsed_url_path === $resource_feed_url ) { $is_continue = true; } } } else { return; // No Booking Calendar active, so ust opening some other page } if ( true === $is_continue) { $download_url_path = $resource_feed_url; $ics_export = wpbm_export_ics_feed__wpbm_ics( array( 'wh_booking_type' => $res_id ) ); if ( headers_sent() ){ die( 'headers_sent_before_download' ); } //////////////////////////////////////////////////////////////////////////////////////////////////////////// // Prepare system before downloading set time limits, server output options //////////////////////////////////////////////////////////////////////////////////////////////////////////// $disabled = explode( ',', ini_get( 'disable_functions' ) ); $is_func_disabled = in_array( 'set_time_limit', $disabled ); if ( ! $is_func_disabled && ! ini_get( 'safe_mode' ) ) { @set_time_limit( 0 ); } //FixIn: 2.0.18.2 // if ( function_exists( 'get_magic_quotes_runtime' ) && get_magic_quotes_runtime() && version_compare( phpversion(), '5.4', '<' ) ) { // set_magic_quotes_runtime( 0 ); // } @session_write_close(); if ( function_exists( 'apache_setenv' ) ) { @apache_setenv( 'no-gzip', 1 ); } @ini_set( 'zlib.output_compression', 'Off' ); @ob_end_clean(); // In case, if somewhere opeded output buffer, may be required for working fpassthru with large files //////////////////////////////////////////////////////////////////////////////////////////////////////////// // Set Headers before file download //////////////////////////////////////////////////////////////////////////////////////////////////////////// $file = array(); $file['content_type'] = 'text/calendar; charset=UTF-8'; $file['name'] = 'wpbm.ics'; if ( function_exists( 'mb_detect_encoding' ) ) //FixIn: 2.0.5.3 $text_encoding = mb_detect_encoding( $ics_export ); else $text_encoding = ''; //Unknown //debuge( mb_detect_encoding( $ics_export ) );die; if ( ( is_array( $text_encoding ) ) && ( ! in_array( 'UTF-8', $text_encoding ) ) ){ $file['size'] = wpbm_get_bytes_from_str( $ics_export ); } else { $file['size'] = 0; // UTF-8 encoding, so size alculated incorrectly, probabaly } nocache_headers(); header( "Robots: none" . "\n" ); @header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 200 OK' . "\n" ); header( "Content-Type: " . $file['content_type'] . "\n" ); header( "Content-Description: File Transfer" . "\n" ); header( "Content-Disposition: inline; filename=\"" . $file['name'] . "\"" . "\n" ); header( "Content-Transfer-Encoding: binary" . "\n" ); if ( (int) $file['size'] > 0 ) header( "Content-Length: " . $file['size'] . "\n" ); echo $ics_export; exit; } // Unknown error, or just opening some other page } add_action( 'template_redirect', 'wpbm_make_export_ics_feeds' ); // --------------------------------------------------------------------------------------------------------------------- // Helpers for ICS export: // --------------------------------------------------------------------------------------------------------------------- /** * Escape ICS text per RFC 5545: backslash, comma, semicolon; newlines -> \n * * @param string $text * * @return string */ function wpbc_ics_escape_text( $text ) { $text = (string) $text; $text = str_replace( array( "\\", ";", "," ), array( "\\\\", "\;", "\," ), $text ); $text = str_replace( array( "\r\n", "\r" ), "\n", $text ); $text = str_replace( "\n", "\\n", $text ); // single backslash-n. return $text; } /** * Format SQL local datetime ("Y-m-d H:i:s") as UTC Z iCal (YYYYMMDDTHHMMSSZ). * * @param string|null $sql_dt Local SQL datetime or null for "now" * @param DateTimeZone $source_tz * * @return string */ function wpbc_to_ics_utc( $sql_dt, DateTimeZone $source_tz ) { $dt = $sql_dt ? DateTime::createFromFormat( 'Y-m-d H:i:s', $sql_dt, $source_tz ) : new DateTime( 'now', $source_tz ); if ( ! $dt ) { $dt = new DateTime( 'now', $source_tz ); } $dt->setTimezone( new DateTimeZone( 'UTC' ) ); return $dt->format( 'Ymd\THis\Z' ); } /** * Format SQL local datetime as local (no Z) in iCal basic (YYYYMMDDTHHMMSS). * * @param string $sql_dt * @param DateTimeZone $tz * * @return string */ function wpbc_to_ics_local( $sql_dt, DateTimeZone $tz ) { $dt = DateTime::createFromFormat( 'Y-m-d H:i:s', $sql_dt, $tz ); if ( ! $dt ) { $dt = new DateTime( $sql_dt ?: 'now', $tz ); } return $dt->format( 'Ymd\THis' ); } /** * Normalize seconds to :00 for clean slots (avoid :01/:02 artifacts). * * @param string $sql_dt * @param DateTimeZone $tz * * @return string "Y-m-d H:i:00" on success, original input if parsing fails */ function wpbc_normalize_sec_sql( $sql_dt, DateTimeZone $tz ) { $dt = DateTime::createFromFormat( 'Y-m-d H:i:s', $sql_dt, $tz ); if ( ! $dt ) { return $sql_dt; } return $dt->format( 'Y-m-d H:i:00' ); } /** * Add/subtract whole days to a Y-m-d in a specific tz (DST-safe), return Y-m-d. * * @param string $ymd "Y-m-d" * @param int $days can be negative * @param DateTimeZone $tz * * @return string "Y-m-d" */ function wpbc_add_days_ymd( $ymd, $days, DateTimeZone $tz ) { $dt = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $ymd . ' 00:00:00', $tz ); if ( ! $dt ) { $dt = new DateTimeImmutable( 'now', $tz ); } $modifier = ( $days >= 0 ? '+' : '' ) . $days . ' days'; return $dt->modify( $modifier )->format( 'Y-m-d' ); } /** * Ensure DTEND is strictly after DTSTART (RFC 5545). * * - All-day mode: treat inputs as Y-m-d; if end <= start → end = start + 1 day (exclusive end). * - Timed mode : normalize to 'Y-m-d H:i:s'; if end <= start → end = start + 1 hour. * * @param string $start_in 'Y-m-d' or 'Y-m-d H:i:s' * @param string $end_in 'Y-m-d' or 'Y-m-d H:i:s' (can be empty) * @param DateTimeZone $tz * @param bool $all_day * * @return array [ $start_out, $end_out ] (all-day → Y-m-d; timed → 'Y-m-d H:i:00') */ function wpbc_ics_coerce_order( $start_in, $end_in, DateTimeZone $tz, $all_day ) { // Helper for all-day date math. $_add_days = function( $ymd, $days ) use ( $tz ) { return wpbc_add_days_ymd( $ymd, $days, $tz ); }; if ( $all_day ) { $sd = substr( (string) $start_in, 0, 10 ); $ed = substr( (string) ($end_in !== '' ? $end_in : $sd), 0, 10 ); if ( $ed <= $sd ) { $ed = $_add_days( $sd, 1 ); } return array( $sd, $ed ); } // Timed mode: normalize to full SQL datetimes. $ss = ( strlen( (string) $start_in ) > 10 ) ? $start_in : ( substr( (string) $start_in, 0, 10 ) . ' 00:00:00' ); $ee = ( $end_in !== '' ) ? ( ( strlen( (string) $end_in ) > 10 ) ? $end_in : ( substr( (string) $end_in, 0, 10 ) . ' 00:00:00' ) ) : ''; $ds = DateTime::createFromFormat( 'Y-m-d H:i:s', $ss, $tz ); if ( ! $ds ) { $ds = new DateTime( $ss ?: 'now', $tz ); } if ( $ee === '' ) { $de = clone $ds; $de->modify( '+1 hour' ); } else { $de = DateTime::createFromFormat( 'Y-m-d H:i:s', $ee, $tz ); if ( ! $de ) { $de = new DateTime( $ee, $tz ); } } if ( $de <= $ds ) { $de = clone $ds; $de->modify( '+1 hour' ); } return array( $ds->format( 'Y-m-d H:i:00' ), $de->format( 'Y-m-d H:i:00' ) ); } /** * Add a VTIMEZONE node exactly once per TZID. * * @param ZCiCal $icalobj * @param string $tzid * @param string|int $year_start "YYYY" * @param string|int $year_end "YYYY" * @param array $added_ref reference array to track added TZIDs * * @return void */ /** * Add a VTIMEZONE node exactly once per TZID. * - For Europe/* zones: emit a compact RRULE-based CET/CEST block + X-LIC-LOCATION. * - For other zones: fall back to library helper (detailed transitions). */ function wpbc_add_vtimezone_once( $icalobj, $tzid, $year_start, $year_end, array &$added_ref ) { if ( empty( $tzid ) ) { return; } if ( isset( $added_ref[ $tzid ] ) ) { return; } // Lightweight CET/CEST definition for Europe/* (covers Europe/Prague and peers). if ( strpos( $tzid, 'Europe/' ) === 0 ) { $vtz = new ZCiCalNode( 'VTIMEZONE', $icalobj->curnode ); $vtz->addNode( new ZCiCalDataNode( 'TZID:' . $tzid ) ); $vtz->addNode( new ZCiCalDataNode( 'X-LIC-LOCATION:' . $tzid ) ); // STANDARD: last Sunday in October 03:00 -> CET (UTC+1). $std = new ZCiCalNode( 'STANDARD', $vtz ); $std->addNode( new ZCiCalDataNode( 'DTSTART:19961027T030000' ) ); $std->addNode( new ZCiCalDataNode( 'TZOFFSETFROM:+0200' ) ); $std->addNode( new ZCiCalDataNode( 'TZOFFSETTO:+0100' ) ); $std->addNode( new ZCiCalDataNode( 'TZNAME:CET' ) ); $std->addNode( new ZCiCalDataNode( 'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU' ) ); // DAYLIGHT: last Sunday in March 02:00 -> CEST (UTC+2). $dst = new ZCiCalNode( 'DAYLIGHT', $vtz ); $dst->addNode( new ZCiCalDataNode( 'DTSTART:19960331T020000' ) ); $dst->addNode( new ZCiCalDataNode( 'TZOFFSETFROM:+0100' ) ); $dst->addNode( new ZCiCalDataNode( 'TZOFFSETTO:+0200' ) ); $dst->addNode( new ZCiCalDataNode( 'TZNAME:CEST' ) ); $dst->addNode( new ZCiCalDataNode( 'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU' ) ); } else { // Fallback: detailed transitions for non‑Europe zones (safer across the globe). ZCTimeZoneHelper::getTZNode( (string) $year_start, (string) $year_end, $tzid, $icalobj->curnode ); } $added_ref[ $tzid ] = true; } // --------------------------------------------------------------------------------------------------------------------- // Export ICS here. // --------------------------------------------------------------------------------------------------------------------- // FixIn: 2.1.20.1 /** * Build the shortened booking-date representation used by the iCalendar exporter. * * This rebuild is required after booking dates have been limited to the * resources authorized for an iCalendar feed. It follows the same * range-compression rules as the Booking Calendar booking-list API. * * @param array $booking_dates Booking-date objects for one booking. * @param int $main_resource_id Resource ID stored on the main booking row. * * @return array { * Shortened dates and their effective booking-resource IDs. * * @type array $dates_short Date values and range separators. * @type array $dates_short_id Effective resource IDs matching the date values. * } */ function wpbc_ics_export_build_short_dates( $booking_dates, $main_resource_id ) { $dates_short = array(); $dates_short_id = array(); $last_date = ''; $last_date_id = ''; $last_token = ''; foreach ( $booking_dates as $booking_date ) { $current_date = $booking_date->booking_date; $current_date_resource_id = ! empty( $booking_date->type_id ) ? intval( $booking_date->type_id ) : intval( $main_resource_id ); if ( '' === $last_date ) { $dates_short[] = $current_date; $dates_short_id[] = $current_date_resource_id; $last_token = $current_date; } elseif ( wpbc_is_less_than_next_day( $current_date, $last_date, true ) ) { if ( '-' !== $last_token ) { $dates_short[] = '-'; $dates_short_id[] = ''; } $last_token = '-'; } else { if ( $last_token !== $last_date ) { $dates_short[] = $last_date; $dates_short_id[] = $last_date_id; } $dates_short[] = ','; $dates_short_id[] = ''; $dates_short[] = $current_date; $dates_short_id[] = $current_date_resource_id; $last_token = $current_date; } $last_date = $current_date; $last_date_id = $current_date_resource_id; } if ( ( '' !== $last_date ) && ( $last_token !== $last_date ) ) { $dates_short[] = $last_date; $dates_short_id[] = $last_date_id; } return array( 'dates_short' => $dates_short, 'dates_short_id' => $dates_short_id, ); } /** * Get one resource ID from the iCalendar export query. * * The scope expansion must only run for a feed URL that requested one resource. * Existing internal callers may pass a comma-separated resource list, which is * left untouched and must not be reinterpreted as one parent-resource feed. * * @param mixed $resource_query Resource selector received by the exporter. * * @return int Positive resource ID, or zero when the query is not one ID. */ function wpbc_ics_export_get_requested_resource_id( $resource_query ) { if ( ! is_scalar( $resource_query ) ) { return 0; } $resource_query = trim( (string) $resource_query ); if ( ( '' === $resource_query ) || ( 1 !== preg_match( '/^[0-9]+$/', $resource_query ) ) ) { return 0; } return absint( $resource_query ); } /** * Resolve the resource IDs authorized for one iCalendar feed. * * Parent feeds always include the requested parent. When the opt-in setting is * enabled, direct capacity children owned by the same MultiUser account are * added. Descendants are not traversed because Booking Calendar capacity uses * a direct parent-child relationship. * * @param int $resource_id Resource requested by the iCalendar feed URL. * @param bool $include_children Whether direct child resources may be included. * * @return array { * Resolved export scope. * * @type bool $is_parent Whether the requested resource has direct children. * @type array $resource_ids Requested resource ID and any authorized child IDs. * } */ function wpbc_ics_export_get_resource_scope( $resource_id, $include_children = false ) { $resource_id = absint( $resource_id ); $scope = array( 'is_parent' => false, 'resource_ids' => ( $resource_id > 0 ) ? array( $resource_id ) : array(), ); if ( ( $resource_id <= 0 ) || ! class_exists( 'wpdev_bk_biz_l' ) || ! function_exists( 'wpbc_br_cache' ) ) { return $scope; } $resource_cache = wpbc_br_cache(); if ( ! is_object( $resource_cache ) || ! method_exists( $resource_cache, 'get_resources' ) ) { return $scope; } $resource_list = $resource_cache->get_resources(); if ( ! is_array( $resource_list ) ) { return $scope; } $parent_owner_id = null; foreach ( $resource_list as $resource_key => $resource_params ) { if ( ! is_array( $resource_params ) ) { continue; } $current_resource_id = isset( $resource_params['id'] ) ? absint( $resource_params['id'] ) : absint( $resource_key ); if ( $resource_id === $current_resource_id ) { $parent_owner_id = isset( $resource_params['users'] ) ? absint( $resource_params['users'] ) : null; break; } } foreach ( $resource_list as $resource_key => $resource_params ) { if ( ! is_array( $resource_params ) ) { continue; } $parent_resource_id = isset( $resource_params['parent'] ) ? absint( $resource_params['parent'] ) : 0; if ( $resource_id !== $parent_resource_id ) { continue; } $scope['is_parent'] = true; if ( ! $include_children ) { continue; } $child_owner_id = isset( $resource_params['users'] ) ? absint( $resource_params['users'] ) : null; if ( ( null !== $child_owner_id ) && ( null === $parent_owner_id || $parent_owner_id !== $child_owner_id ) ) { continue; } $child_resource_id = isset( $resource_params['id'] ) ? absint( $resource_params['id'] ) : absint( $resource_key ); if ( $child_resource_id > 0 ) { $scope['resource_ids'][] = $child_resource_id; } } $scope['resource_ids'] = array_values( array_unique( array_map( 'absint', $scope['resource_ids'] ) ) ); return $scope; } /** * Limit bookings and booking dates to an iCalendar resource scope. * * A NULL booking-date type ID represents the resource stored in the main * booking row. An explicit type ID represents another capacity resource used * by that booking. Identical timestamps inside one booking are collapsed so a * booking occupying multiple capacity units still exports as one event; dates * from different booking IDs are never merged. * * @param array $bookings_arr Booking-list API result. * @param array $resource_ids Resource IDs allowed in the feed. * * @return array Filtered booking-list API result. */ function wpbc_ics_export_filter_bookings_by_resource_scope( $bookings_arr, $resource_ids ) { $resource_ids = array_values( array_filter( array_unique( array_map( 'absint', (array) $resource_ids ) ) ) ); if ( empty( $resource_ids ) || empty( $bookings_arr['bookings'] ) || ! is_array( $bookings_arr['bookings'] ) ) { return $bookings_arr; } $allowed_resource_ids = array_fill_keys( $resource_ids, true ); foreach ( $bookings_arr['bookings'] as $booking_id => $booking_obj ) { $main_resource_id = isset( $booking_obj->booking_type ) ? absint( $booking_obj->booking_type ) : 0; $scoped_dates = array(); if ( empty( $booking_obj->dates ) || ! is_array( $booking_obj->dates ) ) { unset( $bookings_arr['bookings'][ $booking_id ] ); continue; } foreach ( $booking_obj->dates as $booking_date ) { $date_resource_id = ! empty( $booking_date->type_id ) ? absint( $booking_date->type_id ) : $main_resource_id; if ( ! isset( $allowed_resource_ids[ $date_resource_id ] ) || ! isset( $booking_date->booking_date ) ) { continue; } $scoped_dates[ (string) $booking_date->booking_date ] = $booking_date; } if ( empty( $scoped_dates ) ) { unset( $bookings_arr['bookings'][ $booking_id ] ); continue; } $scoped_dates = array_values( $scoped_dates ); usort( $scoped_dates, static function ( $first_date, $second_date ) { return strcmp( $first_date->booking_date, $second_date->booking_date ); } ); $short_dates = wpbc_ics_export_build_short_dates( $scoped_dates, $main_resource_id ); $booking_obj->dates = $scoped_dates; $booking_obj->dates_short = $short_dates['dates_short']; $booking_obj->dates_short_id = $short_dates['dates_short_id']; } $bookings_arr['bookings_count'] = count( $bookings_arr['bookings'] ); return $bookings_arr; } /** * Limit a parent-resource iCalendar feed to dates occupying the parent itself. * * This public compatibility wrapper preserves the behavior introduced in * Booking Manager 2.1.20 for callers that use the original helper directly. * The exporter uses the generalized scope helper when child aggregation is * enabled. * * @param array $bookings_arr Booking-list API result. * @param int $resource_id Resource requested by the iCalendar feed URL. * * @return array Filtered booking-list API result. */ function wpbc_ics_export_filter_parent_resource_bookings( $bookings_arr, $resource_id ) { $resource_scope = wpbc_ics_export_get_resource_scope( $resource_id, false ); if ( ! $resource_scope['is_parent'] ) { return $bookings_arr; } return wpbc_ics_export_filter_bookings_by_resource_scope( $bookings_arr, $resource_scope['resource_ids'] ); } /** * Define export ICS here. * For testing: http://beta/?feed=wpbm-ics * * @param array $param - [ 'wh_booking_type' => '1', 'wh_trash' => '' ]. * * @return string */ function wpbm_export_ics_feed__wpbm_ics( $param = array( 'wh_booking_type' => '1', 'wh_trash' => '' ) ) { // FixIn: 2.1.13.1 – timezone-safe, UTC stamps, OTA all‑day, single VTIMEZONE, no closures. if ( ! function_exists( 'wpbc_api_get_bookings_arr' ) ) { return ''; } // Resolve export timezone: option 'booking_gcal_timezone' if set, else WP site tz. $site_tz_obj = wp_timezone(); $tzid_opt_raw = (string) get_bk_option( 'booking_gcal_timezone' ); $tzid_opt = trim( $tzid_opt_raw ); // "" when “Default” is selected // Validate TZID (must be a known IANA zone). $tzid = ''; if ( $tzid_opt !== '' ) { try { // Will throw on invalid IDs (e.g., typos). $check = new DateTimeZone( $tzid_opt ); $tzid = $tzid_opt; // valid — we’ll emit TZID and VTIMEZONE. } catch ( Exception $e ) { $tzid = ''; // invalid — treat as Default (no TZID emitted). } } // Export timezone object (priority: booking_gcal_timezone > site tz). $export_tz = ( $tzid !== '' ) ? new DateTimeZone( $tzid ) : $site_tz_obj; // End date of getting bookings (cutoff +2 years) – done in export tz. $cutoff = ( new DateTimeImmutable( 'now', $export_tz ) )->modify( '+2 years' )->format( 'Y-m-d' ); $param['wh_booking_date2'] = $cutoff; // Export only approved bookings (FixIn: 2.0.11.1 / 8.5.2.3). $booking_is_ics_export_only_approved = get_bk_option( 'booking_is_ics_export_only_approved' ); $param['wh_approved'] = ( 'On' === $booking_is_ics_export_only_approved ) ? '1' : ''; // '' | 'imported' | 'plugin' (FixIn: 8.8.3.19 / 2.0.20.2). if ( 'imported' === get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) { $param['wh_sync_gid'] = 'imported'; } if ( 'plugin' === get_bk_option( 'booking_is_ics_export_imported_bookings' ) ) { $param['wh_sync_gid'] = 'plugin'; } $_REQUEST['tab'] = 'vm_calendar'; // FixIn: 10.12.3.2 / 8.5.2.15 / 2.0.11.2. // Get array of bookings. $export_resource_id = isset( $param['wh_booking_type'] ) ? wpbc_ics_export_get_requested_resource_id( $param['wh_booking_type'] ) : 0; $include_child_resources = ( 'On' === get_wpbm_option( 'wpbm_is_export_parent_with_children', 'Off' ) ); $resource_scope = wpbc_ics_export_get_resource_scope( $export_resource_id, $include_child_resources ); if ( $resource_scope['is_parent'] && ! empty( $resource_scope['resource_ids'] ) ) { $param['wh_booking_type'] = implode( ',', array_map( 'absint', $resource_scope['resource_ids'] ) ); } $bookings_arr = wpbc_api_get_bookings_arr( $param ); if ( $resource_scope['is_parent'] ) { $bookings_arr = wpbc_ics_export_filter_bookings_by_resource_scope( $bookings_arr, $resource_scope['resource_ids'] ); } ob_start(); // Create the iCal object. $icalobj = new ZCiCal(); // Calendar headers (library adds VERSION/PRODID; we add refresh hints). $datanode = new ZCiCalDataNode( 'REFRESH-INTERVAL;VALUE=DURATION:PT10M' ); // 10 minutes. $icalobj->curnode->data[ $datanode->getName() ] = $datanode; $datanode = new ZCiCalDataNode( 'X-PUBLISHED-TTL:PT10M' ); // 10 minutes. $icalobj->curnode->data[ $datanode->getName() ] = $datanode; $all_day_export = ( 'On' === get_wpbm_option( 'wpbm_is_export_only_full_days' ) ); // OTA toggle. // Host for UIDs (global uniqueness). $host = wp_parse_url( home_url(), PHP_URL_HOST ); if ( empty( $host ) ) { $host = 'example.org'; } if ( 'localhost' === $host ) { $host = 'localhost.localdomain'; } // Track VTIMEZONE additions per TZID. $__wpbc_added_vtimezones = array(); foreach ( $bookings_arr['bookings'] as $bk_id => $bk_arr ) { // Create the master event within the iCal object. $eventobj = new ZCiCalNode( 'VEVENT', $icalobj->curnode ); // --------------------------------------------------------------------------------------------- // SUMMARY // --------------------------------------------------------------------------------------------- if ( function_exists( 'get_title_for_showing_in_day' ) ) { $what_show_in_day_template = get_bk_option( 'booking_default_title_in_day_for_calendar_view_mode' ); $title = get_title_for_showing_in_day( $bk_id, $bookings_arr['bookings'], $what_show_in_day_template ); } else { $title = ( isset( $bk_arr->form_data['name'] ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' ); $title .= ( isset( $bk_arr->form_data['firstname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['firstname'] ) : '' ); $title .= ( isset( $bk_arr->form_data['secondname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' ); $title .= ( isset( $bk_arr->form_data['lastname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['lastname'] ) : '' ); } $is_hide_details = get_wpbm_option( 'wpbm_is_hide_details' ); if ( 'On' === $is_hide_details ) { $title = ''; } $title = ltrim( (string) $title ); // optional: avoid leading space. $title_ics = wpbc_ics_escape_text( wpbm_esc_to_plain_text( $title ) ); $eventobj->addNode( new ZCiCalDataNode( 'SUMMARY:' . $title_ics ) ); // --------------------------------------------------------------------------------------------- // DESCRIPTION // --------------------------------------------------------------------------------------------- $description_raw = ( 'On' === $is_hide_details ) ? '' : $bk_arr->form_show; $description_raw = ltrim( (string) $description_raw ); // remove leading whitespace/newlines. $description_ics = wpbc_ics_escape_text( wpbm_esc_to_plain_text( $description_raw ) ); $description_ics = preg_replace('/\s+/', ' ', $description_ics); $eventobj->addNode( new ZCiCalDataNode( 'DESCRIPTION:' . $description_ics ) ); // --------------------------------------------------------------------------------------------- // START & END (master span from dates_short) // --------------------------------------------------------------------------------------------- $event_start = $bk_arr->dates_short[0]; if ( ( count( $bk_arr->dates_short ) > 1 ) && ( '-' === $bk_arr->dates_short[1] ) ) { $event_end = $bk_arr->dates_short[2]; $event_end = wpbm_get_next_date_if_it_full_date( $event_end ); // make end exclusive for YYYY-mm-dd $period_start_index = 3; } else { // comma or single day. $event_end = $bk_arr->dates_short[0]; $event_end = wpbm_get_next_date_if_it_full_date( $event_end ); $period_start_index = 1; } if ( $all_day_export ) { // OTA all-day mode (end exclusive). $event_start = substr( $event_start, 0, 10 ); // Y-m-d $event_end = substr( $event_end, 0, 10 ); // Y-m-d if ( $event_start === $event_end ) { $event_end = wpbc_add_days_ymd( $event_end, 1, $export_tz ); } } else { // Timed mode: normalize seconds to :00 for clean slots. if ( strlen( $event_start ) > 10 ) { $event_start = wpbc_normalize_sec_sql( $event_start, $export_tz ); } if ( strlen( $event_end ) > 10 ) { $event_end = wpbc_normalize_sec_sql( $event_end, $export_tz ); } } // Add VTIMEZONE once (timed mode with TZID). if ( ! $all_day_export && ! empty( $tzid ) ) { wpbc_add_vtimezone_once( $icalobj, $tzid, substr( $event_start, 0, 4 ), substr( $event_end, 0, 4 ), $__wpbc_added_vtimezones ); } // DTSTART / DTEND for the master VEVENT (coerced so DTEND > DTSTART). if ( $all_day_export ) { list( $event_start, $event_end ) = wpbc_ics_coerce_order( $event_start, $event_end, $export_tz, true ); $eventobj->addNode( new ZCiCalDataNode( 'DTSTART;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $event_start ) ) ); $eventobj->addNode( new ZCiCalDataNode( 'DTEND;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $event_end ) ) ); } else { list( $event_start_fixed, $event_end_fixed ) = wpbc_ics_coerce_order( $event_start, $event_end, $export_tz, false ); if ( ! empty( $tzid ) ) { $eventobj->addNode( new ZCiCalDataNode( 'DTSTART;TZID=' . $tzid . ':' . wpbc_to_ics_local( $event_start_fixed, $export_tz ) ) ); $eventobj->addNode( new ZCiCalDataNode( 'DTEND;TZID=' . $tzid . ':' . wpbc_to_ics_local( $event_end_fixed, $export_tz ) ) ); } else { $eventobj->addNode( new ZCiCalDataNode( 'DTSTART:' . wpbc_to_ics_utc( $event_start_fixed, $export_tz ) ) ); $eventobj->addNode( new ZCiCalDataNode( 'DTEND:' . wpbc_to_ics_utc( $event_end_fixed, $export_tz ) ) ); } } // --------------------------------------------------------------------------------------------- // STATUS / TRANSP // --------------------------------------------------------------------------------------------- $ics_status = 'TENTATIVE'; if ( ! empty( $bk_arr->trash ) ) { $ics_status = 'CANCELLED'; } elseif ( ! empty( $bk_arr->dates[0]->approved ) ) { $ics_status = 'CONFIRMED'; } $eventobj->addNode( new ZCiCalDataNode( 'STATUS:' . $ics_status ) ); $eventobj->addNode( new ZCiCalDataNode( 'TRANSP:OPAQUE' ) ); // --------------------------------------------------------------------------------------------- // Expand extra dates/ranges into standalone VEVENTs (better than RDATE/RRULE for OTA/clients) // --------------------------------------------------------------------------------------------- $segments = array(); // each: ['start_sql' => 'Y-m-d[ H:i:s]', 'end_sql' => 'Y-m-d[ H:i:s]'|null] $prev = ''; $current = null; foreach ( $bk_arr->dates_short as $i => $tok ) { if ( $i < $period_start_index ) { continue; } if ( ',' !== $tok && '-' !== $tok ) { if ( '-' === $prev ) { $tok_norm = wpbm_get_next_date_if_it_full_date( $tok ); // range end (exclusive if Y-m-d) } else { $tok_norm = wpbm_get_only_date_if_it_full_date( $tok ); // start or single } if ( '-' === $prev ) { if ( $current ) { $current['end_sql'] = $tok_norm; } } else { $current = array( 'start_sql' => $tok_norm, 'end_sql' => null ); } } elseif ( ',' === $tok ) { if ( $current ) { $segments[] = $current; $current = null; } } $prev = $tok; } if ( $current ) { $segments[] = $current; } if ( ! empty( $segments ) ) { // Duration basis for timed events. $master_duration = 0; if ( ! $all_day_export ) { $basis_start = isset( $event_start_fixed ) ? $event_start_fixed : $event_start; $basis_end = isset( $event_end_fixed ) ? $event_end_fixed : $event_end; $dts = DateTime::createFromFormat( 'Y-m-d H:i:s', $basis_start, $export_tz ); $dte = DateTime::createFromFormat( 'Y-m-d H:i:s', $basis_end, $export_tz ); $master_duration = ( $dts && $dte && $dte > $dts ) ? ( $dte->getTimestamp() - $dts->getTimestamp() ) : 3600; } $seg_idx = 0; foreach ( $segments as $s ) { ++ $seg_idx; $ev = new ZCiCalNode( 'VEVENT', $icalobj->curnode ); if ( $all_day_export ) { $start_d = substr( $s['start_sql'], 0, 10 ); $end_d = ! empty( $s['end_sql'] ) ? substr( $s['end_sql'], 0, 10 ) : $start_d; list( $start_d, $end_d ) = wpbc_ics_coerce_order( $start_d, $end_d, $export_tz, true ); $ev->addNode( new ZCiCalDataNode( 'DTSTART;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $start_d ) ) ); $ev->addNode( new ZCiCalDataNode( 'DTEND;VALUE=DATE:' . preg_replace( '/[^0-9]/', '', $end_d ) ) ); } else { $start_sql = ( strlen( $s['start_sql'] ) > 10 ) ? wpbc_normalize_sec_sql( $s['start_sql'], $export_tz ) : $s['start_sql']; if ( ! empty( $s['end_sql'] ) ) { $end_sql = ( strlen( $s['end_sql'] ) > 10 ) ? wpbc_normalize_sec_sql( $s['end_sql'], $export_tz ) : $s['end_sql']; } else { $dt = DateTime::createFromFormat( 'Y-m-d H:i:s', $start_sql, $export_tz ); if ( $dt ) { $dt->modify( '+' . $master_duration . ' seconds' ); $end_sql = $dt->format( 'Y-m-d H:i:s' ); } else { $end_sql = $start_sql; } } // Ensure DTEND > DTSTART for each segment. list( $start_sql, $end_sql ) = wpbc_ics_coerce_order( $start_sql, $end_sql, $export_tz, false ); if ( ! empty( $tzid ) ) { $ev->addNode( new ZCiCalDataNode( 'DTSTART;TZID=' . $tzid . ':' . wpbc_to_ics_local( $start_sql, $export_tz ) ) ); $ev->addNode( new ZCiCalDataNode( 'DTEND;TZID=' . $tzid . ':' . wpbc_to_ics_local( $end_sql, $export_tz ) ) ); } else { $ev->addNode( new ZCiCalDataNode( 'DTSTART:' . wpbc_to_ics_utc( $start_sql, $export_tz ) ) ); $ev->addNode( new ZCiCalDataNode( 'DTEND:' . wpbc_to_ics_utc( $end_sql, $export_tz ) ) ); } } // Copy common fields. $ev->addNode( new ZCiCalDataNode( 'SUMMARY:' . $title_ics ) ); $ev->addNode( new ZCiCalDataNode( 'DESCRIPTION:' . $description_ics ) ); $ev->addNode( new ZCiCalDataNode( 'STATUS:' . $ics_status ) ); $ev->addNode( new ZCiCalDataNode( 'TRANSP:OPAQUE' ) ); // Unique UID per extra VEVENT. $uid_extra = $bk_id . '-' . $seg_idx . '@' . $host; $ev->addNode( new ZCiCalDataNode( 'UID:' . $uid_extra ) ); // Timestamps in UTC Z. $ev->addNode( new ZCiCalDataNode( 'DTSTAMP:' . wpbc_to_ics_utc( null, $export_tz ) ) ); $ev->addNode( new ZCiCalDataNode( 'CREATED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) ); $ev->addNode( new ZCiCalDataNode( 'LAST-MODIFIED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) ); } } // --------------------------------------------------------------------------------------------- // UID (master) // --------------------------------------------------------------------------------------------- $uid = $bk_id . '@' . $host; // stable UID not tied to start time. $eventobj->addNode( new ZCiCalDataNode( 'UID:' . $uid ) ); // DTSTAMP / CREATED / LAST-MODIFIED in UTC Z. $eventobj->addNode( new ZCiCalDataNode( 'DTSTAMP:' . wpbc_to_ics_utc( null, $export_tz ) ) ); $eventobj->addNode( new ZCiCalDataNode( 'CREATED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) ); $eventobj->addNode( new ZCiCalDataNode( 'LAST-MODIFIED:' . wpbc_to_ics_utc( $bk_arr->modification_date, $export_tz ) ) ); // --------------------------------------------------------------------------------------------- // ATTENDEE (optional) // --------------------------------------------------------------------------------------------- if ( isset( $bk_arr->form_data['email'] ) && ( 'On' !== $is_hide_details ) ) { $cn_title = ( isset( $bk_arr->form_data['name'] ) ? ' ' . esc_textarea( $bk_arr->form_data['name'] ) : '' ); $cn_title .= ( isset( $bk_arr->form_data['firstname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['firstname'] ) : '' ); $cn_title .= ( isset( $bk_arr->form_data['secondname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['secondname'] ) : '' ); $cn_title .= ( isset( $bk_arr->form_data['lastname'] ) ? ' ' . esc_textarea( $bk_arr->form_data['lastname'] ) : '' ); $cn_clean = trim( wpbm_esc_to_plain_text( $cn_title ) ); $cn_clean = addcslashes( $cn_clean, "\\,;\"" ); // escape per RFC $attendee_email = $bk_arr->form_data['email']; $eventobj->addNode( new ZCiCalDataNode( 'ATTENDEE;CN="' . $cn_clean . '":MAILTO:' . $attendee_email ) ); } } // Output iCalendar text. // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped echo $icalobj->export(); $ics_export = ob_get_contents(); $ics_export = trim( $ics_export ); ob_end_clean(); return $ics_export; } /** Get Next day in MySQL format without time, if current day its Full day - ending with 00:00:00 * * @param string $mysql_date - 2017-07-12 00:00:00 * @return string - 2017-07-13 */ function wpbm_get_next_date_if_it_full_date( $mysql_date ) { // Check if this date ending with 00:00:00 its means that it full day, // 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 if ( ( strlen( $mysql_date ) > 10 ) // , like 2017-07-12 00:00:00 && ( substr( $mysql_date, 11 ) == '00:00:00' ) ) { $mysql_date = date_i18n( "Y-m-d 00:00:00", strtotime( $mysql_date ) ); $mysql_date = strtotime( $mysql_date ); $mysql_date = date_i18n( "Y-m-d", strtotime( '+1 day', $mysql_date ) ); } return $mysql_date; } /** Get day in MySQL format without time, if current day its Full day - ending with 00:00:00 * * @param string $mysql_date - 2017-07-12 00:00:00 * @return string - 2017-07-12 */ function wpbm_get_only_date_if_it_full_date( $mysql_date ) { if ( ( strlen( $mysql_date ) > 10 ) // , like 2017-07-12 00:00:00 && ( substr( $mysql_date, 11) == '00:00:00' ) ) { $mysql_date = substr( $mysql_date, 0, 10 ); } return $mysql_date; }