Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/hydra-booking/admin/Controller/BookingController.php
+++ b/hydra-booking/admin/Controller/BookingController.php
@@ -931,7 +931,19 @@
}
if ( 'schedule' == $request['status'] ) {
- do_action( 'hydra_booking/after_booking_schedule', $single_booking_meta );
+ $attendee = null;
+ if ( ! empty( $request['id'] ) ) {
+ $Attendee = new Attendees();
+ $attendee = $Attendee->getAttendeeWithBooking(
+ array(
+ array( 'booking_id', '=', absint( $request['id'] ) ),
+ ),
+ 1,
+ 'DESC'
+ );
+ }
+
+ do_action( 'hydra_booking/after_booking_schedule', absint( $request['id'] ), $attendee );
}
@@ -964,6 +976,10 @@
}
// Delete Booking
$booking = new Booking();
+ $single_booking_meta = $booking->get( absint( $booking_id ) );
+ if ( ! empty( $single_booking_meta ) ) {
+ do_action( 'hydra_booking/after_booking_deleted', $single_booking_meta );
+ }
$bookingDelete = $booking->delete( $booking_id );
$current_user = get_userdata( $booking_owner );
// get user role
--- a/hydra-booking/hydra-booking.php
+++ b/hydra-booking/hydra-booking.php
@@ -3,7 +3,7 @@
* Plugin Name: Hydra Booking — Appointment Scheduling & Booking Calendar
* Plugin URI: https://hydrabooking.com/
* Description: Appointment Booking Plugin with Automated Scheduling - Apple/Outlook/ Google Calendar, WooCommerce, Zoom, Fluent Forms, Zapier, Mailchimp & CRM Integration.
- * Version: 1.1.41
+ * Version: 1.1.42
* Tested up to: 6.9
* Author: Themefic
* Author URI: https://themefic.com/
@@ -26,7 +26,7 @@
define( 'TFHB_PATH', plugin_dir_path( __FILE__ ) );
define( 'TFHB_URL', plugin_dir_url( __FILE__ ) );
- define( 'TFHB_VERSION', '1.1.41' );
+ define( 'TFHB_VERSION', '1.1.42' );
define( 'TFHB_BASE_FILE', __FILE__);
define( 'TFHB_DEV_MODE', false ); // Set true to enable dev mode
--- a/hydra-booking/includes/hooks/ActionHooks.php
+++ b/hydra-booking/includes/hooks/ActionHooks.php
@@ -20,6 +20,7 @@
if(!empty($google_calendar) && $google_calendar['status'] == true){
add_action( 'hydra_booking/after_booking_confirmed', array( new GoogleCalendar(), 'insert_calender_after_booking_confirmed' ), 11, 2 );
add_action( 'hydra_booking/after_booking_canceled', array( new GoogleCalendar(), 'deleteGoogleCalender' ), 11, 2 );
+ add_action( 'hydra_booking/after_booking_deleted', array( new GoogleCalendar(), 'deleteGoogleCalender' ), 11, 1 );
add_action( 'hydra_booking/after_booking_schedule', array( new GoogleCalendar(), 'remove_attendde_event_from_existing_booking' ), 11, 2 );
}
--- a/hydra-booking/includes/services/Integrations/GoogleCalendar/GoogleCalendar.php
+++ b/hydra-booking/includes/services/Integrations/GoogleCalendar/GoogleCalendar.php
@@ -32,7 +32,7 @@
public function __construct() {
- $this->setClientData();
+ $this->setClientData();
@@ -43,6 +43,32 @@
return wp_date( $helper->get_date_time_format_from_settings( 'M d, Y', 'h:i A' ) );
}
+ private function set_event_time_from_booking( &$event, $booking_data, $meeting_date = '' ) {
+ if ( empty( $meeting_date ) ) {
+ $meeting_date = isset( $booking_data->meeting_dates ) ? $booking_data->meeting_dates : '';
+ }
+
+ if ( empty( $meeting_date ) || empty( $booking_data->start_time ) || empty( $booking_data->end_time ) ) {
+ return;
+ }
+
+ $start_time = strtotime( $booking_data->start_time );
+ $end_time = strtotime( $booking_data->end_time );
+
+ $start_date = gmdate( 'Y-m-d', strtotime( $meeting_date ) ) . 'T' . gmdate( 'H:i:s', $start_time );
+ $end_date = gmdate( 'Y-m-d', strtotime( $meeting_date ) ) . 'T' . gmdate( 'H:i:s', $end_time );
+
+ $event->start = array(
+ 'dateTime' => $start_date,
+ 'timeZone' => $booking_data->availability_time_zone,
+ );
+
+ $event->end = array(
+ 'dateTime' => $end_date,
+ 'timeZone' => $booking_data->availability_time_zone,
+ );
+ }
+
// Update Google Calender
public function checkConnectionStatus(){
if($this->clientId != '' && $this->clientSecret != '' && $this->redirectUrl != ''){
@@ -67,6 +93,62 @@
// example : wp-json/hydra-booking/v1/integration/google-api
return get_rest_url() . 'hydra-booking/v1/integration/google-api';
}
+
+ private function get_oauth_state_key( $state ) {
+ return 'tfhb_google_oauth_' . md5( $state );
+ }
+
+ private function create_oauth_state( $user_id ) {
+ $user_id = absint( $user_id );
+
+ if ( ! $user_id || ! get_userdata( $user_id ) ) {
+ return '';
+ }
+
+ $state = wp_generate_password( 32, false, false );
+
+ set_transient(
+ $this->get_oauth_state_key( $state ),
+ array(
+ 'user_id' => $user_id,
+ 'initiated_by' => get_current_user_id(),
+ 'session_token' => wp_get_session_token(),
+ ),
+ HOUR_IN_SECONDS
+ );
+
+ return $state;
+ }
+
+ private function get_oauth_state_data( $state ) {
+ $state = sanitize_text_field( wp_unslash( $state ) );
+
+ if ( empty( $state ) ) {
+ return false;
+ }
+
+ $state_data = get_transient( $this->get_oauth_state_key( $state ) );
+
+ if ( ! is_array( $state_data ) || empty( $state_data['user_id'] ) ) {
+ return false;
+ }
+
+ $current_session_token = wp_get_session_token();
+
+ if ( ! empty( $state_data['session_token'] ) && ! hash_equals( $state_data['session_token'], $current_session_token ) ) {
+ return false;
+ }
+
+ return $state_data;
+ }
+
+ private function delete_oauth_state( $state ) {
+ $state = sanitize_text_field( wp_unslash( $state ) );
+
+ if ( ! empty( $state ) ) {
+ delete_transient( $this->get_oauth_state_key( $state ) );
+ }
+ }
// Set Access Token
public function setAccessToken( $user_id ) {
@@ -87,59 +169,109 @@
)
);
}
- public function permission_callback() {
+ public function permission_callback( $request ) {
+ $state = $request instanceof WP_REST_Request ? $request->get_param( 'state' ) : '';
+
+ if ( false === $this->get_oauth_state_data( $state ) ) {
+ return new WP_Error(
+ 'rest_forbidden',
+ __( 'Sorry, you are not allowed to do that.', 'hydra-booking' ),
+ array( 'status' => rest_authorization_required_code() )
+ );
+ }
+
return true;
}
- public function GetAccessData() {
+ public function GetAccessData( $request ) {
+ $code = $request instanceof WP_REST_Request ? $request->get_param( 'code' ) : ( isset( $_GET['code'] ) ? wp_unslash( $_GET['code'] ) : '' );
+ $state = $request instanceof WP_REST_Request ? $request->get_param( 'state' ) : ( isset( $_GET['state'] ) ? wp_unslash( $_GET['state'] ) : '' );
+ $error = $request instanceof WP_REST_Request ? $request->get_param( 'error' ) : ( isset( $_GET['error'] ) ? wp_unslash( $_GET['error'] ) : '' );
+
+ $state_data = $this->get_oauth_state_data( $state );
+
+ if ( false === $state_data ) {
+ return new WP_Error(
+ 'invalid_google_oauth_state',
+ __( 'Invalid or expired Google authorization state.', 'hydra-booking' ),
+ array( 'status' => 403 )
+ );
+ }
- // Set the Client Data
- if ( isset( $_GET['code'] ) && isset( $_GET['state'] ) ) {
+ $user_id = absint( $state_data['user_id'] );
+ $redirect_url = get_site_url() . '/wp-admin/admin.php?page=hydra-booking#/hosts/profile/' . $user_id . '/calendars';
- try {
+ if ( ! empty( $error ) ) {
+ $this->delete_oauth_state( $state );
+ wp_safe_redirect( add_query_arg( 'google_calendar_error', sanitize_text_field( $error ), $redirect_url ) );
+ exit;
+ }
- $user_id = $_GET['state'];
+ if ( empty( $code ) ) {
+ $this->delete_oauth_state( $state );
+ return new WP_Error(
+ 'missing_google_oauth_code',
+ __( 'Missing Google authorization code.', 'hydra-booking' ),
+ array( 'status' => 400 )
+ );
+ }
+
+ try {
+ $data = $this->GetAccessToken( sanitize_text_field( $code ) );
+
+ if ( empty( $data['access_token'] ) || empty( $data['id_token'] ) ) {
+ $this->delete_oauth_state( $state );
+ return new WP_Error(
+ 'google_oauth_token_error',
+ __( 'Unable to validate the Google authorization response.', 'hydra-booking' ),
+ array( 'status' => 400 )
+ );
+ }
- $data = $this->GetAccessToken( $_GET['code'] );
- $email = $this->getEmailByIdToken( $data['id_token'] );
+ $email = $this->getEmailByIdToken( $data['id_token'] );
- // Get all calendar in the account
- $url = 'https://www.googleapis.com/calendar/v3/users/me/calendarList';
- $response = wp_remote_get( $url, array( 'headers' => array( 'Authorization' => 'Bearer ' . $data['access_token'] ) ) );
- $body = wp_remote_retrieve_body( $response );
- $body = json_decode( $body, true );
+ // Get all calendar in the account
+ $url = 'https://www.googleapis.com/calendar/v3/users/me/calendarList';
+ $response = wp_remote_get( $url, array( 'headers' => array( 'Authorization' => 'Bearer ' . $data['access_token'] ) ) );
+ $body = wp_remote_retrieve_body( $response );
+ $body = json_decode( $body, true );
- $data['email'] = $email;
+ $data['email'] = sanitize_email( $email );
+ $data['items'] = array();
+ if ( isset( $body['items'] ) && is_array( $body['items'] ) ) {
foreach ( $body['items'] as $calendar ) {
- if ( $calendar['accessRole'] == 'owner' || $calendar['accessRole'] == 'writer' ) {
+ if ( isset( $calendar['accessRole'] ) && ( 'owner' === $calendar['accessRole'] || 'writer' === $calendar['accessRole'] ) ) {
$data['items'][] = array(
- 'id' => $calendar['id'],
- 'title' => $calendar['summary'],
+ 'id' => isset( $calendar['id'] ) ? $calendar['id'] : '',
+ 'title' => isset( $calendar['summary'] ) ? $calendar['summary'] : '',
'write_status' => 0,
);
}
}
+ }
- // remove the Id Token
- unset( $data['id_token'] );
-
- $_tfhb_host_integration_settings = is_array( get_user_meta( $user_id, '_tfhb_host_integration_settings', true ) ) ? get_user_meta( $user_id, '_tfhb_host_integration_settings', true ) : array();
-
- $_tfhb_host_integration_settings['google_calendar']['tfhb_google_calendar'] = $data;
-
- // save to user metadata
- update_user_meta( $user_id, '_tfhb_host_integration_settings', $_tfhb_host_integration_settings );
+ // remove the Id Token
+ unset( $data['id_token'] );
- $redirect_url = get_site_url() . '/wp-admin/admin.php?page=hydra-booking#/hosts/profile/' . $user_id . '/calendars';
+ $_tfhb_host_integration_settings = is_array( get_user_meta( $user_id, '_tfhb_host_integration_settings', true ) ) ? get_user_meta( $user_id, '_tfhb_host_integration_settings', true ) : array();
- wp_redirect( $redirect_url );
-
+ $_tfhb_host_integration_settings['google_calendar']['tfhb_google_calendar'] = $data;
- } catch ( Exception $e ) {
- echo esc_html($e->getMessage());
- exit();
- }
+ // save to user metadata
+ update_user_meta( $user_id, '_tfhb_host_integration_settings', $_tfhb_host_integration_settings );
+ $this->delete_oauth_state( $state );
+
+ wp_safe_redirect( $redirect_url );
+ exit;
+
+ } catch ( Exception $e ) {
+ $this->delete_oauth_state( $state );
+ return new WP_Error(
+ 'google_oauth_error',
+ esc_html( $e->getMessage() ),
+ array( 'status' => 400 )
+ );
}
}
@@ -207,7 +339,24 @@
}
public function GetAccessTokenUrl( $user_id ) {
- return $this->authUrl . '?client_id=' . $this->clientId . '&redirect_uri=' . $this->redirectUrl . '&scope=' . $this->authScope . '&response_type=code&access_type=offline&prompt=consent&state=' . $user_id;
+ $state = $this->create_oauth_state( $user_id );
+
+ if ( empty( $state ) ) {
+ return '';
+ }
+
+ return add_query_arg(
+ array(
+ 'client_id' => $this->clientId,
+ 'redirect_uri' => $this->redirectUrl,
+ 'scope' => $this->authScope,
+ 'response_type' => 'code',
+ 'access_type' => 'offline',
+ 'prompt' => 'consent',
+ 'state' => $state,
+ ),
+ $this->authUrl
+ );
}
@@ -524,6 +673,54 @@
* @param $data
* @return mixed
*/
+ /**
+ * Delete all Google Calendar events stored for a booking and remove the meta record.
+ * Passing sendUpdates=all causes Google to email cancellation notices to all attendees.
+ *
+ * @param object $booking_meta Row from BookingMeta (must have ->id and ->value).
+ * @param int $host_id
+ * @return void
+ */
+ private function delete_google_calendar_events( $booking_meta, $host_id ) {
+ $host = new Host();
+ $hostData = $host->get( $host_id );
+
+ if ( ! $hostData ) {
+ return;
+ }
+
+ $_tfhb_host_integration_settings = is_array( get_user_meta( $hostData->user_id, '_tfhb_host_integration_settings', true ) ) ? get_user_meta( $hostData->user_id, '_tfhb_host_integration_settings', true ) : array();
+ $google_calendar = isset( $_tfhb_host_integration_settings['google_calendar'] ) ? $_tfhb_host_integration_settings['google_calendar'] : array();
+ $calendarId = isset( $google_calendar['selected_calendar_id'] ) && ! empty( $google_calendar['selected_calendar_id'] ) ? $google_calendar['selected_calendar_id'] : ( isset( $google_calendar['tfhb_google_calendar']['email'] ) ? $google_calendar['tfhb_google_calendar']['email'] : '' );
+
+ if ( ! $calendarId ) {
+ return;
+ }
+
+ $value = json_decode( $booking_meta->value );
+ $events = isset( $value->google_calendar ) ? $value->google_calendar : array();
+
+ foreach ( $events as $event ) {
+ $event_id = isset( $event->id ) ? $event->id : '';
+ if ( empty( $event_id ) ) {
+ continue;
+ }
+
+ // DELETE with sendUpdates=all → Google sends cancellation emails to all guests.
+ wp_remote_request(
+ $this->calendarEvent . $calendarId . '/events/' . $event_id . '?sendUpdates=all',
+ array(
+ 'headers' => array( 'Authorization' => 'Bearer ' . $this->accessToken ),
+ 'method' => 'DELETE',
+ )
+ );
+ }
+
+ // Remove the stored calendar meta so InsertGoogleCalender creates a fresh record.
+ $BookingMeta = new BookingMeta();
+ $BookingMeta->delete( $booking_meta->id );
+ }
+
public function remove_attendde_event_from_existing_booking( $old_booking_id, $attendee){
@@ -532,15 +729,49 @@
// Get Booking With Attendee
$booking = new Booking();
$booking_data = $booking->get( $booking_id );
+ if ( ! $booking_data ) {
+ return;
+ }
+
+ $is_same_booking_reschedule = ! empty( $attendee ) && isset( $attendee->booking_id ) && absint( $attendee->booking_id ) === absint( $old_booking_id );
$this->refreshToken( $booking_data->host_id );
+ $meeting_dates = ! empty( $booking_data->meeting_dates ) ? explode( ',', $booking_data->meeting_dates ) : array();
// if is not array or not object json decode
- $locations = !empty($attendee->meeting_locations) ? $attendee->meeting_locations : array();
+ $locations = ! empty( $attendee ) && isset( $attendee->meeting_locations ) ? $attendee->meeting_locations : array();
$_tfhb_integration_settings = get_option( '_tfhb_integration_settings' );
$BookingMeta = new BookingMeta();
$booking_meta = $BookingMeta->getWithIdKey( $booking_data->id, 'booking_calendar', 1);
-
+
+ // ----------------------------------------------------------------
+ // Same-booking reschedule: cancel the old event (Google sends cancel
+ // emails) then create a fresh event at the new time (Google sends
+ // a new invite). No attendee-filter PUT is needed.
+ // ----------------------------------------------------------------
+ if ( $is_same_booking_reschedule ) {
+ if ( $booking_meta ) {
+ $this->delete_google_calendar_events( $booking_meta, $booking_data->host_id );
+ }
+
+ // InsertGoogleCalender reads meeting_dates/start_time/end_time from the
+ // booking row which already holds the rescheduled values at this point.
+ $this->InsertGoogleCalender( $attendee );
+
+ $ActivityMeta = new BookingMeta();
+ $ActivityMeta->add( array(
+ 'booking_id' => $attendee->booking_id,
+ 'meta_key' => 'booking_activity',
+ 'value' => array(
+ 'datetime' => $this->get_activity_datetime(),
+ 'title' => 'Updated Google Calendar Event',
+ 'description' => 'Google Calendar event cancelled and recreated at new time',
+ ),
+ ) );
+
+ return;
+ }
+ // ----------------------------------------------------------------
if($booking_meta){
@@ -551,16 +782,20 @@
$hostData = $host->get( $booking_data->host_id );
$google_calendar_body = array();
- foreach ( $events as $event ) {
+ foreach ( $events as $index => $event ) {
$event_id = $event->id;
+ $meeting_date = isset( $meeting_dates[ $index ] ) ? trim( $meeting_dates[ $index ] ) : ( isset( $meeting_dates[0] ) ? trim( $meeting_dates[0] ) : '' );
+ $this->set_event_time_from_booking( $event, $booking_data, $meeting_date );
- $attendees_data = $event->attendees;
+ $attendees_data = isset( $event->attendees ) && is_array( $event->attendees ) ? $event->attendees : array();
// remove existing attendee
- $attendees_data = array_filter($attendees_data, function($value) use ($attendee) {
- return $value->email != $attendee->email;
- });
+ if ( ! empty( $attendee ) && isset( $attendee->email ) ) {
+ $attendees_data = array_filter($attendees_data, function($value) use ($attendee) {
+ return isset( $value->email ) && $value->email != $attendee->email;
+ });
+ }
$event->attendees = $attendees_data;
@@ -600,7 +835,7 @@
// Add activity after email sent
$UpdateBookingMeta->add([
- 'booking_id' => $attendee->booking_id,
+ 'booking_id' => ! empty( $attendee ) && isset( $attendee->booking_id ) ? $attendee->booking_id : $booking_data->id,
'meta_key' => 'booking_activity',
'value' => array(
'datetime' => $this->get_activity_datetime(),
@@ -611,7 +846,9 @@
);
}
}
- $this->insert_calender_after_booking_confirmed($attendee);
+ if ( ! empty( $attendee ) && isset( $attendee->booking_id ) ) {
+ $this->insert_calender_after_booking_confirmed($attendee);
+ }
}
@@ -622,14 +859,40 @@
$this->refreshToken( $booking->host_id );
$events = json_decode($BookingMeta->value);
$events = $events->google_calendar;
+ $meeting_dates = ! empty( $booking->meeting_dates ) ? explode( ',', $booking->meeting_dates ) : array();
$host = new Host();
$hostData = $host->get( $booking->host_id );
$google_calendar_body = array();
- foreach ( $events as $event ) {
+ foreach ( $events as $index => $event ) {
$event_id = $event->id;
+ $meeting_date = isset( $meeting_dates[ $index ] ) ? trim( $meeting_dates[ $index ] ) : ( isset( $meeting_dates[0] ) ? trim( $meeting_dates[0] ) : '' );
+ $this->set_event_time_from_booking( $event, $booking, $meeting_date );
- $event->attendees[] = array('email' => $booking->email);;
+ if ( ! isset( $event->attendees ) || ! is_array( $event->attendees ) ) {
+ $event->attendees = array();
+ }
+
+ if ( ! empty( $booking->email ) ) {
+ $existing_attendee_emails = array_map(
+ function( $attendee ) {
+ if ( is_object( $attendee ) && isset( $attendee->email ) ) {
+ return $attendee->email;
+ }
+
+ if ( is_array( $attendee ) && isset( $attendee['email'] ) ) {
+ return $attendee['email'];
+ }
+
+ return '';
+ },
+ $event->attendees
+ );
+
+ if ( ! in_array( $booking->email, $existing_attendee_emails, true ) ) {
+ $event->attendees[] = array('email' => $booking->email);
+ }
+ }
$_tfhb_host_integration_settings = is_array( get_user_meta( $hostData->user_id, '_tfhb_host_integration_settings', true ) ) ? get_user_meta( $hostData->user_id, '_tfhb_host_integration_settings', true ) : array();
$google_calendar = isset( $_tfhb_host_integration_settings['google_calendar'] ) ? $_tfhb_host_integration_settings['google_calendar'] : array();