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 +219 -17 1.0.59 → 1.0.64 View file →
@@ -49,26 +49,47 @@
49 49 */
50 50 function timetics_get_google_access_token( $user_id = 0 ) {
51 51 $data = timetics_get_google_auth( $user_id );
52 52
53 - if ( ! $data ) {
53 + if ( empty( $data ) || empty( $data['access_token'] ) ) {
54 54 return false;
55 55 }
56 56
57 - if ( ( $data['expires_in'] - 30 ) < time() ) {
58 - $refresh_toekn = timetics_get_google_refresh_token( $user_id );
59 - $client = timetics_get_google_client();
57 + $expires_in = isset( $data['expires_in'] ) ? (int) $data['expires_in'] : 0;
60 58
61 - $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 + }
62 63
63 - 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 ) {
64 79 return false;
65 80 }
66 81
67 - 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;
68 86 }
69 87
70 - 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'];
71 92 }
72 93 }
73 94
74 95 if ( ! function_exists( 'timetics_get_google_refresh_token' ) ) {
@@ -107,8 +128,12 @@
107 128 *
108 129 * @return void
109 130 */
110 131 function timetics_update_google_auth( $user_id = 0, $data = [] ) {
132 + if ( empty( $data ) || ! is_array( $data ) ) {
133 + return;
134 + }
135 +
111 136 if ( ! empty( $data['code'] ) ) {
112 137 update_user_meta( $user_id, 'timetics_google_auth_code', $data['code'] );
113 138 }
114 139
@@ -115,11 +140,17 @@
115 140 if ( ! empty( $data['refresh_token'] ) ) {
116 141 update_user_meta( $user_id, 'timetics_google_refresh_token', $data['refresh_token'] );
117 142 }
118 143
119 - $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;
120 150
121 151 update_user_meta( $user_id, 'timetics_google_auth', $data );
152 + delete_user_meta( $user_id, 'timetics_google_auth_error' );
122 153 }
123 154 }
124 155
125 156 if ( ! function_exists( 'timetics_get_google_auth' ) ) {
@@ -314,8 +345,70 @@
314 345 return $total;
315 346 }
316 347 }
317 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 +
318 411 if ( ! function_exists( 'timetics_get_staff_integrations' ) ) {
319 412 /**
320 413 * Get all staff integrations
321 414 *
@@ -321,9 +414,15 @@
321 414 *
322 415 * @return array
323 416 */
324 417 function timetics_get_staff_integrations( $user_id = 0 ) {
325 - $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 +
326 425 $google_credentials = timetics_get_google_credentials();
327 426 $is_setup = ! empty( $google_credentials['client_id'] ) && ! empty( $google_credentials['client_secret'] );
328 427 $current_user_id = ( $user_id == get_current_user_id() ) ? true : false;
329 428
@@ -332,9 +431,10 @@
332 431 'id' => 'google-calendar-meet',
333 432 'name' => esc_html__( 'Google Meet', 'timetics' ),
334 433 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
335 434 'auth_url' => timetics_get_google_auth_url(),
336 - 'connected' => ! empty( $google_auth ),
435 + 'connected' => $is_connected,
436 + 'needs_reauth' => $needs_reauth,
337 437 'setup' => $is_setup,
338 438 'current_user' => $current_user_id,
339 439 ],
340 440 [
@@ -341,9 +441,10 @@
341 441 'id' => 'google-calendar',
342 442 'name' => esc_html__( 'Google Calendar', 'timetics' ),
343 443 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
344 444 'auth_url' => timetics_get_google_auth_url(),
345 - 'connected' => ! empty( $google_auth ),
445 + 'connected' => $is_connected,
446 + 'needs_reauth' => $needs_reauth,
346 447 'setup' => $is_setup,
347 448 'current_user' => $current_user_id,
348 449 ],
349 450 ];
@@ -410,8 +511,9 @@
410 511 'default_booking_status' => 'pending',
411 512 'currency' => 'USD',
412 513 'primary_color' => '#3161F1',
413 514 'secondary_color' => '#6188ff',
515 + 'unpaid_booking_expiry_minutes' => 5,
414 516 ];
415 517
416 518 $settings = apply_filters( 'timetics_default_settings', $settings );
417 519
@@ -513,8 +615,14 @@
513 615 * @return void
514 616 */
515 617 function timetics_register_cron() {
516 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 + }
517 625 }
518 626 }
519 627
520 628 if ( ! function_exists( 'timetics_is_woocommerce_active' ) ) {
@@ -837,15 +945,14 @@
837 945 'upgrade_link' => '',
838 946 'status' => 'on',
839 947 'is_pro' => false,
840 948 'title' => __( 'Aisentic', 'timetics' ),
841 - 'description' => __( 'AI assistant for WordPress — automate content, replies and on-site tasks without leaving your dashboard.', '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' ),
842 950 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'aisentic' ),
843 951 'notice' => '',
844 952 'demo_link' => '',
845 953 'settings_link' => '',
846 954 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/aisentic/',
847 - 'download_url' => 'https://github.com/themewinter/aisentic-public/releases/download/v1.0.0/aisentic-1.0.0.zip',
848 955 ],
849 956 'optiontics' => [
850 957 'name' => 'optiontics',
851 958 'slug' => 'optiontics',
@@ -854,9 +961,9 @@
854 961 'upgrade_link' => '',
855 962 'status' => 'on',
856 963 'is_pro' => false,
857 964 'title' => __( 'Optiontics', 'timetics' ),
858 - 'description' => __( 'Product add-ons / extras for food items — let customers pick toppings, sizes and other paid options along with the products.', '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' ),
859 966 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'optiontics' ),
860 967 'notice' => '',
861 968 'demo_link' => '',
862 969 'settings_link' => '',
@@ -904,8 +1011,54 @@
904 1011 return $enabled;
905 1012 }
906 1013 }
907 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 +
908 1061 if ( ! function_exists( 'timetics_format_email_datetime' ) ) {
909 1062 /**
910 1063 * Format date, time and day according to WordPress admin settings
911 1064 * Used in email templates to display consistent date/time formats
@@ -918,10 +1071,10 @@
918 1071 function timetics_format_email_datetime( $start_date, $start_time = '' ) {
919 1072 $wp_date_format = get_option( 'date_format' );
920 1073 $wp_time_format = get_option( 'time_format' );
921 1074
922 - $datetime = $start_time
923 - ? $start_date . ' ' . $start_time
1075 + $datetime = $start_time
1076 + ? $start_date . ' ' . $start_time
924 1077 : $start_date;
925 1078
926 1079 $timestamp = strtotime( $datetime );
927 1080
@@ -929,6 +1082,55 @@
929 1082 'day' => date_i18n( 'l', $timestamp ),
930 1083 'time' => date_i18n( $wp_time_format, $timestamp ),
931 1084 'date' => date_i18n( $wp_date_format, $timestamp ),
932 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'] );
933 1135 }
934 1136 }