PluginProbe
Booking Manager – Sync WP Booking Calendar – Import Events, Export Bookings to ICS Calendar / 2.1.22
Booking Manager – Sync WP Booking Calendar – Import Events, Export Bookings to ICS Calendar v2.1.22
2.1.22 2.1.21 2.1.20 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 trunk 1.1 2.0 2.0.1 2.0.10.2 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.17 2.0.18 2.0.2 2.0.20 2.0.21 All 56 releases
booking-manager / core / wpbc / wpbm-bc-export.php

wpbm-bc-export.php in Booking Manager – Sync WP Booking Calendar – Import Events, Export Bookings to ICS Calendar 2.1.22, at core/wpbc/wpbm-bc-export.php

1,076 lines 60.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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