PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 All 65 releases
← All changes | utils/global-helper.php +423 -16 1.0.17 → 1.0.64 View file →
@@ -1,5 +1,7 @@
1 1 <?php
2 +defined( 'ABSPATH' ) || exit;
3 +
2 4 use function Timetics\timetics;
3 5 use Timetics\Core\Bookings\Booking;
4 6 use Timetics\Core\Integrations\Google\Client;
5 7 use Timetics\Core\Integrations\Google\Service\Calendar;
@@ -47,26 +49,47 @@
47 49 */
48 50 function timetics_get_google_access_token( $user_id = 0 ) {
49 51 $data = timetics_get_google_auth( $user_id );
50 52
51 - if ( ! $data ) {
53 + if ( empty( $data ) || empty( $data['access_token'] ) ) {
52 54 return false;
53 55 }
54 56
55 - if ( ( $data['expires_in'] - 30 ) < time() ) {
56 - $refresh_toekn = timetics_get_google_refresh_token( $user_id );
57 - $client = timetics_get_google_client();
57 + $expires_in = isset( $data['expires_in'] ) ? (int) $data['expires_in'] : 0;
58 58
59 - $data = $client->fetch_access_token_with_refresh_token( $refresh_toekn );
59 + // Token still valid (30s safety margin) — use it as-is.
60 + if ( ( $expires_in - 30 ) > time() ) {
61 + return $data['access_token'];
62 + }
60 63
61 - if ( ! $data ) {
64 + // Needs a refresh.
65 + $refresh_token = timetics_get_google_refresh_token( $user_id );
66 +
67 + if ( empty( $refresh_token ) ) {
68 + return false;
69 + }
70 +
71 + $client = timetics_get_google_client();
72 + $response = $client->fetch_access_token_with_refresh_token( $refresh_token );
73 +
74 + if ( is_wp_error( $response ) ) {
75 + $error_data = $response->get_error_data();
76 + $transient = ! is_array( $error_data ) || ! empty( $error_data['transient'] );
77 +
78 + if ( $transient ) {
62 79 return false;
63 80 }
64 81
65 - timetics_update_google_auth( $user_id, $data );
82 + // Permanent failure (invalid_grant): the refresh token is dead and the account genuinely needs to be re-connected. Flag it so the UI can reflect the real state.
83 + update_user_meta( $user_id, 'timetics_google_auth_error', $response->get_error_code() );
84 +
85 + return false;
66 86 }
67 87
68 - return $data['access_token'];
88 + // Persisting a fresh credential also clears any stale auth-error flag
89 + timetics_update_google_auth( $user_id, $response );
90 +
91 + return $response['access_token'];
69 92 }
70 93 }
71 94
72 95 if ( ! function_exists( 'timetics_get_google_refresh_token' ) ) {
@@ -105,8 +128,12 @@
105 128 *
106 129 * @return void
107 130 */
108 131 function timetics_update_google_auth( $user_id = 0, $data = [] ) {
132 + if ( empty( $data ) || ! is_array( $data ) ) {
133 + return;
134 + }
135 +
109 136 if ( ! empty( $data['code'] ) ) {
110 137 update_user_meta( $user_id, 'timetics_google_auth_code', $data['code'] );
111 138 }
112 139
@@ -113,11 +140,17 @@
113 140 if ( ! empty( $data['refresh_token'] ) ) {
114 141 update_user_meta( $user_id, 'timetics_google_refresh_token', $data['refresh_token'] );
115 142 }
116 143
117 - $data['expires_in'] = $data['expires_in'] + time();
144 + // Google returns expires_in as a lifetime in seconds (~3600).
145 + // Guard against a missing/zero value: storing time() alone would make the
146 + // token look permanently expired, forcing a refresh on every request and
147 + // hammering Google's token endpoint until it rate-limits the app.
148 + $lifetime = ! empty( $data['expires_in'] ) ? (int) $data['expires_in'] : 3600;
149 + $data['expires_in'] = time() + $lifetime;
118 150
119 151 update_user_meta( $user_id, 'timetics_google_auth', $data );
152 + delete_user_meta( $user_id, 'timetics_google_auth_error' );
120 153 }
121 154 }
122 155
123 156 if ( ! function_exists( 'timetics_get_google_auth' ) ) {
@@ -166,11 +199,11 @@
166 199 *
167 200 * @return string
168 201 */
169 202 function timetics_get_google_auth_url() {
170 - $client = timetics_get_google_client();
203 + $local_url = site_url( 'timetics-integration/google-auth' );
171 204
172 - return $client->get_auth_url();
205 + return wp_nonce_url( $local_url, 'timetics_auth_action', 'timetics_auth_nonce' );
173 206 }
174 207 }
175 208
176 209 if ( ! function_exists( 'timetics_get_posts' ) ) {
@@ -312,8 +345,70 @@
312 345 return $total;
313 346 }
314 347 }
315 348
349 +if ( ! function_exists( 'timetics_can_view_all_data' ) ) {
350 + /**
351 + * Check the current user is allowed to see site wide data
352 + *
353 + * Staff hold `manage_timetics`, so that capability cannot be used to tell an
354 + * administrator apart from a team member.
355 + *
356 + * @return bool
357 + */
358 + function timetics_can_view_all_data() {
359 + return current_user_can( 'manage_options' );
360 + }
361 +}
362 +
363 +if ( ! function_exists( 'timetics_get_visible_booking_ids' ) ) {
364 + /**
365 + * Get booking ids a user is allowed to see
366 + *
367 + * @param integer $user_id
368 + *
369 + * @return array Never empty, so it is always safe to pass to `post__in`.
370 + */
371 + function timetics_get_visible_booking_ids( $user_id = 0 ) {
372 + if ( ! $user_id ) {
373 + $user_id = get_current_user_id();
374 + }
375 +
376 + $booking_ids = Booking::get_visible_ids_for_user( $user_id );
377 +
378 + // WP_Query ignores an empty post__in and returns everything, so fall back to a
379 + // non-existent id to mean "nothing visible".
380 + return ! empty( $booking_ids ) ? $booking_ids : [0];
381 + }
382 +}
383 +
384 +if ( ! function_exists( 'timetics_get_visible_customer_ids' ) ) {
385 + /**
386 + * Get customer user ids a user is allowed to see, derived from their visible bookings
387 + *
388 + * @param integer $user_id
389 + *
390 + * @return array Never empty, so it is always safe to pass to `include`.
391 + */
392 + function timetics_get_visible_customer_ids( $user_id = 0 ) {
393 + $booking_ids = timetics_get_visible_booking_ids( $user_id );
394 + $customer_ids = [];
395 +
396 + foreach ( $booking_ids as $booking_id ) {
397 + $customer_id = (int) get_post_meta( $booking_id, '_tt_booking_customer', true );
398 +
399 + if ( $customer_id ) {
400 + $customer_ids[] = $customer_id;
401 + }
402 + }
403 +
404 + $customer_ids = array_values( array_unique( $customer_ids ) );
405 +
406 + // WP_User_Query ignores an empty include and returns everything.
407 + return ! empty( $customer_ids ) ? $customer_ids : [0];
408 + }
409 +}
410 +
316 411 if ( ! function_exists( 'timetics_get_staff_integrations' ) ) {
317 412 /**
318 413 * Get all staff integrations
319 414 *
@@ -319,9 +414,15 @@
319 414 *
320 415 * @return array
321 416 */
322 417 function timetics_get_staff_integrations( $user_id = 0 ) {
323 - $google_auth = timetics_get_google_access_token( $user_id );
418 + timetics_get_google_access_token( $user_id );
419 +
420 + $refresh_token = timetics_get_google_refresh_token( $user_id );
421 + $auth_error = get_user_meta( $user_id, 'timetics_google_auth_error', true );
422 + $is_connected = ! empty( $refresh_token ) && empty( $auth_error );
423 + $needs_reauth = ! empty( $refresh_token ) && ! empty( $auth_error );
424 +
324 425 $google_credentials = timetics_get_google_credentials();
325 426 $is_setup = ! empty( $google_credentials['client_id'] ) && ! empty( $google_credentials['client_secret'] );
326 427 $current_user_id = ( $user_id == get_current_user_id() ) ? true : false;
327 428
@@ -330,9 +431,10 @@
330 431 'id' => 'google-calendar-meet',
331 432 'name' => esc_html__( 'Google Meet', 'timetics' ),
332 433 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
333 434 'auth_url' => timetics_get_google_auth_url(),
334 - 'connected' => ! empty( $google_auth ),
435 + 'connected' => $is_connected,
436 + 'needs_reauth' => $needs_reauth,
335 437 'setup' => $is_setup,
336 438 'current_user' => $current_user_id,
337 439 ],
338 440 [
@@ -339,9 +441,10 @@
339 441 'id' => 'google-calendar',
340 442 'name' => esc_html__( 'Google Calendar', 'timetics' ),
341 443 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
342 444 'auth_url' => timetics_get_google_auth_url(),
343 - 'connected' => ! empty( $google_auth ),
445 + 'connected' => $is_connected,
446 + 'needs_reauth' => $needs_reauth,
344 447 'setup' => $is_setup,
345 448 'current_user' => $current_user_id,
346 449 ],
347 450 ];
@@ -403,12 +506,14 @@
403 506 */
404 507 function timetics_update_default_settings() {
405 508 $settings = [
406 509 'google_auth_redirect_uri' => timetics_get_auth_redirect_uri( 'google-auth' ),
510 + 'outlook_auth_redirect_uri'=> timetics_get_auth_redirect_uri( 'outlook-auth' ),
407 511 'default_booking_status' => 'pending',
408 512 'currency' => 'USD',
409 513 'primary_color' => '#3161F1',
410 514 'secondary_color' => '#6188ff',
515 + 'unpaid_booking_expiry_minutes' => 5,
411 516 ];
412 517
413 518 $settings = apply_filters( 'timetics_default_settings', $settings );
414 519
@@ -510,8 +615,14 @@
510 615 * @return void
511 616 */
512 617 function timetics_register_cron() {
513 618 wp_schedule_event( time(), 'daily', 'timetics_booking_clear_schedule' );
619 +
620 + // Guarded: Hooks::maybe_schedule_cleanup_cron() also calls this on
621 + // every 'init', so without the guard it'd stack duplicate events.
622 + if ( ! wp_next_scheduled( 'timetics_cleanup_unpaid_bookings' ) ) {
623 + wp_schedule_event( time(), 'timetics_five_minutes', 'timetics_cleanup_unpaid_bookings' );
624 + }
514 625 }
515 626 }
516 627
517 628 if ( ! function_exists( 'timetics_is_woocommerce_active' ) ) {
@@ -719,11 +830,307 @@
719 830 $user_data = [
720 831 'id' => $user->ID,
721 832 'username' => $user->user_login,
722 833 'email' => $user->user_email,
723 - 'roles' => ['administrator'],
834 + 'roles' => $user-> roles,
724 835 ];
836 + return $user_data;
837 + }
838 +}
725 839
726 - return $user_data;
840 +if ( ! function_exists( 'timetics_connected_platforms' ) ) {
841 +
842 + /**
843 + * Get connected platforms
844 + *
845 + * @param integer $id
846 + *
847 + * @return array Connected platform lists
848 + */
849 + function timetics_connected_platforms( $id ) {
850 + $integrations = timetics_get_staff_integrations( $id );
851 + $integration_names = [];
852 +
853 + if ( $integrations ) {
854 + foreach ( $integrations as $integration ) {
855 + if ( $integration['connected'] ) {
856 + $integration_names[] = $integration['id'];
857 + }
858 + }
859 + }
860 +
861 + return $integration_names;
727 862 }
728 863 }
729 864
865 +if ( ! function_exists( 'timetics_modules_list' ) ) {
866 + /**
867 + * Get the master list of Arraytics plugins for the About Us page.
868 + *
869 + * Each entry describes an external Arraytics plugin that can be
870 + * installed/activated/deactivated from within the Timetics admin.
871 + *
872 + * @return array
873 + */
874 + function timetics_modules_list(): array {
875 + return [
876 + 'eventin' => [
877 + 'name' => 'eventin',
878 + 'slug' => 'wp-event-solution',
879 + 'type' => 'plugin',
880 + 'upgrade' => false,
881 + 'upgrade_link' => 'https://themewinter.com/eventin',
882 + 'status' => 'on',
883 + 'is_pro' => false,
884 + 'title' => __( 'Eventin', 'timetics' ),
885 + 'description' => __( 'Best Free WordPress Event Calendar Plugin to manage booking, registration, RSVPs, schedule recurring events and sell event tickets with WooCommerce.', 'timetics' ),
886 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'eventin' ),
887 + 'notice' => '',
888 + 'demo_link' => 'https://product.themewinter.com/eventin',
889 + 'settings_link' => '',
890 + 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/eventin/',
891 + ],
892 + 'booktics' => [
893 + 'name' => 'booktics',
894 + 'slug' => 'booktics',
895 + 'type' => 'plugin',
896 + 'upgrade' => false,
897 + 'upgrade_link' => 'https://arraytics.com/booktics',
898 + 'status' => 'on',
899 + 'is_pro' => false,
900 + 'title' => __( 'Booktics', 'timetics' ),
901 + 'description' => __( 'The ultimate online booking plugin for WordPress — services, staff, schedules, and payments in one place.', 'timetics' ),
902 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'booktics' ),
903 + 'notice' => '',
904 + 'demo_link' => 'https://arraytics.com/booktics',
905 + 'settings_link' => '',
906 + 'doc_link' => 'https://docs.arraytics.com/docs/booktics/',
907 + ],
908 + 'poptics' => [
909 + 'name' => 'poptics',
910 + 'slug' => 'poptics',
911 + 'type' => 'plugin',
912 + 'upgrade' => false,
913 + 'upgrade_link' => 'https://arraytics.com/poptics',
914 + 'status' => 'on',
915 + 'is_pro' => false,
916 + 'title' => __( 'Poptics', 'timetics' ),
917 + 'description' => __( 'Discover the ultimate popup mix with Poptics Popup Builder to boost your lead generation and sales conversions.', 'timetics' ),
918 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'poptics' ),
919 + 'notice' => '',
920 + 'demo_link' => 'https://arraytics.com/poptics',
921 + 'settings_link' => '',
922 + 'doc_link' => 'https://docs.aethonic.com/docs/getting-started/intro/',
923 + ],
924 + 'wpcafe' => [
925 + 'name' => 'wpcafe',
926 + 'slug' => 'wp-cafe',
927 + 'type' => 'plugin',
928 + 'upgrade' => false,
929 + 'upgrade_link' => 'https://themewinter.com/wpcafe',
930 + 'status' => 'on',
931 + 'is_pro' => false,
932 + 'title' => __( 'WPCafe', 'timetics' ),
933 + 'description' => __( 'WPCafe — Restaurant plugin with Online Ordering, Delivery, Pickup, Food Menu, Reservations, and Table Management.', 'timetics' ),
934 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'wpcafe' ),
935 + 'notice' => '',
936 + 'demo_link' => 'https://product.themewinter.com/wpcafe',
937 + 'settings_link' => '',
938 + 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/wp-cafe/',
939 + ],
940 + 'aisentic' => [
941 + 'name' => 'aisentic',
942 + 'slug' => 'aisentic',
943 + 'type' => 'plugin',
944 + 'upgrade' => false,
945 + 'upgrade_link' => '',
946 + 'status' => 'on',
947 + 'is_pro' => false,
948 + 'title' => __( 'Aisentic', 'timetics' ),
949 + 'description' => __( 'Your AI assistant for Timetics. Manage bookings, analyze schedules and automate routine tasks using plain English — get work done in seconds instead of hours.', 'timetics' ),
950 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'aisentic' ),
951 + 'notice' => '',
952 + 'demo_link' => '',
953 + 'settings_link' => '',
954 + 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/aisentic/',
955 + ],
956 + 'optiontics' => [
957 + 'name' => 'optiontics',
958 + 'slug' => 'optiontics',
959 + 'type' => 'plugin',
960 + 'upgrade' => false,
961 + 'upgrade_link' => '',
962 + 'status' => 'on',
963 + 'is_pro' => false,
964 + 'title' => __( 'Optiontics', 'timetics' ),
965 + 'description' => __( 'Offer paid add-ons and extra options with every booking — services, extras, packages or custom upgrades that customers can pick during checkout.', 'timetics' ),
966 + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'optiontics' ),
967 + 'notice' => '',
968 + 'demo_link' => '',
969 + 'settings_link' => '',
970 + 'doc_link' => 'https://support.themewinter.com/docs/plugins/plugin-docs/optiontics/how-to-create-product-options/',
971 + 'download_url' => 'https://github.com/themewinter/optiontics-public/releases/download/release/optiontics.zip',
972 + ],
973 + ];
974 + }
975 +}
976 +
977 +if ( ! function_exists( 'timetics_extension' ) ) {
978 + /**
979 + * Get the Timetics Extension manager instance (tools-sdk).
980 + *
981 + * @return \Arraytics\ToolsSdk\Extension
982 + */
983 + function timetics_extension(): \Arraytics\ToolsSdk\Extension {
984 + return new \Arraytics\ToolsSdk\Extension( 'timetics_available_modules', timetics_modules_list() );
985 + }
986 +}
987 +
988 +if ( ! function_exists( 'timetics_get_enabled_modules' ) ) {
989 + /**
990 + * Get list of enabled module slugs.
991 + *
992 + * @return array
993 + */
994 + function timetics_get_enabled_modules(): array {
995 + $modules = get_option( 'timetics_available_modules', [] );
996 +
997 + if ( empty( $modules ) || ! is_array( $modules ) ) {
998 + return [];
999 + }
1000 +
1001 + $enabled = [];
1002 +
1003 + foreach ( $modules as $slug => $data ) {
1004 + if ( is_array( $data ) && isset( $data['status'] ) && 'on' === $data['status'] ) {
1005 + $enabled[] = $slug;
1006 + } elseif ( is_string( $data ) && 'on' === $data ) {
1007 + $enabled[] = $slug;
1008 + }
1009 + }
1010 +
1011 + return $enabled;
1012 + }
1013 +}
1014 +
1015 +if ( ! function_exists( 'timetics_wp_timezone_string' ) ) {
1016 + /**
1017 + * wp_timezone_string() polyfill — core's version needs WP 5.3, plugin supports 5.2.
1018 + *
1019 + * @return string
1020 + */
1021 + function timetics_wp_timezone_string() {
1022 + if ( function_exists( 'wp_timezone_string' ) ) {
1023 + return wp_timezone_string();
1024 + }
1025 +
1026 + $timezone_string = get_option( 'timezone_string' );
1027 +
1028 + if ( $timezone_string ) {
1029 + return $timezone_string;
1030 + }
1031 +
1032 + $offset = (float) get_option( 'gmt_offset' );
1033 + $hours = (int) $offset;
1034 + $minutes = abs( ( $offset - $hours ) * 60 );
1035 +
1036 + return sprintf( '%s%02d:%02d', ( $offset < 0 ) ? '-' : '+', abs( $hours ), $minutes );
1037 + }
1038 +}
1039 +
1040 +if ( ! function_exists( 'timetics_reminder_fallback_timezone' ) ) {
1041 + /**
1042 + * Timezone to interpret a booking's stored wall clock in when the booking
1043 + * itself carries no usable timezone.
1044 + *
1045 + * A booking normally stores the customer's timezone next to the customer's
1046 + * local start time, and that pair must be read together. Bookings created
1047 + * before that meta existed, or from the admin side, have no timezone — those
1048 + * times were entered against the site clock, so the site timezone is the
1049 + * closer reading. Falling back to UTC put every reminder on such a booking
1050 + * out by the site's GMT offset.
1051 + *
1052 + * @return string A timezone identifier or ±hh:mm offset accepted by DateTimeZone.
1053 + */
1054 + function timetics_reminder_fallback_timezone() {
1055 + $timezone = timetics_wp_timezone_string();
1056 +
1057 + return $timezone ? $timezone : 'UTC';
1058 + }
1059 +}
1060 +
1061 +if ( ! function_exists( 'timetics_format_email_datetime' ) ) {
1062 + /**
1063 + * Format date, time and day according to WordPress admin settings
1064 + * Used in email templates to display consistent date/time formats
1065 + *
1066 + * @param string $start_date Start date (Y-m-d format)
1067 + * @param string $start_time Start time (h:i a format)
1068 + *
1069 + * @return array Array with 'day', 'time', 'date' keys
1070 + */
1071 + function timetics_format_email_datetime( $start_date, $start_time = '' ) {
1072 + $wp_date_format = get_option( 'date_format' );
1073 + $wp_time_format = get_option( 'time_format' );
1074 +
1075 + $datetime = $start_time
1076 + ? $start_date . ' ' . $start_time
1077 + : $start_date;
1078 +
1079 + $timestamp = strtotime( $datetime );
1080 +
1081 + return [
1082 + 'day' => date_i18n( 'l', $timestamp ),
1083 + 'time' => date_i18n( $wp_time_format, $timestamp ),
1084 + 'date' => date_i18n( $wp_date_format, $timestamp ),
1085 + ];
1086 + }
1087 +}
1088 +
1089 +if ( ! function_exists( 'timetics_aisentic_identity' ) ) {
1090 + /**
1091 + * Resolve the identity Timetics hands to Aisentic when the user connects.
1092 + *
1093 + * The consent UI shows these exact values before anything leaves the site,
1094 + * so the connect request must read them from here too. Showing one email
1095 + * and sending another would break the consent.
1096 + *
1097 + * The account belongs to the person who opts in, so it is the logged-in
1098 + * user's name and email. The Ask AI setup dialog lets them edit the email,
1099 + * which arrives here as $email. The site admin email is the last fallback.
1100 + *
1101 + * @param string $email Optional email the user typed in the consent UI.
1102 + * @return array{name:string,email:string,site_url:string}
1103 + */
1104 + function timetics_aisentic_identity( $email = '' ) {
1105 + $user = wp_get_current_user();
1106 + $name = sanitize_text_field( (string) $user->display_name );
1107 + $email = sanitize_email( (string) ( $email ?: $user->user_email ?: get_option( 'admin_email', '' ) ) );
1108 +
1109 + return [
1110 + 'name' => $name,
1111 + 'email' => $email,
1112 + 'site_url' => site_url(),
1113 + ];
1114 + }
1115 +}
1116 +
1117 +if ( ! function_exists( 'timetics_aisentic_is_registered' ) ) {
1118 + /**
1119 + * Whether Aisentic already holds a provider api key.
1120 + *
1121 + * Reads Aisentic's own settings, so a site the user registered from
1122 + * Aisentic's own screens counts as connected as well. Without that the
1123 + * dashboard banner would keep nagging people who are already set up.
1124 + *
1125 + * @return bool
1126 + */
1127 + function timetics_aisentic_is_registered() {
1128 + if ( function_exists( 'aisentic_get_option' ) ) {
1129 + return ! empty( aisentic_get_option( 'llmProviders.aisentic.apiKey', '' ) );
1130 + }
1131 +
1132 + $settings = get_option( 'aisentic_settings', [] );
1133 +
1134 + return ! empty( $settings['llmProviders']['aisentic']['apiKey'] );
1135 + }
1136 +}