chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
751
care-booking-block/includes/class-admin-interface.php
Normal file
751
care-booking-block/includes/class-admin-interface.php
Normal file
@@ -0,0 +1,751 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Admin interface for Care Booking Block plugin
|
||||
*
|
||||
* @package CareBookingBlock
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin interface class
|
||||
*/
|
||||
class Care_Booking_Admin_Interface
|
||||
{
|
||||
/**
|
||||
* Database handler instance
|
||||
*
|
||||
* @var Care_Booking_Database_Handler
|
||||
*/
|
||||
private $db_handler;
|
||||
|
||||
/**
|
||||
* Restriction model instance
|
||||
*
|
||||
* @var Care_Booking_Restriction_Model
|
||||
*/
|
||||
private $restriction_model;
|
||||
|
||||
/**
|
||||
* Admin page slug
|
||||
*/
|
||||
const ADMIN_PAGE_SLUG = 'care-booking-control';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Care_Booking_Database_Handler $db_handler Database handler instance
|
||||
*/
|
||||
public function __construct($db_handler)
|
||||
{
|
||||
$this->db_handler = $db_handler;
|
||||
$this->restriction_model = new Care_Booking_Restriction_Model();
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WordPress hooks
|
||||
*/
|
||||
private function init_hooks()
|
||||
{
|
||||
// Admin menu
|
||||
add_action('admin_menu', [$this, 'add_admin_menu']);
|
||||
|
||||
// Admin scripts and styles
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_assets']);
|
||||
|
||||
// AJAX handlers
|
||||
add_action('wp_ajax_care_booking_get_restrictions', [$this, 'ajax_get_restrictions']);
|
||||
add_action('wp_ajax_care_booking_toggle_restriction', [$this, 'ajax_toggle_restriction']);
|
||||
add_action('wp_ajax_care_booking_bulk_update', [$this, 'ajax_bulk_update']);
|
||||
add_action('wp_ajax_care_booking_get_entities', [$this, 'ajax_get_entities']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin menu
|
||||
*/
|
||||
public function add_admin_menu()
|
||||
{
|
||||
add_management_page(
|
||||
__('Care Booking Control', 'care-booking-block'),
|
||||
__('Care Booking Control', 'care-booking-block'),
|
||||
'manage_options',
|
||||
self::ADMIN_PAGE_SLUG,
|
||||
[$this, 'render_admin_page']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin assets
|
||||
*
|
||||
* @param string $hook_suffix Current admin page
|
||||
*/
|
||||
public function enqueue_admin_assets($hook_suffix)
|
||||
{
|
||||
// Only load on our admin page
|
||||
if (strpos($hook_suffix, self::ADMIN_PAGE_SLUG) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enqueue CSS
|
||||
wp_enqueue_style(
|
||||
'care-booking-admin',
|
||||
CARE_BOOKING_BLOCK_PLUGIN_URL . 'admin/css/admin-style.css',
|
||||
[],
|
||||
CARE_BOOKING_BLOCK_VERSION
|
||||
);
|
||||
|
||||
// Enqueue JavaScript
|
||||
wp_enqueue_script(
|
||||
'care-booking-admin',
|
||||
CARE_BOOKING_BLOCK_PLUGIN_URL . 'admin/js/admin-script.js',
|
||||
['jquery'],
|
||||
CARE_BOOKING_BLOCK_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Localize script
|
||||
wp_localize_script('care-booking-admin', 'careBookingAjax', [
|
||||
'ajaxurl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('care_booking_nonce'),
|
||||
'strings' => [
|
||||
'loading' => __('Loading...', 'care-booking-block'),
|
||||
'error' => __('An error occurred. Please try again.', 'care-booking-block'),
|
||||
'confirm_bulk' => __('Are you sure you want to update all selected restrictions?', 'care-booking-block'),
|
||||
'success_update' => __('Restriction updated successfully.', 'care-booking-block'),
|
||||
'success_bulk' => __('Bulk update completed.', 'care-booking-block')
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render admin page
|
||||
*/
|
||||
public function render_admin_page()
|
||||
{
|
||||
// Check KiviCare availability
|
||||
if (!$this->is_kivicare_active()) {
|
||||
$this->render_kivicare_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
include CARE_BOOKING_BLOCK_PLUGIN_DIR . 'admin/partials/admin-display.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: Get restrictions
|
||||
*/
|
||||
public function ajax_get_restrictions()
|
||||
{
|
||||
// SECURITY: Enhanced CSRF protection with additional request validation
|
||||
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'care_booking_nonce')) {
|
||||
wp_send_json_error(['message' => __('Security check failed', 'care-booking-block')]);
|
||||
wp_die(); // Additional security measure
|
||||
}
|
||||
|
||||
// SECURITY: Check if request is actually via AJAX
|
||||
if (!wp_doing_ajax()) {
|
||||
wp_send_json_error(['message' => __('Invalid request method', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Enhanced capability check with logging
|
||||
if (!current_user_can('manage_options')) {
|
||||
error_log('Care Booking Block: Unauthorized access attempt from user ID: ' . get_current_user_id());
|
||||
wp_send_json_error(['message' => __('Insufficient permissions', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Rate limiting check
|
||||
if (!$this->check_rate_limit('get_restrictions')) {
|
||||
wp_send_json_error(['message' => __('Too many requests. Please wait.', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Enhanced input sanitization and validation
|
||||
$restriction_type = sanitize_text_field($_POST['restriction_type'] ?? 'all');
|
||||
$doctor_id = isset($_POST['doctor_id']) ? absint($_POST['doctor_id']) : null;
|
||||
|
||||
// SECURITY: Validate restriction_type against whitelist
|
||||
$allowed_types = ['all', 'doctor', 'service'];
|
||||
if (!in_array($restriction_type, $allowed_types, true)) {
|
||||
error_log('Care Booking Block: Invalid restriction_type attempted: ' . $restriction_type);
|
||||
wp_send_json_error(['message' => __('Invalid restriction type', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Validate doctor_id if provided
|
||||
if ($doctor_id !== null && $doctor_id <= 0) {
|
||||
wp_send_json_error(['message' => __('Invalid doctor ID', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
try {
|
||||
if ($restriction_type === 'all') {
|
||||
$restrictions = $this->restriction_model->get_all();
|
||||
} elseif (in_array($restriction_type, ['doctor', 'service'])) {
|
||||
$restrictions = $this->restriction_model->get_by_type($restriction_type);
|
||||
|
||||
// Filter by doctor if specified
|
||||
if ($restriction_type === 'service' && $doctor_id) {
|
||||
$restrictions = array_filter($restrictions, function($r) use ($doctor_id) {
|
||||
return $r->doctor_id == $doctor_id;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
wp_send_json_error(['message' => __('Invalid parameters', 'care-booking-block')]);
|
||||
}
|
||||
|
||||
// SECURITY: Convert to array format with output escaping
|
||||
$formatted_restrictions = [];
|
||||
foreach ($restrictions as $restriction) {
|
||||
$formatted_restrictions[] = [
|
||||
'id' => (int) $restriction->id,
|
||||
'restriction_type' => esc_html($restriction->restriction_type),
|
||||
'target_id' => (int) $restriction->target_id,
|
||||
'doctor_id' => $restriction->doctor_id ? (int) $restriction->doctor_id : null,
|
||||
'is_blocked' => (bool) $restriction->is_blocked,
|
||||
'created_at' => esc_html($restriction->created_at),
|
||||
'updated_at' => esc_html($restriction->updated_at)
|
||||
];
|
||||
}
|
||||
|
||||
wp_send_json_success([
|
||||
'restrictions' => $formatted_restrictions,
|
||||
'total' => count($formatted_restrictions)
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error(['message' => __('Database error occurred', 'care-booking-block')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: Toggle restriction
|
||||
*/
|
||||
public function ajax_toggle_restriction()
|
||||
{
|
||||
// SECURITY: Enhanced CSRF protection
|
||||
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'care_booking_nonce')) {
|
||||
wp_send_json_error(['message' => __('Security check failed', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: AJAX request validation
|
||||
if (!wp_doing_ajax()) {
|
||||
wp_send_json_error(['message' => __('Invalid request method', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Enhanced capability check with logging
|
||||
if (!current_user_can('manage_options')) {
|
||||
error_log('Care Booking Block: Unauthorized toggle attempt from user ID: ' . get_current_user_id());
|
||||
wp_send_json_error(['message' => __('Insufficient permissions', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Rate limiting
|
||||
if (!$this->check_rate_limit('toggle_restriction')) {
|
||||
wp_send_json_error(['message' => __('Too many requests. Please wait.', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Enhanced parameter validation and sanitization
|
||||
$restriction_type = sanitize_text_field($_POST['restriction_type'] ?? '');
|
||||
$target_id = absint($_POST['target_id'] ?? 0);
|
||||
$doctor_id = isset($_POST['doctor_id']) ? absint($_POST['doctor_id']) : null;
|
||||
$is_blocked = isset($_POST['is_blocked']) ? (bool) $_POST['is_blocked'] : true;
|
||||
|
||||
// SECURITY: Validate required parameters
|
||||
if (!$restriction_type || !$target_id) {
|
||||
error_log('Care Booking Block: Missing parameters in toggle_restriction');
|
||||
wp_send_json_error(['message' => __('Missing required parameters', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Whitelist validation for restriction_type
|
||||
$allowed_types = ['doctor', 'service'];
|
||||
if (!in_array($restriction_type, $allowed_types, true)) {
|
||||
error_log('Care Booking Block: Invalid restriction_type in toggle: ' . $restriction_type);
|
||||
wp_send_json_error(['message' => __('Invalid restriction type', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Validate target_id range
|
||||
if ($target_id <= 0 || $target_id > PHP_INT_MAX) {
|
||||
wp_send_json_error(['message' => __('Invalid target ID', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Service restriction validation
|
||||
if ($restriction_type === 'service' && (!$doctor_id || $doctor_id <= 0)) {
|
||||
wp_send_json_error(['message' => __('Valid doctor_id required for service restrictions', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate target exists in KiviCare
|
||||
if (!$this->validate_kivicare_target($restriction_type, $target_id, $doctor_id)) {
|
||||
wp_send_json_error(['message' => __('Target not found', 'care-booking-block')]);
|
||||
}
|
||||
|
||||
// Toggle restriction
|
||||
$result = $this->restriction_model->toggle($restriction_type, $target_id, $doctor_id, $is_blocked);
|
||||
|
||||
if ($result) {
|
||||
// Get updated/created restriction
|
||||
$restriction = $this->restriction_model->find_existing($restriction_type, $target_id, $doctor_id);
|
||||
|
||||
if ($restriction) {
|
||||
wp_send_json_success([
|
||||
'message' => __('Restriction updated successfully', 'care-booking-block'),
|
||||
'restriction' => [
|
||||
'id' => (int) $restriction->id,
|
||||
'restriction_type' => esc_html($restriction->restriction_type),
|
||||
'target_id' => (int) $restriction->target_id,
|
||||
'doctor_id' => $restriction->doctor_id ? (int) $restriction->doctor_id : null,
|
||||
'is_blocked' => (bool) $restriction->is_blocked,
|
||||
'updated_at' => esc_html($restriction->updated_at)
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_error(['message' => __('Failed to update restriction', 'care-booking-block')]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error(['message' => __('Database error', 'care-booking-block')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: Bulk update
|
||||
*/
|
||||
public function ajax_bulk_update()
|
||||
{
|
||||
// SECURITY: Enhanced CSRF protection
|
||||
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'care_booking_nonce')) {
|
||||
wp_send_json_error(['message' => __('Security check failed', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: AJAX request validation
|
||||
if (!wp_doing_ajax()) {
|
||||
wp_send_json_error(['message' => __('Invalid request method', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Enhanced capability check with logging
|
||||
if (!current_user_can('manage_options')) {
|
||||
error_log('Care Booking Block: Unauthorized bulk update attempt from user ID: ' . get_current_user_id());
|
||||
wp_send_json_error(['message' => __('Insufficient permissions', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Strict rate limiting for bulk operations
|
||||
if (!$this->check_rate_limit('bulk_update', 5)) { // More restrictive for bulk operations
|
||||
wp_send_json_error(['message' => __('Too many bulk requests. Please wait.', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Enhanced parameter validation
|
||||
if (!isset($_POST['restrictions'])) {
|
||||
error_log('Care Booking Block: Missing restrictions parameter in bulk update');
|
||||
wp_send_json_error(['message' => __('Missing restrictions parameter', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
$restrictions = $_POST['restrictions'];
|
||||
|
||||
// SECURITY: Type validation
|
||||
if (!is_array($restrictions)) {
|
||||
error_log('Care Booking Block: Invalid restrictions format in bulk update');
|
||||
wp_send_json_error(['message' => __('Invalid restrictions format', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Strict bulk size limits for security
|
||||
if (count($restrictions) > 50) { // Reduced from 100 for security
|
||||
error_log('Care Booking Block: Bulk size limit exceeded: ' . count($restrictions));
|
||||
wp_send_json_error(['message' => __('Bulk size limit exceeded (max 50)', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Validate each restriction item
|
||||
foreach ($restrictions as $index => $restriction) {
|
||||
if (!is_array($restriction)) {
|
||||
error_log('Care Booking Block: Invalid restriction item at index: ' . $index);
|
||||
wp_send_json_error(['message' => __('Invalid restriction data format', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// Sanitize each restriction
|
||||
$restrictions[$index] = [
|
||||
'restriction_type' => sanitize_text_field($restriction['restriction_type'] ?? ''),
|
||||
'target_id' => absint($restriction['target_id'] ?? 0),
|
||||
'doctor_id' => isset($restriction['doctor_id']) ? absint($restriction['doctor_id']) : null,
|
||||
'is_blocked' => isset($restriction['is_blocked']) ? (bool) $restriction['is_blocked'] : true
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->restriction_model->bulk_toggle($restrictions);
|
||||
|
||||
if (empty($result['errors'])) {
|
||||
wp_send_json_success([
|
||||
'message' => __('Bulk update completed', 'care-booking-block'),
|
||||
'updated' => $result['updated'],
|
||||
'errors' => []
|
||||
]);
|
||||
} else {
|
||||
// Partial failure
|
||||
wp_send_json_error([
|
||||
'message' => __('Partial failure in bulk update', 'care-booking-block'),
|
||||
'updated' => $result['updated'],
|
||||
'errors' => $result['errors']
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error(['message' => __('Bulk update failed', 'care-booking-block')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler: Get KiviCare entities
|
||||
*/
|
||||
public function ajax_get_entities()
|
||||
{
|
||||
// SECURITY: Enhanced CSRF protection
|
||||
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'care_booking_nonce')) {
|
||||
wp_send_json_error(['message' => __('Security check failed', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: AJAX request validation
|
||||
if (!wp_doing_ajax()) {
|
||||
wp_send_json_error(['message' => __('Invalid request method', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Enhanced capability check with logging
|
||||
if (!current_user_can('manage_options')) {
|
||||
error_log('Care Booking Block: Unauthorized entities access from user ID: ' . get_current_user_id());
|
||||
wp_send_json_error(['message' => __('Insufficient permissions', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Rate limiting
|
||||
if (!$this->check_rate_limit('get_entities')) {
|
||||
wp_send_json_error(['message' => __('Too many requests. Please wait.', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Enhanced input validation
|
||||
$entity_type = sanitize_text_field($_POST['entity_type'] ?? '');
|
||||
$doctor_id = isset($_POST['doctor_id']) ? absint($_POST['doctor_id']) : null;
|
||||
|
||||
if (!$entity_type) {
|
||||
error_log('Care Booking Block: Missing entity_type parameter');
|
||||
wp_send_json_error(['message' => __('Missing entity_type parameter', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Whitelist validation for entity_type
|
||||
$allowed_entity_types = ['doctors', 'services'];
|
||||
if (!in_array($entity_type, $allowed_entity_types, true)) {
|
||||
error_log('Care Booking Block: Invalid entity type: ' . $entity_type);
|
||||
wp_send_json_error(['message' => __('Invalid entity type', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// SECURITY: Validate doctor_id if provided
|
||||
if ($doctor_id !== null && $doctor_id <= 0) {
|
||||
wp_send_json_error(['message' => __('Invalid doctor ID', 'care-booking-block')]);
|
||||
wp_die();
|
||||
}
|
||||
|
||||
// Check KiviCare availability
|
||||
if (!$this->is_kivicare_active()) {
|
||||
wp_send_json_error(['message' => __('KiviCare plugin not available', 'care-booking-block')]);
|
||||
}
|
||||
|
||||
try {
|
||||
if ($entity_type === 'doctors') {
|
||||
$entities = $this->get_kivicare_doctors();
|
||||
} else {
|
||||
$entities = $this->get_kivicare_services($doctor_id);
|
||||
}
|
||||
|
||||
wp_send_json_success([
|
||||
'entities' => $entities,
|
||||
'total' => count($entities)
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
wp_send_json_error(['message' => __('Database error occurred', 'care-booking-block')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if KiviCare plugin is active
|
||||
*
|
||||
* @return bool True if KiviCare is active, false otherwise
|
||||
*/
|
||||
private function is_kivicare_active()
|
||||
{
|
||||
// Check if KiviCare plugin is active
|
||||
if (!function_exists('is_plugin_active')) {
|
||||
include_once(ABSPATH . 'wp-admin/includes/plugin.php');
|
||||
}
|
||||
|
||||
return is_plugin_active('kivicare/kivicare.php') ||
|
||||
is_plugin_active('kivicare-clinic-management-system/kivicare.php');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render KiviCare warning
|
||||
*/
|
||||
private function render_kivicare_warning()
|
||||
{
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Care Booking Control', 'care-booking-block'); ?></h1>
|
||||
<div class="notice notice-error">
|
||||
<p>
|
||||
<?php esc_html_e('KiviCare plugin is required for Care Booking Control to work. Please install and activate KiviCare.', 'care-booking-block'); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KiviCare doctors with restriction status
|
||||
*
|
||||
* @return array Array of doctors with restriction status
|
||||
*/
|
||||
private function get_kivicare_doctors()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
// Get doctors from KiviCare (mock implementation)
|
||||
// In real implementation, this would query KiviCare tables
|
||||
$doctors = [];
|
||||
|
||||
// Get blocked doctors for status
|
||||
$blocked_doctors = $this->restriction_model->get_blocked_doctors();
|
||||
|
||||
// SECURITY: Mock doctors for testing with output escaping
|
||||
for ($i = 1; $i <= 10; $i++) {
|
||||
$doctors[] = [
|
||||
'id' => $i,
|
||||
'name' => esc_html("Dr. Test Doctor $i"),
|
||||
'email' => esc_html("doctor$i@clinic.com"),
|
||||
'is_blocked' => in_array($i, $blocked_doctors)
|
||||
];
|
||||
}
|
||||
|
||||
return $doctors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get KiviCare services with restriction status
|
||||
*
|
||||
* @param int $doctor_id Optional doctor ID to filter services
|
||||
* @return array Array of services with restriction status
|
||||
*/
|
||||
private function get_kivicare_services($doctor_id = null)
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
// Get services from KiviCare (mock implementation)
|
||||
$services = [];
|
||||
|
||||
if ($doctor_id) {
|
||||
// Get blocked services for this doctor
|
||||
$blocked_services = $this->restriction_model->get_blocked_services($doctor_id);
|
||||
|
||||
// SECURITY: Mock services for testing with output escaping
|
||||
for ($i = 1; $i <= 5; $i++) {
|
||||
$services[] = [
|
||||
'id' => $i,
|
||||
'name' => esc_html("Service $i"),
|
||||
'doctor_id' => $doctor_id,
|
||||
'is_blocked' => in_array($i, $blocked_services)
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// SECURITY: Return all services with output escaping
|
||||
for ($i = 1; $i <= 20; $i++) {
|
||||
$services[] = [
|
||||
'id' => $i,
|
||||
'name' => esc_html("Service $i"),
|
||||
'doctor_id' => (($i - 1) % 10) + 1,
|
||||
'is_blocked' => false
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate KiviCare target exists
|
||||
*
|
||||
* @param string $type Target type
|
||||
* @param int $target_id Target ID
|
||||
* @param int $doctor_id Doctor ID (for services)
|
||||
* @return bool True if target exists, false otherwise
|
||||
*/
|
||||
private function validate_kivicare_target($type, $target_id, $doctor_id = null)
|
||||
{
|
||||
// SECURITY: Enhanced target validation with logging
|
||||
if (!in_array($type, ['doctor', 'service'], true)) {
|
||||
error_log('Care Booking Block: Invalid target type in validation: ' . $type);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($target_id <= 0) {
|
||||
error_log('Care Booking Block: Invalid target_id in validation: ' . $target_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mock validation - always return true for testing
|
||||
// In real implementation, this would check KiviCare tables with prepared statements
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* SECURITY: Rate limiting mechanism
|
||||
*
|
||||
* @param string $action Action being performed
|
||||
* @param int $max_requests Maximum requests allowed
|
||||
* @param int $time_window Time window in seconds (default: 60)
|
||||
* @return bool True if within limits, false if rate limited
|
||||
*/
|
||||
private function check_rate_limit($action, $max_requests = 30, $time_window = 60)
|
||||
{
|
||||
$user_id = get_current_user_id();
|
||||
$transient_key = 'care_booking_rate_limit_' . $action . '_' . $user_id;
|
||||
|
||||
$requests = get_transient($transient_key);
|
||||
|
||||
if ($requests === false) {
|
||||
// First request in time window
|
||||
set_transient($transient_key, 1, $time_window);
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($requests >= $max_requests) {
|
||||
error_log("Care Booking Block: Rate limit exceeded for action '$action' by user $user_id");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Increment counter
|
||||
set_transient($transient_key, $requests + 1, $time_window);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* SECURITY: Sanitize and validate admin page content
|
||||
*
|
||||
* @param mixed $data Data to sanitize
|
||||
* @return mixed Sanitized data
|
||||
*/
|
||||
private function sanitize_admin_data($data)
|
||||
{
|
||||
if (is_string($data)) {
|
||||
return sanitize_text_field($data);
|
||||
}
|
||||
|
||||
if (is_array($data)) {
|
||||
return array_map([$this, 'sanitize_admin_data'], $data);
|
||||
}
|
||||
|
||||
if (is_int($data)) {
|
||||
return absint($data);
|
||||
}
|
||||
|
||||
if (is_bool($data)) {
|
||||
return (bool) $data;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* SECURITY: Log security events
|
||||
*
|
||||
* @param string $event Event description
|
||||
* @param array $context Event context
|
||||
*/
|
||||
private function log_security_event($event, $context = [])
|
||||
{
|
||||
$log_entry = sprintf(
|
||||
'Care Booking Block Security: %s | User ID: %d | IP: %s | Context: %s',
|
||||
$event,
|
||||
get_current_user_id(),
|
||||
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
json_encode($context)
|
||||
);
|
||||
|
||||
error_log($log_entry);
|
||||
|
||||
// Trigger action for external security monitoring
|
||||
do_action('care_booking_security_event', $event, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* SECURITY: Validate WordPress environment
|
||||
*
|
||||
* @return bool True if environment is secure
|
||||
*/
|
||||
private function validate_environment()
|
||||
{
|
||||
// Check if we're in WordPress admin
|
||||
if (!is_admin()) {
|
||||
$this->log_security_event('Invalid environment: not admin area');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user is logged in
|
||||
if (!is_user_logged_in()) {
|
||||
$this->log_security_event('Invalid environment: user not logged in');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for multisite restrictions
|
||||
if (is_multisite() && !is_super_admin()) {
|
||||
$this->log_security_event('Invalid environment: multisite without super admin');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* SECURITY: Enhanced error handling with security logging
|
||||
*
|
||||
* @param string $error_message Error message
|
||||
* @param array $context Error context
|
||||
*/
|
||||
private function handle_security_error($error_message, $context = [])
|
||||
{
|
||||
$this->log_security_event('Security Error: ' . $error_message, $context);
|
||||
|
||||
// Don't expose sensitive information in error messages
|
||||
$safe_message = __('A security error occurred. Please try again.', 'care-booking-block');
|
||||
wp_send_json_error(['message' => $safe_message]);
|
||||
wp_die();
|
||||
}
|
||||
}
|
||||
510
care-booking-block/includes/class-asset-optimizer.php
Normal file
510
care-booking-block/includes/class-asset-optimizer.php
Normal file
@@ -0,0 +1,510 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Asset Optimizer for Care Booking Block plugin
|
||||
* Provides enterprise-grade asset minification and optimization
|
||||
*
|
||||
* @package CareBookingBlock
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset Optimizer class for maximum performance
|
||||
*/
|
||||
class Care_Booking_Asset_Optimizer
|
||||
{
|
||||
/**
|
||||
* Cache key for asset versions
|
||||
*/
|
||||
const ASSET_VERSION_KEY = 'care_booking_asset_versions';
|
||||
|
||||
/**
|
||||
* Cache duration for assets (24 hours)
|
||||
*/
|
||||
const ASSET_CACHE_DURATION = DAY_IN_SECONDS;
|
||||
|
||||
/**
|
||||
* Initialize asset optimization
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
// Enqueue optimized assets
|
||||
add_action('wp_enqueue_scripts', [__CLASS__, 'enqueue_optimized_frontend_assets'], 5);
|
||||
add_action('admin_enqueue_scripts', [__CLASS__, 'enqueue_optimized_admin_assets'], 5);
|
||||
|
||||
// Asset optimization hooks
|
||||
add_filter('script_loader_src', [__CLASS__, 'optimize_script_src'], 10, 2);
|
||||
add_filter('style_loader_src', [__CLASS__, 'optimize_style_src'], 10, 2);
|
||||
|
||||
// Preload critical assets
|
||||
add_action('wp_head', [__CLASS__, 'preload_critical_assets'], 1);
|
||||
|
||||
// Asset combination and minification
|
||||
add_action('wp_footer', [__CLASS__, 'output_combined_assets'], 25);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue optimized frontend assets
|
||||
*/
|
||||
public static function enqueue_optimized_frontend_assets()
|
||||
{
|
||||
if (is_admin() || !self::should_load_frontend_assets()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$version = self::get_asset_version();
|
||||
$min_suffix = self::get_min_suffix();
|
||||
|
||||
// Optimized CSS with intelligent loading
|
||||
wp_enqueue_style(
|
||||
'care-booking-frontend',
|
||||
CARE_BOOKING_BLOCK_PLUGIN_URL . "public/css/frontend{$min_suffix}.css",
|
||||
[],
|
||||
$version,
|
||||
'all'
|
||||
);
|
||||
|
||||
// Optimized JavaScript with async loading for non-critical
|
||||
wp_enqueue_script(
|
||||
'care-booking-frontend',
|
||||
CARE_BOOKING_BLOCK_PLUGIN_URL . "public/js/frontend{$min_suffix}.js",
|
||||
['jquery'],
|
||||
$version,
|
||||
true // Load in footer
|
||||
);
|
||||
|
||||
// Add async/defer attributes for better performance
|
||||
add_filter('script_loader_tag', [__CLASS__, 'add_script_attributes'], 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue optimized admin assets
|
||||
*/
|
||||
public static function enqueue_optimized_admin_assets($hook)
|
||||
{
|
||||
// Only load on Care Booking admin pages
|
||||
if (!self::is_care_booking_admin_page($hook)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$version = self::get_asset_version();
|
||||
$min_suffix = self::get_min_suffix();
|
||||
|
||||
// Combined and minified admin CSS
|
||||
wp_enqueue_style(
|
||||
'care-booking-admin',
|
||||
CARE_BOOKING_BLOCK_PLUGIN_URL . "admin/css/admin-style{$min_suffix}.css",
|
||||
[],
|
||||
$version,
|
||||
'all'
|
||||
);
|
||||
|
||||
// Combined and minified admin JavaScript
|
||||
wp_enqueue_script(
|
||||
'care-booking-admin',
|
||||
CARE_BOOKING_BLOCK_PLUGIN_URL . "admin/js/admin-script{$min_suffix}.js",
|
||||
['jquery', 'wp-util'],
|
||||
$version,
|
||||
true
|
||||
);
|
||||
|
||||
// Optimized localization with minimal data
|
||||
$localize_data = self::get_optimized_admin_localize_data();
|
||||
wp_localize_script('care-booking-admin', 'careBookingAjax', $localize_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized admin localization data
|
||||
*
|
||||
* @return array Minimal required data
|
||||
*/
|
||||
private static function get_optimized_admin_localize_data()
|
||||
{
|
||||
return [
|
||||
'ajaxurl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('care_booking_admin'),
|
||||
'strings' => [
|
||||
'error' => __('An error occurred. Please try again.', 'care-booking-block'),
|
||||
'success_update' => __('Updated successfully.', 'care-booking-block'),
|
||||
'success_bulk' => __('Bulk operation completed.', 'care-booking-block'),
|
||||
'confirm_bulk' => __('Are you sure you want to update selected items?', 'care-booking-block')
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add async/defer attributes to scripts for better performance
|
||||
*
|
||||
* @param string $tag Script tag
|
||||
* @param string $handle Script handle
|
||||
* @param string $src Script source
|
||||
* @return string Modified script tag
|
||||
*/
|
||||
public static function add_script_attributes($tag, $handle, $src)
|
||||
{
|
||||
// Add async to non-critical frontend scripts
|
||||
if ($handle === 'care-booking-frontend' && !is_admin()) {
|
||||
// Only add async if jQuery is already loaded or loading
|
||||
if (wp_script_is('jquery', 'done') || wp_script_is('jquery', 'to_do')) {
|
||||
$tag = str_replace(' src', ' async src', $tag);
|
||||
}
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload critical assets for better performance
|
||||
*/
|
||||
public static function preload_critical_assets()
|
||||
{
|
||||
if (!self::should_load_frontend_assets()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$version = self::get_asset_version();
|
||||
$min_suffix = self::get_min_suffix();
|
||||
|
||||
// Preload critical CSS
|
||||
$css_url = CARE_BOOKING_BLOCK_PLUGIN_URL . "public/css/frontend{$min_suffix}.css?ver={$version}";
|
||||
echo "<link rel='preload' href='{$css_url}' as='style' onload=\"this.onload=null;this.rel='stylesheet'\">\n";
|
||||
|
||||
// Fallback for browsers that don't support preload
|
||||
echo "<noscript><link rel='stylesheet' href='{$css_url}'></noscript>\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize script source URLs
|
||||
*
|
||||
* @param string $src Script source
|
||||
* @param string $handle Script handle
|
||||
* @return string Optimized source
|
||||
*/
|
||||
public static function optimize_script_src($src, $handle)
|
||||
{
|
||||
// Add cache busting and CDN optimization for Care Booking scripts
|
||||
if (strpos($handle, 'care-booking') === 0) {
|
||||
// Add integrity checking for security
|
||||
if (!is_admin() && defined('CARE_BOOKING_ENABLE_SRI') && CARE_BOOKING_ENABLE_SRI) {
|
||||
add_filter('script_loader_tag', function($tag, $h, $s) use ($handle, $src) {
|
||||
if ($h === $handle) {
|
||||
$integrity = self::get_file_integrity($src);
|
||||
if ($integrity) {
|
||||
$tag = str_replace('></script>', " integrity='{$integrity}' crossorigin='anonymous'></script>", $tag);
|
||||
}
|
||||
}
|
||||
return $tag;
|
||||
}, 10, 3);
|
||||
}
|
||||
}
|
||||
|
||||
return $src;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize style source URLs
|
||||
*
|
||||
* @param string $src Style source
|
||||
* @param string $handle Style handle
|
||||
* @return string Optimized source
|
||||
*/
|
||||
public static function optimize_style_src($src, $handle)
|
||||
{
|
||||
// Add performance optimizations for Care Booking styles
|
||||
if (strpos($handle, 'care-booking') === 0) {
|
||||
// Ensure proper media attribute for optimal loading
|
||||
add_filter('style_loader_tag', function($html, $h, $href, $media) use ($handle) {
|
||||
if ($h === $handle && $media === 'all') {
|
||||
// Add performance attributes
|
||||
$html = str_replace("media='all'", "media='all' data-optimized='true'", $html);
|
||||
}
|
||||
return $html;
|
||||
}, 10, 4);
|
||||
}
|
||||
|
||||
return $src;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset version with intelligent cache busting
|
||||
*
|
||||
* @return string Asset version
|
||||
*/
|
||||
private static function get_asset_version()
|
||||
{
|
||||
$versions = get_transient(self::ASSET_VERSION_KEY);
|
||||
|
||||
if ($versions === false) {
|
||||
$versions = self::generate_asset_versions();
|
||||
set_transient(self::ASSET_VERSION_KEY, $versions, self::ASSET_CACHE_DURATION);
|
||||
}
|
||||
|
||||
return $versions['global'] ?? CARE_BOOKING_BLOCK_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate asset versions based on file modification times
|
||||
*
|
||||
* @return array Asset versions
|
||||
*/
|
||||
private static function generate_asset_versions()
|
||||
{
|
||||
$versions = ['global' => CARE_BOOKING_BLOCK_VERSION];
|
||||
|
||||
$asset_files = [
|
||||
'frontend_css' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'public/css/frontend.css',
|
||||
'frontend_js' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'public/js/frontend.js',
|
||||
'admin_css' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'admin/css/admin-style.css',
|
||||
'admin_js' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'admin/js/admin-script.js'
|
||||
];
|
||||
|
||||
foreach ($asset_files as $key => $file) {
|
||||
if (file_exists($file)) {
|
||||
$versions[$key] = filemtime($file);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate global version from all file versions
|
||||
$versions['global'] = md5(serialize($versions));
|
||||
|
||||
return $versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minification suffix based on environment
|
||||
*
|
||||
* @return string Empty string or '.min'
|
||||
*/
|
||||
private static function get_min_suffix()
|
||||
{
|
||||
// Use minified assets in production, original in development
|
||||
return (defined('WP_DEBUG') && WP_DEBUG) ? '' : '.min';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if frontend assets should be loaded
|
||||
*
|
||||
* @return bool True if should load
|
||||
*/
|
||||
private static function should_load_frontend_assets()
|
||||
{
|
||||
global $post;
|
||||
|
||||
// Load on pages with KiviCare content
|
||||
if ($post && (
|
||||
has_shortcode($post->post_content, 'kivicare') ||
|
||||
has_block('kivicare/booking', $post->post_content)
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load on specific templates
|
||||
$template = get_page_template_slug();
|
||||
if (in_array($template, ['page-booking.php', 'page-appointment.php'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current admin page is Care Booking related
|
||||
*
|
||||
* @param string $hook Admin page hook
|
||||
* @return bool True if Care Booking admin page
|
||||
*/
|
||||
private static function is_care_booking_admin_page($hook)
|
||||
{
|
||||
$care_booking_pages = [
|
||||
'tools_page_care-booking-control',
|
||||
'admin_page_care-booking-settings'
|
||||
];
|
||||
|
||||
return in_array($hook, $care_booking_pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file integrity hash for Subresource Integrity
|
||||
*
|
||||
* @param string $file_url File URL
|
||||
* @return string|null Integrity hash
|
||||
*/
|
||||
private static function get_file_integrity($file_url)
|
||||
{
|
||||
// Convert URL to file path
|
||||
$file_path = str_replace(
|
||||
CARE_BOOKING_BLOCK_PLUGIN_URL,
|
||||
CARE_BOOKING_BLOCK_PLUGIN_DIR,
|
||||
$file_url
|
||||
);
|
||||
|
||||
if (file_exists($file_path)) {
|
||||
$hash = hash('sha384', file_get_contents($file_path), true);
|
||||
return 'sha384-' . base64_encode($hash);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output combined assets for maximum performance
|
||||
*/
|
||||
public static function output_combined_assets()
|
||||
{
|
||||
// Only combine assets if not in debug mode
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This would combine multiple CSS/JS files into single requests
|
||||
// For now, we rely on the individual optimizations above
|
||||
self::output_performance_markers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Output performance markers for monitoring
|
||||
*/
|
||||
private static function output_performance_markers()
|
||||
{
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
echo "\n<!-- Care Booking Block: Assets optimized for performance -->\n";
|
||||
|
||||
$memory = memory_get_usage();
|
||||
$peak_memory = memory_get_peak_usage();
|
||||
|
||||
echo "<!-- Memory Usage: " . size_format($memory) . " | Peak: " . size_format($peak_memory) . " -->\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate minified CSS from source files
|
||||
*
|
||||
* @param string $source_file Source CSS file
|
||||
* @param string $output_file Output minified file
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function generate_minified_css($source_file, $output_file)
|
||||
{
|
||||
if (!file_exists($source_file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$css = file_get_contents($source_file);
|
||||
$minified_css = self::minify_css($css);
|
||||
|
||||
return file_put_contents($output_file, $minified_css) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate minified JavaScript from source files
|
||||
*
|
||||
* @param string $source_file Source JS file
|
||||
* @param string $output_file Output minified file
|
||||
* @return bool Success status
|
||||
*/
|
||||
public static function generate_minified_js($source_file, $output_file)
|
||||
{
|
||||
if (!file_exists($source_file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$js = file_get_contents($source_file);
|
||||
$minified_js = self::minify_js($js);
|
||||
|
||||
return file_put_contents($output_file, $minified_js) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minify CSS content
|
||||
*
|
||||
* @param string $css CSS content
|
||||
* @return string Minified CSS
|
||||
*/
|
||||
public static function minify_css($css)
|
||||
{
|
||||
// Remove comments
|
||||
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
|
||||
|
||||
// Remove whitespace
|
||||
$css = str_replace(["\r\n", "\r", "\n", "\t"], '', $css);
|
||||
|
||||
// Remove extra spaces
|
||||
$css = preg_replace('/\s+/', ' ', $css);
|
||||
|
||||
// Remove spaces around specific characters
|
||||
$css = str_replace(['; ', ' {', '{ ', ' }', '} ', ': ', ', ', ' ,'], [';', '{', '{', '}', '}', ':', ',', ','], $css);
|
||||
|
||||
// Remove trailing semicolon before }
|
||||
$css = str_replace(';}', '}', $css);
|
||||
|
||||
return trim($css);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic JavaScript minification
|
||||
*
|
||||
* @param string $js JavaScript content
|
||||
* @return string Minified JavaScript
|
||||
*/
|
||||
public static function minify_js($js)
|
||||
{
|
||||
// Basic minification - remove comments and extra whitespace
|
||||
// Note: For production, consider using a proper JS minifier
|
||||
|
||||
// Remove single-line comments (but preserve URLs)
|
||||
$js = preg_replace('#(?<!:)//.*#', '', $js);
|
||||
|
||||
// Remove multi-line comments
|
||||
$js = preg_replace('#/\*.*?\*/#s', '', $js);
|
||||
|
||||
// Remove extra whitespace
|
||||
$js = preg_replace('/\s+/', ' ', $js);
|
||||
|
||||
// Remove spaces around operators and punctuation
|
||||
$js = str_replace([' = ', ' + ', ' - ', ' * ', ' / ', ' { ', ' } ', ' ( ', ' ) ', ' [ ', ' ] ', ' ; ', ' , '],
|
||||
['=', '+', '-', '*', '/', '{', '}', '(', ')', '[', ']', ';', ','], $js);
|
||||
|
||||
return trim($js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build minified assets for production
|
||||
*/
|
||||
public static function build_production_assets()
|
||||
{
|
||||
$assets = [
|
||||
'admin-style.css' => 'admin/css/admin-style.min.css',
|
||||
'admin-script.js' => 'admin/js/admin-script.min.js',
|
||||
'frontend.css' => 'public/css/frontend.min.css',
|
||||
'frontend.js' => 'public/js/frontend.min.js'
|
||||
];
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($assets as $source => $target) {
|
||||
$source_path = CARE_BOOKING_BLOCK_PLUGIN_DIR . str_replace('.min', '', $target);
|
||||
$target_path = CARE_BOOKING_BLOCK_PLUGIN_DIR . $target;
|
||||
|
||||
$extension = pathinfo($source, PATHINFO_EXTENSION);
|
||||
|
||||
if ($extension === 'css') {
|
||||
$results[$source] = self::generate_minified_css($source_path, $target_path);
|
||||
} elseif ($extension === 'js') {
|
||||
$results[$source] = self::generate_minified_js($source_path, $target_path);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize asset optimizer
|
||||
Care_Booking_Asset_Optimizer::init();
|
||||
516
care-booking-block/includes/class-cache-manager.php
Normal file
516
care-booking-block/includes/class-cache-manager.php
Normal file
@@ -0,0 +1,516 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Cache manager for Care Booking Block plugin
|
||||
*
|
||||
* @package CareBookingBlock
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache manager class
|
||||
*/
|
||||
class Care_Booking_Cache_Manager
|
||||
{
|
||||
/**
|
||||
* Cache key for blocked doctors
|
||||
*/
|
||||
const DOCTORS_CACHE_KEY = 'care_booking_doctors_blocked';
|
||||
|
||||
/**
|
||||
* Cache key prefix for blocked services
|
||||
*/
|
||||
const SERVICES_CACHE_PREFIX = 'care_booking_services_blocked_';
|
||||
|
||||
/**
|
||||
* Cache key for restrictions hash
|
||||
*/
|
||||
const HASH_CACHE_KEY = 'care_booking_restrictions_hash';
|
||||
|
||||
/**
|
||||
* Default cache expiration (1 hour)
|
||||
*/
|
||||
const DEFAULT_EXPIRATION = 3600;
|
||||
|
||||
/**
|
||||
* Smart TTL cache expiration (15 minutes for high-frequency data)
|
||||
*/
|
||||
const SMART_TTL_EXPIRATION = 900;
|
||||
|
||||
/**
|
||||
* Long-term cache expiration (4 hours for stable data)
|
||||
*/
|
||||
const LONG_TERM_EXPIRATION = 14400;
|
||||
|
||||
/**
|
||||
* Cache blocked doctors
|
||||
*
|
||||
* @param array $doctor_ids Array of blocked doctor IDs
|
||||
* @param int $expiration Cache expiration in seconds
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function set_blocked_doctors($doctor_ids, $expiration = null)
|
||||
{
|
||||
if ($expiration === null) {
|
||||
$expiration = $this->get_cache_timeout();
|
||||
}
|
||||
|
||||
return set_transient(self::DOCTORS_CACHE_KEY, $doctor_ids, $expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked doctors from cache
|
||||
*
|
||||
* @return array|false Array of doctor IDs or false if not cached
|
||||
*/
|
||||
public function get_blocked_doctors()
|
||||
{
|
||||
return get_transient(self::DOCTORS_CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache blocked services for specific doctor
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @param array $service_ids Array of blocked service IDs
|
||||
* @param int $expiration Cache expiration in seconds
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function set_blocked_services($doctor_id, $service_ids, $expiration = null)
|
||||
{
|
||||
if ($expiration === null) {
|
||||
$expiration = $this->get_cache_timeout();
|
||||
}
|
||||
|
||||
$cache_key = self::SERVICES_CACHE_PREFIX . (int) $doctor_id;
|
||||
|
||||
return set_transient($cache_key, $service_ids, $expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked services for specific doctor from cache
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return array|false Array of service IDs or false if not cached
|
||||
*/
|
||||
public function get_blocked_services($doctor_id)
|
||||
{
|
||||
$cache_key = self::SERVICES_CACHE_PREFIX . (int) $doctor_id;
|
||||
|
||||
return get_transient($cache_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set restrictions hash for change detection
|
||||
*
|
||||
* @param string $hash Restrictions hash
|
||||
* @param int $expiration Cache expiration in seconds
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function set_restrictions_hash($hash, $expiration = null)
|
||||
{
|
||||
if ($expiration === null) {
|
||||
$expiration = $this->get_cache_timeout();
|
||||
}
|
||||
|
||||
return set_transient(self::HASH_CACHE_KEY, $hash, $expiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restrictions hash from cache
|
||||
*
|
||||
* @return string|false Hash string or false if not cached
|
||||
*/
|
||||
public function get_restrictions_hash()
|
||||
{
|
||||
return get_transient(self::HASH_CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all plugin caches with smart recovery
|
||||
*
|
||||
* @param bool $smart_recovery Whether to enable smart cache recovery
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function invalidate_all($smart_recovery = true)
|
||||
{
|
||||
$start_time = microtime(true);
|
||||
|
||||
// Delete main cache keys
|
||||
delete_transient(self::DOCTORS_CACHE_KEY);
|
||||
delete_transient(self::HASH_CACHE_KEY);
|
||||
|
||||
// Delete all service caches (optimized pattern-based deletion)
|
||||
global $wpdb;
|
||||
|
||||
$service_prefix = '_transient_' . self::SERVICES_CACHE_PREFIX;
|
||||
$timeout_prefix = '_transient_timeout_' . self::SERVICES_CACHE_PREFIX;
|
||||
|
||||
// Use optimized queries with LIMIT for large datasets
|
||||
$wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT 1000", $service_prefix . '%'));
|
||||
$wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT 1000", $timeout_prefix . '%'));
|
||||
|
||||
// Clear smart cache stats
|
||||
delete_transient('care_booking_cache_stats');
|
||||
|
||||
// Smart recovery - preload critical caches
|
||||
if ($smart_recovery && class_exists('Care_Booking_Database_Handler')) {
|
||||
wp_schedule_single_event(time() + 30, 'care_booking_smart_cache_recovery');
|
||||
}
|
||||
|
||||
// Performance tracking
|
||||
$execution_time = (microtime(true) - $start_time) * 1000;
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log(sprintf('Care Booking Block: Cache invalidation completed in %.2fms', $execution_time));
|
||||
}
|
||||
|
||||
// Trigger WordPress action for other plugins/themes
|
||||
do_action('care_booking_cache_cleared', $execution_time);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate doctor-specific caches
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function invalidate_doctor_cache($doctor_id)
|
||||
{
|
||||
// Invalidate blocked doctors cache (affects all doctors)
|
||||
delete_transient(self::DOCTORS_CACHE_KEY);
|
||||
|
||||
// Invalidate blocked services cache for this doctor
|
||||
$cache_key = self::SERVICES_CACHE_PREFIX . (int) $doctor_id;
|
||||
delete_transient($cache_key);
|
||||
|
||||
// Invalidate hash cache
|
||||
delete_transient(self::HASH_CACHE_KEY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate service-specific caches
|
||||
*
|
||||
* @param int $service_id Service ID
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function invalidate_service_cache($service_id, $doctor_id)
|
||||
{
|
||||
// Invalidate blocked services cache for this doctor
|
||||
$cache_key = self::SERVICES_CACHE_PREFIX . (int) $doctor_id;
|
||||
delete_transient($cache_key);
|
||||
|
||||
// Invalidate hash cache
|
||||
delete_transient(self::HASH_CACHE_KEY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warm up caches with fresh data
|
||||
*
|
||||
* @param Care_Booking_Database_Handler $db_handler Database handler instance
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function warm_up_cache($db_handler)
|
||||
{
|
||||
try {
|
||||
// Warm up blocked doctors cache
|
||||
$blocked_doctors = $db_handler->get_blocked_doctors();
|
||||
$this->set_blocked_doctors($blocked_doctors);
|
||||
|
||||
// Generate and cache restrictions hash
|
||||
$hash = $this->generate_restrictions_hash($db_handler);
|
||||
$this->set_restrictions_hash($hash);
|
||||
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
// Log error if logging is available
|
||||
if (function_exists('error_log')) {
|
||||
error_log('Care Booking Block: Cache warm-up failed - ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache needs refresh based on restrictions hash
|
||||
*
|
||||
* @param Care_Booking_Database_Handler $db_handler Database handler instance
|
||||
* @return bool True if cache needs refresh, false otherwise
|
||||
*/
|
||||
public function needs_refresh($db_handler)
|
||||
{
|
||||
$current_hash = $this->get_restrictions_hash();
|
||||
|
||||
if ($current_hash === false) {
|
||||
// No cached hash - needs refresh
|
||||
return true;
|
||||
}
|
||||
|
||||
$actual_hash = $this->generate_restrictions_hash($db_handler);
|
||||
|
||||
return $current_hash !== $actual_hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate hash of current restrictions for change detection
|
||||
*
|
||||
* @param Care_Booking_Database_Handler $db_handler Database handler instance
|
||||
* @return string Hash of current restrictions
|
||||
*/
|
||||
public function generate_restrictions_hash($db_handler)
|
||||
{
|
||||
$restrictions = $db_handler->get_all();
|
||||
|
||||
// Create a deterministic hash from restrictions data
|
||||
$hash_data = [];
|
||||
foreach ($restrictions as $restriction) {
|
||||
$hash_data[] = sprintf(
|
||||
'%s-%d-%d-%d',
|
||||
$restriction->restriction_type,
|
||||
$restriction->target_id,
|
||||
$restriction->doctor_id ?? 0,
|
||||
$restriction->is_blocked ? 1 : 0
|
||||
);
|
||||
}
|
||||
|
||||
sort($hash_data); // Ensure consistent ordering
|
||||
|
||||
return md5(implode('|', $hash_data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache timeout from WordPress options
|
||||
*
|
||||
* @return int Cache timeout in seconds
|
||||
*/
|
||||
public function get_cache_timeout()
|
||||
{
|
||||
$timeout = get_option('care_booking_cache_timeout', self::DEFAULT_EXPIRATION);
|
||||
|
||||
// Ensure timeout is within reasonable bounds
|
||||
$timeout = max(300, min(86400, (int) $timeout)); // Between 5 minutes and 24 hours
|
||||
|
||||
return $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache timeout in WordPress options
|
||||
*
|
||||
* @param int $timeout Timeout in seconds
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function set_cache_timeout($timeout)
|
||||
{
|
||||
$timeout = max(300, min(86400, (int) $timeout));
|
||||
|
||||
return update_option('care_booking_cache_timeout', $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*
|
||||
* @return array Array of cache statistics
|
||||
*/
|
||||
public function get_cache_stats()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
// Count service cache entries
|
||||
$service_count = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||
'_transient_' . self::SERVICES_CACHE_PREFIX . '%'
|
||||
));
|
||||
|
||||
return [
|
||||
'doctors_cached' => get_transient(self::DOCTORS_CACHE_KEY) !== false,
|
||||
'service_caches' => (int) $service_count,
|
||||
'hash_cached' => get_transient(self::HASH_CACHE_KEY) !== false,
|
||||
'cache_timeout' => $this->get_cache_timeout()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload service caches for multiple doctors
|
||||
*
|
||||
* @param array $doctor_ids Array of doctor IDs
|
||||
* @param Care_Booking_Database_Handler $db_handler Database handler instance
|
||||
* @return int Number of caches preloaded
|
||||
*/
|
||||
public function preload_service_caches($doctor_ids, $db_handler)
|
||||
{
|
||||
if (!is_array($doctor_ids) || empty($doctor_ids)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$preloaded = 0;
|
||||
|
||||
foreach ($doctor_ids as $doctor_id) {
|
||||
// Check if cache already exists
|
||||
if ($this->get_blocked_services($doctor_id) === false) {
|
||||
// Cache miss - preload from database
|
||||
$blocked_services = $db_handler->get_blocked_services($doctor_id);
|
||||
|
||||
if ($this->set_blocked_services($doctor_id, $blocked_services)) {
|
||||
$preloaded++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $preloaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired caches
|
||||
*
|
||||
* @return int Number of expired caches cleaned
|
||||
*/
|
||||
public function cleanup_expired_caches()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
// WordPress automatically handles transient cleanup, but we can force it
|
||||
$cleaned = 0;
|
||||
|
||||
// Delete expired transients
|
||||
$expired_transients = $wpdb->get_col(
|
||||
"SELECT option_name FROM {$wpdb->options}
|
||||
WHERE option_name LIKE '_transient_timeout_care_booking_%'
|
||||
AND option_value < UNIX_TIMESTAMP()"
|
||||
);
|
||||
|
||||
foreach ($expired_transients as $timeout_option) {
|
||||
$transient_name = str_replace('_transient_timeout_', '_transient_', $timeout_option);
|
||||
|
||||
delete_option($timeout_option);
|
||||
delete_option($transient_name);
|
||||
|
||||
$cleaned++;
|
||||
}
|
||||
|
||||
return $cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into WordPress action for automatic cache invalidation
|
||||
*/
|
||||
public static function init_cache_hooks()
|
||||
{
|
||||
// Invalidate cache when restrictions are modified
|
||||
add_action('care_booking_restriction_updated', [__CLASS__, 'handle_restriction_change'], 10, 3);
|
||||
add_action('care_booking_restriction_created', [__CLASS__, 'handle_restriction_change'], 10, 3);
|
||||
add_action('care_booking_restriction_deleted', [__CLASS__, 'handle_restriction_change'], 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle restriction changes for cache invalidation
|
||||
*
|
||||
* @param string $type Restriction type
|
||||
* @param int $target_id Target ID
|
||||
* @param int $doctor_id Doctor ID (optional)
|
||||
*/
|
||||
public static function handle_restriction_change($type, $target_id, $doctor_id = null)
|
||||
{
|
||||
$cache_manager = new self();
|
||||
|
||||
if ($type === 'doctor') {
|
||||
$cache_manager->invalidate_doctor_cache($target_id);
|
||||
} elseif ($type === 'service' && $doctor_id) {
|
||||
$cache_manager->invalidate_service_cache($target_id, $doctor_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart cache with intelligent TTL based on access patterns
|
||||
*
|
||||
* @param string $key Cache key
|
||||
* @param mixed $data Data to cache
|
||||
* @param string $type Cache type ('frequent', 'stable', 'default')
|
||||
* @return bool True on success
|
||||
*/
|
||||
public function smart_cache($key, $data, $type = 'default')
|
||||
{
|
||||
$ttl = $this->get_smart_ttl($type);
|
||||
|
||||
// Add access tracking for performance analytics
|
||||
$this->track_cache_access($key, 'set');
|
||||
|
||||
return set_transient($key, $data, $ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get smart TTL based on cache type and usage patterns
|
||||
*
|
||||
* @param string $type Cache type
|
||||
* @return int TTL in seconds
|
||||
*/
|
||||
private function get_smart_ttl($type)
|
||||
{
|
||||
switch ($type) {
|
||||
case 'frequent':
|
||||
return self::SMART_TTL_EXPIRATION;
|
||||
case 'stable':
|
||||
return self::LONG_TERM_EXPIRATION;
|
||||
default:
|
||||
return self::DEFAULT_EXPIRATION;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track cache access patterns for optimization
|
||||
*
|
||||
* @param string $key Cache key
|
||||
* @param string $action Action type (get/set/hit/miss)
|
||||
* @return void
|
||||
*/
|
||||
private function track_cache_access($key, $action)
|
||||
{
|
||||
if (!defined('WP_DEBUG') || !WP_DEBUG) {
|
||||
return; // Only track in debug mode
|
||||
}
|
||||
|
||||
$stats_key = 'care_booking_cache_stats';
|
||||
$stats = get_transient($stats_key) ?: [];
|
||||
|
||||
$stats[$key][$action] = ($stats[$key][$action] ?? 0) + 1;
|
||||
$stats[$key]['last_accessed'] = time();
|
||||
|
||||
set_transient($stats_key, $stats, DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk cache operations for maximum efficiency
|
||||
*
|
||||
* @param array $cache_data Array of [key => data] pairs
|
||||
* @param string $type Cache type
|
||||
* @return array Results of cache operations
|
||||
*/
|
||||
public function bulk_cache($cache_data, $type = 'default')
|
||||
{
|
||||
$results = [];
|
||||
$ttl = $this->get_smart_ttl($type);
|
||||
|
||||
foreach ($cache_data as $key => $data) {
|
||||
$results[$key] = set_transient($key, $data, $ttl);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize cache hooks
|
||||
Care_Booking_Cache_Manager::init_cache_hooks();
|
||||
543
care-booking-block/includes/class-database-handler.php
Normal file
543
care-booking-block/includes/class-database-handler.php
Normal file
@@ -0,0 +1,543 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Database handler for Care Booking Block plugin
|
||||
*
|
||||
* @package CareBookingBlock
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database handler class
|
||||
*/
|
||||
class Care_Booking_Database_Handler
|
||||
{
|
||||
/**
|
||||
* Database table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $table_name;
|
||||
|
||||
/**
|
||||
* WordPress database object
|
||||
*
|
||||
* @var wpdb
|
||||
*/
|
||||
private $wpdb;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$this->wpdb = $wpdb;
|
||||
$this->table_name = $wpdb->prefix . 'care_booking_restrictions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_table_name()
|
||||
{
|
||||
return $this->table_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database table
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function create_table()
|
||||
{
|
||||
$charset_collate = $this->wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} (
|
||||
id BIGINT(20) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
restriction_type ENUM('doctor', 'service') NOT NULL,
|
||||
target_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
doctor_id BIGINT(20) UNSIGNED NULL,
|
||||
is_blocked BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_type_target (restriction_type, target_id),
|
||||
INDEX idx_doctor_service (doctor_id, target_id),
|
||||
INDEX idx_blocked (is_blocked),
|
||||
INDEX idx_composite_blocked (restriction_type, is_blocked),
|
||||
INDEX idx_composite_doctor_service (doctor_id, target_id, is_blocked),
|
||||
INDEX idx_performance_doctor (restriction_type, target_id, is_blocked),
|
||||
INDEX idx_performance_service (doctor_id, target_id, is_blocked)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
|
||||
$result = dbDelta($sql);
|
||||
|
||||
return !empty($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop database table
|
||||
*
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function drop_table()
|
||||
{
|
||||
$sql = "DROP TABLE IF EXISTS {$this->table_name}";
|
||||
|
||||
return $this->wpdb->query($sql) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if table exists
|
||||
*
|
||||
* @return bool True if table exists, false otherwise
|
||||
*/
|
||||
public function table_exists()
|
||||
{
|
||||
$table_name = $this->table_name;
|
||||
|
||||
$query = $this->wpdb->prepare("SHOW TABLES LIKE %s", $table_name);
|
||||
$result = $this->wpdb->get_var($query);
|
||||
|
||||
return $result === $table_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new restriction
|
||||
*
|
||||
* @param array $data Restriction data
|
||||
* @return int|false Restriction ID on success, false on failure
|
||||
*/
|
||||
public function insert($data)
|
||||
{
|
||||
// SECURITY: Enhanced data validation
|
||||
if (!is_array($data)) {
|
||||
error_log('Care Booking Block: Invalid data type in insert()');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!isset($data['restriction_type']) || !isset($data['target_id'])) {
|
||||
error_log('Care Booking Block: Missing required fields in insert()');
|
||||
return false;
|
||||
}
|
||||
|
||||
// SECURITY: Whitelist validation for restriction type
|
||||
$allowed_types = ['doctor', 'service'];
|
||||
if (!in_array($data['restriction_type'], $allowed_types, true)) {
|
||||
error_log('Care Booking Block: Invalid restriction_type in insert(): ' . $data['restriction_type']);
|
||||
return false;
|
||||
}
|
||||
|
||||
// SECURITY: Validate target_id
|
||||
$target_id = absint($data['target_id']);
|
||||
if ($target_id <= 0 || $target_id > PHP_INT_MAX) {
|
||||
error_log('Care Booking Block: Invalid target_id in insert(): ' . $data['target_id']);
|
||||
return false;
|
||||
}
|
||||
|
||||
// SECURITY: Validate service restrictions require doctor_id
|
||||
if ($data['restriction_type'] === 'service') {
|
||||
if (empty($data['doctor_id']) || absint($data['doctor_id']) <= 0) {
|
||||
error_log('Care Booking Block: Missing or invalid doctor_id for service restriction');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// SECURITY: Prepare data with proper sanitization
|
||||
$insert_data = [
|
||||
'restriction_type' => sanitize_text_field($data['restriction_type']),
|
||||
'target_id' => $target_id,
|
||||
'doctor_id' => isset($data['doctor_id']) ? absint($data['doctor_id']) : null,
|
||||
'is_blocked' => isset($data['is_blocked']) ? (bool) $data['is_blocked'] : false
|
||||
];
|
||||
|
||||
// SECURITY: Define data types for prepared statement
|
||||
$format = ['%s', '%d', '%d', '%d'];
|
||||
|
||||
// SECURITY: Use WordPress prepared statement (wpdb->insert uses prepare internally)
|
||||
$result = $this->wpdb->insert($this->table_name, $insert_data, $format);
|
||||
|
||||
if ($result === false) {
|
||||
error_log('Care Booking Block: Database insert failed: ' . $this->wpdb->last_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update restriction
|
||||
*
|
||||
* @param int $id Restriction ID
|
||||
* @param array $data Update data
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function update($id, $data)
|
||||
{
|
||||
$id = absint($id);
|
||||
if ($id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
$update_data = [];
|
||||
$format = [];
|
||||
|
||||
if (isset($data['restriction_type'])) {
|
||||
if (!in_array($data['restriction_type'], ['doctor', 'service'])) {
|
||||
return false;
|
||||
}
|
||||
$update_data['restriction_type'] = sanitize_text_field($data['restriction_type']);
|
||||
$format[] = '%s';
|
||||
}
|
||||
|
||||
if (isset($data['target_id'])) {
|
||||
$update_data['target_id'] = absint($data['target_id']);
|
||||
$format[] = '%d';
|
||||
}
|
||||
|
||||
if (isset($data['doctor_id'])) {
|
||||
$update_data['doctor_id'] = absint($data['doctor_id']);
|
||||
$format[] = '%d';
|
||||
}
|
||||
|
||||
if (isset($data['is_blocked'])) {
|
||||
$update_data['is_blocked'] = (bool) $data['is_blocked'];
|
||||
$format[] = '%d';
|
||||
}
|
||||
|
||||
if (empty($update_data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->wpdb->update(
|
||||
$this->table_name,
|
||||
$update_data,
|
||||
['id' => $id],
|
||||
$format,
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete restriction
|
||||
*
|
||||
* @param int $id Restriction ID
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
$id = absint($id);
|
||||
if ($id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->wpdb->delete(
|
||||
$this->table_name,
|
||||
['id' => $id],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restriction by ID
|
||||
*
|
||||
* @param int $id Restriction ID
|
||||
* @return object|false Restriction object on success, false on failure
|
||||
*/
|
||||
public function get($id)
|
||||
{
|
||||
// SECURITY: Enhanced input validation
|
||||
$id = absint($id);
|
||||
if ($id <= 0 || $id > PHP_INT_MAX) {
|
||||
error_log('Care Booking Block: Invalid ID in get(): ' . $id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// SECURITY: Use prepared statement (already implemented correctly)
|
||||
$query = $this->wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id);
|
||||
|
||||
$result = $this->wpdb->get_row($query);
|
||||
|
||||
// SECURITY: Log any database errors
|
||||
if ($this->wpdb->last_error) {
|
||||
error_log('Care Booking Block: Database error in get(): ' . $this->wpdb->last_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restrictions by type
|
||||
*
|
||||
* @param string $type Restriction type ('doctor' or 'service')
|
||||
* @return array Array of restriction objects
|
||||
*/
|
||||
public function get_by_type($type)
|
||||
{
|
||||
if (!in_array($type, ['doctor', 'service'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT * FROM {$this->table_name} WHERE restriction_type = %s ORDER BY target_id",
|
||||
$type
|
||||
);
|
||||
|
||||
$results = $this->wpdb->get_results($query);
|
||||
|
||||
return is_array($results) ? $results : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all restrictions
|
||||
*
|
||||
* @return array Array of restriction objects
|
||||
*/
|
||||
public function get_all()
|
||||
{
|
||||
$query = "SELECT * FROM {$this->table_name} ORDER BY restriction_type, target_id";
|
||||
|
||||
$results = $this->wpdb->get_results($query);
|
||||
|
||||
return is_array($results) ? $results : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked doctor IDs with performance optimization
|
||||
*
|
||||
* @return array Array of blocked doctor IDs
|
||||
*/
|
||||
public function get_blocked_doctors()
|
||||
{
|
||||
// Performance-optimized query using composite index
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT target_id FROM {$this->table_name}
|
||||
WHERE restriction_type = %s AND is_blocked = %d
|
||||
ORDER BY target_id",
|
||||
'doctor',
|
||||
1
|
||||
);
|
||||
|
||||
$results = $this->wpdb->get_col($query);
|
||||
|
||||
return is_array($results) ? array_map('intval', $results) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked service IDs for specific doctor with performance optimization
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return array Array of blocked service IDs
|
||||
*/
|
||||
public function get_blocked_services($doctor_id)
|
||||
{
|
||||
$doctor_id = absint($doctor_id);
|
||||
if ($doctor_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Performance-optimized query using composite index idx_performance_service
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT target_id FROM {$this->table_name}
|
||||
WHERE doctor_id = %d AND target_id > 0 AND is_blocked = %d
|
||||
ORDER BY target_id",
|
||||
$doctor_id,
|
||||
1
|
||||
);
|
||||
|
||||
$results = $this->wpdb->get_col($query);
|
||||
|
||||
return is_array($results) ? array_map('intval', $results) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find existing restriction
|
||||
*
|
||||
* @param string $type Restriction type
|
||||
* @param int $target_id Target ID
|
||||
* @param int $doctor_id Doctor ID (for service restrictions)
|
||||
* @return object|false Restriction object or false if not found
|
||||
*/
|
||||
public function find_existing($type, $target_id, $doctor_id = null)
|
||||
{
|
||||
if (!in_array($type, ['doctor', 'service'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$target_id = absint($target_id);
|
||||
if ($target_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === 'doctor') {
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT * FROM {$this->table_name}
|
||||
WHERE restriction_type = %s AND target_id = %d LIMIT 1",
|
||||
$type,
|
||||
$target_id
|
||||
);
|
||||
} else {
|
||||
$doctor_id = absint($doctor_id);
|
||||
if ($doctor_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT * FROM {$this->table_name}
|
||||
WHERE restriction_type = %s AND target_id = %d AND doctor_id = %d LIMIT 1",
|
||||
$type,
|
||||
$target_id,
|
||||
$doctor_id
|
||||
);
|
||||
}
|
||||
|
||||
return $this->wpdb->get_row($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk insert restrictions
|
||||
*
|
||||
* @param array $restrictions Array of restriction data
|
||||
* @return array Array of inserted IDs (or false for failed insertions)
|
||||
*/
|
||||
public function bulk_insert($restrictions)
|
||||
{
|
||||
if (!is_array($restrictions) || empty($restrictions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($restrictions as $restriction_data) {
|
||||
$result = $this->insert($restriction_data);
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count restrictions by type
|
||||
*
|
||||
* @param string $type Restriction type
|
||||
* @return int Number of restrictions
|
||||
*/
|
||||
public function count_by_type($type)
|
||||
{
|
||||
if (!in_array($type, ['doctor', 'service'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = $this->wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table_name} WHERE restriction_type = %s",
|
||||
$type
|
||||
);
|
||||
|
||||
$result = $this->wpdb->get_var($query);
|
||||
|
||||
return is_numeric($result) ? (int) $result : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database error if any
|
||||
*
|
||||
* @return string Database error message
|
||||
*/
|
||||
public function get_last_error()
|
||||
{
|
||||
return $this->wpdb->last_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up restrictions for non-existent targets
|
||||
*
|
||||
* @return int Number of cleaned up restrictions
|
||||
*/
|
||||
public function cleanup_orphaned_restrictions()
|
||||
{
|
||||
// This method would need integration with KiviCare tables
|
||||
// For now, we'll return 0 as a placeholder
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query performance statistics
|
||||
*
|
||||
* @return array Performance stats
|
||||
*/
|
||||
public function get_performance_stats()
|
||||
{
|
||||
$stats = [
|
||||
'total_queries' => $this->wpdb->num_queries,
|
||||
'table_exists' => $this->table_exists(),
|
||||
'row_count' => $this->wpdb->get_var("SELECT COUNT(*) FROM {$this->table_name}"),
|
||||
'index_usage' => $this->analyze_index_usage(),
|
||||
'query_cache_hits' => $this->get_query_cache_stats()
|
||||
];
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze database index usage for optimization
|
||||
*
|
||||
* @return array Index usage statistics
|
||||
*/
|
||||
private function analyze_index_usage()
|
||||
{
|
||||
if (!defined('WP_DEBUG') || !WP_DEBUG) {
|
||||
return ['debug_only' => true];
|
||||
}
|
||||
|
||||
$indexes = [
|
||||
'idx_type_target',
|
||||
'idx_doctor_service',
|
||||
'idx_blocked',
|
||||
'idx_composite_blocked',
|
||||
'idx_performance_doctor',
|
||||
'idx_performance_service'
|
||||
];
|
||||
|
||||
$usage_stats = [];
|
||||
foreach ($indexes as $index) {
|
||||
// This would typically require EXPLAIN queries
|
||||
$usage_stats[$index] = 'active';
|
||||
}
|
||||
|
||||
return $usage_stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query cache statistics
|
||||
*
|
||||
* @return array Cache statistics
|
||||
*/
|
||||
private function get_query_cache_stats()
|
||||
{
|
||||
// Basic query cache monitoring
|
||||
$cache_key = 'care_booking_query_cache_stats';
|
||||
$stats = get_transient($cache_key) ?: ['hits' => 0, 'misses' => 0];
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
798
care-booking-block/includes/class-kivicare-integration.php
Normal file
798
care-booking-block/includes/class-kivicare-integration.php
Normal file
@@ -0,0 +1,798 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* KiviCare integration for Care Booking Block plugin
|
||||
*
|
||||
* @package CareBookingBlock
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* KiviCare integration class
|
||||
*/
|
||||
class Care_Booking_KiviCare_Integration
|
||||
{
|
||||
/**
|
||||
* Database handler instance
|
||||
*
|
||||
* @var Care_Booking_Database_Handler
|
||||
*/
|
||||
private $db_handler;
|
||||
|
||||
/**
|
||||
* Restriction model instance
|
||||
*
|
||||
* @var Care_Booking_Restriction_Model
|
||||
*/
|
||||
private $restriction_model;
|
||||
|
||||
/**
|
||||
* Cache manager instance
|
||||
*
|
||||
* @var Care_Booking_Cache_Manager
|
||||
*/
|
||||
private $cache_manager;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Care_Booking_Database_Handler $db_handler Database handler instance
|
||||
*/
|
||||
public function __construct($db_handler)
|
||||
{
|
||||
$this->db_handler = $db_handler;
|
||||
$this->restriction_model = new Care_Booking_Restriction_Model();
|
||||
$this->cache_manager = new Care_Booking_Cache_Manager();
|
||||
|
||||
$this->init_hooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WordPress hooks
|
||||
*/
|
||||
private function init_hooks()
|
||||
{
|
||||
// Enhanced KiviCare filter hooks with multiple compatibility points
|
||||
// Priority 10 for standard filtering, Priority 5 for early filtering
|
||||
add_filter('kc_get_doctors_for_booking', [$this, 'filter_doctors'], 10, 1);
|
||||
add_filter('kivicare_doctors_list', [$this, 'filter_doctors'], 10, 1);
|
||||
add_filter('kivicare_get_doctors', [$this, 'filter_doctors'], 10, 1);
|
||||
|
||||
// Service filtering with multiple hook points
|
||||
add_filter('kc_get_services_by_doctor', [$this, 'filter_services'], 10, 2);
|
||||
add_filter('kivicare_services_list', [$this, 'filter_services'], 10, 2);
|
||||
add_filter('kivicare_get_services', [$this, 'filter_services'], 10, 2);
|
||||
|
||||
// Enhanced CSS injection with optimized priority
|
||||
add_action('wp_head', [$this, 'inject_restriction_css'], 15);
|
||||
|
||||
// Frontend JavaScript for graceful degradation
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_scripts'], 10);
|
||||
|
||||
// Frontend CSS for base styles
|
||||
add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_styles'], 10);
|
||||
|
||||
// KiviCare 3.0+ REST API hooks
|
||||
add_filter('rest_pre_serve_request', [$this, 'filter_rest_api_response'], 10, 4);
|
||||
|
||||
// Admin bar integration (optional)
|
||||
if (is_admin_bar_showing()) {
|
||||
add_action('admin_bar_menu', [$this, 'add_admin_bar_menu'], 100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter KiviCare doctors list to remove blocked doctors
|
||||
*
|
||||
* @param array $doctors Array of doctors from KiviCare
|
||||
* @return array Filtered array of doctors
|
||||
*/
|
||||
public function filter_doctors($doctors)
|
||||
{
|
||||
// Validate input
|
||||
if (!is_array($doctors)) {
|
||||
return $doctors;
|
||||
}
|
||||
|
||||
// Skip filtering in admin area (keep full access for administrators)
|
||||
if (is_admin() && current_user_can('manage_options')) {
|
||||
return $doctors;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get blocked doctors (with caching)
|
||||
$blocked_doctors = $this->restriction_model->get_blocked_doctors();
|
||||
|
||||
if (empty($blocked_doctors)) {
|
||||
return $doctors;
|
||||
}
|
||||
|
||||
// Filter out blocked doctors
|
||||
$filtered_doctors = [];
|
||||
foreach ($doctors as $key => $doctor) {
|
||||
// Handle both array and object formats
|
||||
$doctor_id = is_array($doctor) ? ($doctor['id'] ?? 0) : ($doctor->id ?? 0);
|
||||
|
||||
if (!in_array((int) $doctor_id, $blocked_doctors)) {
|
||||
$filtered_doctors[$key] = $doctor;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered_doctors;
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Log error and return original array on failure
|
||||
if (function_exists('error_log')) {
|
||||
error_log('Care Booking Block: Doctor filtering error - ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $doctors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter KiviCare services list to remove blocked services for specific doctor
|
||||
*
|
||||
* @param array $services Array of services from KiviCare
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return array Filtered array of services
|
||||
*/
|
||||
public function filter_services($services, $doctor_id = null)
|
||||
{
|
||||
// Validate input
|
||||
if (!is_array($services)) {
|
||||
return $services;
|
||||
}
|
||||
|
||||
// Skip filtering in admin area (keep full access for administrators)
|
||||
if (is_admin() && current_user_can('manage_options')) {
|
||||
return $services;
|
||||
}
|
||||
|
||||
try {
|
||||
$filtered_services = [];
|
||||
|
||||
// If no doctor_id provided, try to extract from services or context
|
||||
if (!$doctor_id) {
|
||||
$doctor_id = $this->extract_doctor_id_from_context($services);
|
||||
}
|
||||
|
||||
// Get blocked services for this doctor (with enhanced caching)
|
||||
$blocked_services = $doctor_id ?
|
||||
$this->restriction_model->get_blocked_services($doctor_id) : [];
|
||||
|
||||
// Also get globally blocked doctors to filter services
|
||||
$blocked_doctors = $this->restriction_model->get_blocked_doctors();
|
||||
|
||||
foreach ($services as $key => $service) {
|
||||
// Handle both array and object formats
|
||||
$service_id = is_array($service) ? ($service['id'] ?? 0) : ($service->id ?? 0);
|
||||
$service_doctor_id = is_array($service) ?
|
||||
($service['doctor_id'] ?? $doctor_id) :
|
||||
($service->doctor_id ?? $doctor_id);
|
||||
|
||||
// Skip if service belongs to a blocked doctor
|
||||
if ($service_doctor_id && in_array((int) $service_doctor_id, $blocked_doctors)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if service is specifically blocked for this doctor
|
||||
if ($service_doctor_id && !empty($blocked_services) &&
|
||||
in_array((int) $service_id, $blocked_services)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filtered_services[$key] = $service;
|
||||
}
|
||||
|
||||
return $filtered_services;
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Log error and return original array on failure
|
||||
if (function_exists('error_log')) {
|
||||
error_log('Care Booking Block: Service filtering error - ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $services;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract doctor ID from service context or URL parameters
|
||||
*
|
||||
* @param array $services Services array
|
||||
* @return int|null Doctor ID if found
|
||||
*/
|
||||
private function extract_doctor_id_from_context($services)
|
||||
{
|
||||
// Try to get from first service
|
||||
if (!empty($services)) {
|
||||
$first_service = reset($services);
|
||||
$doctor_id = is_array($first_service) ?
|
||||
($first_service['doctor_id'] ?? null) :
|
||||
($first_service->doctor_id ?? null);
|
||||
|
||||
if ($doctor_id) {
|
||||
return (int) $doctor_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get from URL parameters
|
||||
if (isset($_GET['doctor_id'])) {
|
||||
return (int) $_GET['doctor_id'];
|
||||
}
|
||||
|
||||
// Try to get from POST data
|
||||
if (isset($_POST['doctor_id'])) {
|
||||
return (int) $_POST['doctor_id'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend JavaScript for graceful degradation
|
||||
*/
|
||||
public function enqueue_frontend_scripts()
|
||||
{
|
||||
// Only on frontend and if KiviCare is active
|
||||
if (is_admin() || !$this->is_kivicare_active()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're on a page that might have KiviCare content
|
||||
if (!$this->should_load_frontend_scripts()) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'care-booking-frontend',
|
||||
CARE_BOOKING_BLOCK_PLUGIN_URL . 'public/js/frontend.js',
|
||||
['jquery'],
|
||||
CARE_BOOKING_BLOCK_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Localize script with configuration
|
||||
wp_localize_script('care-booking-frontend', 'careBookingConfig', [
|
||||
'ajaxurl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('care_booking_frontend'),
|
||||
'debug' => defined('WP_DEBUG') && WP_DEBUG,
|
||||
'fallbackEnabled' => true,
|
||||
'retryAttempts' => 3,
|
||||
'retryDelay' => 1000,
|
||||
'selectors' => [
|
||||
'doctors' => '.kivicare-doctor, .kc-doctor-item, .doctor-card',
|
||||
'services' => '.kivicare-service, .kc-service-item, .service-card',
|
||||
'forms' => '.kivicare-booking-form, .kc-booking-form',
|
||||
'loading' => '.care-booking-loading'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if frontend scripts should be loaded on current page
|
||||
*
|
||||
* @return bool True if scripts should be loaded
|
||||
*/
|
||||
private function should_load_frontend_scripts()
|
||||
{
|
||||
global $post;
|
||||
|
||||
// Always load on pages with KiviCare shortcodes
|
||||
if ($post && has_shortcode($post->post_content, 'kivicare')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load on pages with KiviCare blocks
|
||||
if ($post && has_block('kivicare/booking', $post->post_content)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load on template pages that might contain KiviCare
|
||||
$template = get_page_template_slug();
|
||||
if (in_array($template, ['page-booking.php', 'page-appointment.php'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load if URL contains KiviCare parameters
|
||||
if (isset($_GET['kivicare']) || isset($_GET['booking']) || isset($_GET['appointment'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend CSS for base styles
|
||||
*/
|
||||
public function enqueue_frontend_styles()
|
||||
{
|
||||
// Only on frontend and if KiviCare is active
|
||||
if (is_admin() || !$this->is_kivicare_active()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're on a page that might have KiviCare content
|
||||
if (!$this->should_load_frontend_scripts()) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'care-booking-frontend',
|
||||
CARE_BOOKING_BLOCK_PLUGIN_URL . 'public/css/frontend.css',
|
||||
[],
|
||||
CARE_BOOKING_BLOCK_VERSION,
|
||||
'all'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject optimized CSS to hide blocked elements on frontend
|
||||
*
|
||||
* Priority 15 - After theme styles but before most plugins
|
||||
*/
|
||||
public function inject_restriction_css()
|
||||
{
|
||||
// Only inject on frontend
|
||||
if (is_admin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if not on pages with KiviCare content (performance optimization)
|
||||
if (!$this->should_inject_css()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get blocked doctors and services with caching
|
||||
$blocked_doctors = $this->restriction_model->get_blocked_doctors();
|
||||
$blocked_services = $this->get_all_blocked_services();
|
||||
|
||||
// Early return if no restrictions
|
||||
if (empty($blocked_doctors) && empty($blocked_services)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate optimized CSS
|
||||
$css = $this->generate_restriction_css($blocked_doctors, $blocked_services);
|
||||
|
||||
if (!empty($css)) {
|
||||
// Output with proper caching headers and minification
|
||||
echo "\n<!-- Care Booking Block Styles -->\n";
|
||||
echo '<style id="care-booking-restrictions" data-care-booking="restriction-css" data-version="' . CARE_BOOKING_BLOCK_VERSION . '">';
|
||||
|
||||
// Add performance optimizations
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
echo "\n" . $css . "\n";
|
||||
} else {
|
||||
// Minified output for production
|
||||
echo $this->minify_css($css);
|
||||
}
|
||||
|
||||
echo '</style>';
|
||||
echo "\n<!-- End Care Booking Block Styles -->\n";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Silently fail to avoid breaking frontend
|
||||
if (function_exists('error_log')) {
|
||||
error_log('Care Booking Block: CSS injection error - ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// In debug mode, show a minimal error indicator
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
echo '<!-- Care Booking Block: CSS injection failed -->';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if CSS should be injected on current page
|
||||
*
|
||||
* @return bool True if CSS should be injected
|
||||
*/
|
||||
private function should_inject_css()
|
||||
{
|
||||
// Always inject if KiviCare is active and we have restrictions
|
||||
if (!$this->is_kivicare_active()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the same logic as frontend scripts
|
||||
return $this->should_load_frontend_scripts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Minify CSS for production
|
||||
*
|
||||
* @param string $css CSS to minify
|
||||
* @return string Minified CSS
|
||||
*/
|
||||
private function minify_css($css)
|
||||
{
|
||||
// Remove comments
|
||||
$css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
|
||||
|
||||
// Remove whitespace
|
||||
$css = str_replace(["\r\n", "\r", "\n", "\t"], '', $css);
|
||||
|
||||
// Remove extra spaces
|
||||
$css = preg_replace('/\s+/', ' ', $css);
|
||||
|
||||
// Remove spaces around specific characters
|
||||
$css = str_replace(['; ', ' {', '{ ', ' }', '} ', ': ', ', ', ' ,'], [';', '{', '{', '}', '}', ':', ',', ','], $css);
|
||||
|
||||
return trim($css);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate optimized CSS for hiding blocked elements
|
||||
*
|
||||
* @param array $blocked_doctors Array of blocked doctor IDs
|
||||
* @param array $blocked_services Array of blocked service data
|
||||
* @return string Generated CSS with optimization and caching
|
||||
*/
|
||||
private function generate_restriction_css($blocked_doctors, $blocked_services)
|
||||
{
|
||||
// Check cache first
|
||||
$cache_key = 'care_booking_css_' . md5(serialize([$blocked_doctors, $blocked_services]));
|
||||
$cached_css = get_transient($cache_key);
|
||||
|
||||
if ($cached_css !== false) {
|
||||
return $cached_css;
|
||||
}
|
||||
|
||||
$css_rules = [];
|
||||
$css_comments = [];
|
||||
|
||||
// CSS for blocked doctors with enhanced selectors
|
||||
if (!empty($blocked_doctors)) {
|
||||
$doctor_selectors = [];
|
||||
$css_comments[] = "/* Blocked doctors: " . count($blocked_doctors) . " */";
|
||||
|
||||
foreach ($blocked_doctors as $doctor_id) {
|
||||
$doctor_id = (int) $doctor_id;
|
||||
|
||||
// KiviCare 3.0+ primary selectors
|
||||
$doctor_selectors[] = ".kivicare-doctor[data-doctor-id=\"{$doctor_id}\"]";
|
||||
$doctor_selectors[] = ".kc-doctor-item[data-id=\"{$doctor_id}\"]";
|
||||
$doctor_selectors[] = ".doctor-card[data-doctor=\"{$doctor_id}\"]";
|
||||
|
||||
// Legacy selectors
|
||||
$doctor_selectors[] = "#doctor-{$doctor_id}";
|
||||
$doctor_selectors[] = ".kc-doctor-{$doctor_id}";
|
||||
|
||||
// Form selectors
|
||||
$doctor_selectors[] = ".doctor-selection option[value=\"{$doctor_id}\"]";
|
||||
$doctor_selectors[] = "select[name='doctor_id'] option[value=\"{$doctor_id}\"]";
|
||||
|
||||
// Booking form selectors
|
||||
$doctor_selectors[] = ".booking-doctor-{$doctor_id}";
|
||||
$doctor_selectors[] = ".appointment-doctor-{$doctor_id}";
|
||||
}
|
||||
|
||||
if (!empty($doctor_selectors)) {
|
||||
// Split into chunks for better CSS performance
|
||||
$chunks = array_chunk($doctor_selectors, 50);
|
||||
foreach ($chunks as $chunk) {
|
||||
$css_rules[] = implode(',', $chunk) . ' { display: none !important; visibility: hidden !important; }';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSS for blocked services with enhanced context
|
||||
if (!empty($blocked_services)) {
|
||||
$service_selectors = [];
|
||||
$css_comments[] = "/* Blocked services: " . count($blocked_services) . " */";
|
||||
|
||||
foreach ($blocked_services as $service_data) {
|
||||
$service_id = (int) $service_data['service_id'];
|
||||
$doctor_id = (int) $service_data['doctor_id'];
|
||||
|
||||
// KiviCare 3.0+ primary selectors
|
||||
$service_selectors[] = ".kivicare-service[data-service-id=\"{$service_id}\"][data-doctor-id=\"{$doctor_id}\"]";
|
||||
$service_selectors[] = ".kc-service-item[data-service=\"{$service_id}\"][data-doctor=\"{$doctor_id}\"]";
|
||||
$service_selectors[] = ".service-card[data-service=\"{$service_id}\"][data-doctor=\"{$doctor_id}\"]";
|
||||
|
||||
// Legacy selectors
|
||||
$service_selectors[] = "#service-{$service_id}-doctor-{$doctor_id}";
|
||||
$service_selectors[] = ".kc-service-{$service_id}.kc-doctor-{$doctor_id}";
|
||||
|
||||
// Form selectors
|
||||
$service_selectors[] = ".service-selection[data-doctor=\"{$doctor_id}\"] option[value=\"{$service_id}\"]";
|
||||
$service_selectors[] = "select[name='service_id'][data-doctor=\"{$doctor_id}\"] option[value=\"{$service_id}\"]";
|
||||
|
||||
// Booking form selectors
|
||||
$service_selectors[] = ".booking-service-{$service_id}.doctor-{$doctor_id}";
|
||||
$service_selectors[] = ".appointment-service-{$service_id}.doctor-{$doctor_id}";
|
||||
}
|
||||
|
||||
if (!empty($service_selectors)) {
|
||||
// Split into chunks for better CSS performance
|
||||
$chunks = array_chunk($service_selectors, 50);
|
||||
foreach ($chunks as $chunk) {
|
||||
$css_rules[] = implode(',', $chunk) . ' { display: none !important; visibility: hidden !important; }';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add graceful degradation styles
|
||||
$css_rules[] = '.care-booking-fallback { opacity: 0.7; pointer-events: none; }';
|
||||
$css_rules[] = '.care-booking-loading::after { content: "Loading..."; }';
|
||||
|
||||
// Combine CSS with optimization
|
||||
$final_css = '';
|
||||
|
||||
if (!empty($css_comments)) {
|
||||
$final_css .= implode(PHP_EOL, $css_comments) . PHP_EOL;
|
||||
}
|
||||
|
||||
if (!empty($css_rules)) {
|
||||
// Minify CSS in production
|
||||
if (defined('WP_DEBUG') && !WP_DEBUG) {
|
||||
$final_css .= implode('', $css_rules);
|
||||
} else {
|
||||
$final_css .= implode(PHP_EOL, $css_rules);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache for 1 hour
|
||||
set_transient($cache_key, $final_css, 3600);
|
||||
|
||||
return $final_css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all blocked services across all doctors
|
||||
*
|
||||
* @return array Array of blocked service data
|
||||
*/
|
||||
private function get_all_blocked_services()
|
||||
{
|
||||
$blocked_services = [];
|
||||
|
||||
// Get all service restrictions
|
||||
$service_restrictions = $this->restriction_model->get_by_type('service');
|
||||
|
||||
foreach ($service_restrictions as $restriction) {
|
||||
if ($restriction->is_blocked) {
|
||||
$blocked_services[] = [
|
||||
'service_id' => (int) $restriction->target_id,
|
||||
'doctor_id' => (int) $restriction->doctor_id
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $blocked_services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin bar menu for quick access
|
||||
*
|
||||
* @param WP_Admin_Bar $wp_admin_bar WordPress admin bar object
|
||||
*/
|
||||
public function add_admin_bar_menu($wp_admin_bar)
|
||||
{
|
||||
// Only show for users with manage_options capability
|
||||
if (!current_user_can('manage_options')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wp_admin_bar->add_menu([
|
||||
'id' => 'care-booking-control',
|
||||
'title' => __('Care Booking', 'care-booking-block'),
|
||||
'href' => admin_url('tools.php?page=care-booking-control'),
|
||||
'meta' => [
|
||||
'title' => __('Care Booking Control', 'care-booking-block')
|
||||
]
|
||||
]);
|
||||
|
||||
// Add submenu with statistics
|
||||
$stats = $this->restriction_model->get_statistics();
|
||||
|
||||
$wp_admin_bar->add_menu([
|
||||
'parent' => 'care-booking-control',
|
||||
'id' => 'care-booking-stats',
|
||||
'title' => sprintf(
|
||||
__('Restrictions: %d doctors, %d services', 'care-booking-block'),
|
||||
$stats['blocked_doctors'],
|
||||
$stats['service_restrictions']
|
||||
),
|
||||
'href' => admin_url('tools.php?page=care-booking-control'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if specific doctor is blocked
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return bool True if blocked, false otherwise
|
||||
*/
|
||||
public function is_doctor_blocked($doctor_id)
|
||||
{
|
||||
return $this->restriction_model->is_doctor_blocked($doctor_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if specific service is blocked for a doctor
|
||||
*
|
||||
* @param int $service_id Service ID
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return bool True if blocked, false otherwise
|
||||
*/
|
||||
public function is_service_blocked($service_id, $doctor_id)
|
||||
{
|
||||
return $this->restriction_model->is_service_blocked($service_id, $doctor_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked doctors count
|
||||
*
|
||||
* @return int Number of blocked doctors
|
||||
*/
|
||||
public function get_blocked_doctors_count()
|
||||
{
|
||||
return count($this->restriction_model->get_blocked_doctors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked services count for specific doctor
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return int Number of blocked services
|
||||
*/
|
||||
public function get_blocked_services_count($doctor_id)
|
||||
{
|
||||
return count($this->restriction_model->get_blocked_services($doctor_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply restrictions to KiviCare query (if supported)
|
||||
*
|
||||
* @param string $query SQL query
|
||||
* @param string $context Query context
|
||||
* @return string Modified query
|
||||
*/
|
||||
public function filter_kivicare_query($query, $context = '')
|
||||
{
|
||||
// This would be used if KiviCare provides query filtering hooks
|
||||
// For now, return original query
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle KiviCare appointment booking validation
|
||||
*
|
||||
* @param array $booking_data Booking data
|
||||
* @return bool|WP_Error True if allowed, WP_Error if blocked
|
||||
*/
|
||||
public function validate_booking($booking_data)
|
||||
{
|
||||
$doctor_id = $booking_data['doctor_id'] ?? 0;
|
||||
$service_id = $booking_data['service_id'] ?? 0;
|
||||
|
||||
// Check if doctor is blocked
|
||||
if ($this->is_doctor_blocked($doctor_id)) {
|
||||
return new WP_Error(
|
||||
'doctor_blocked',
|
||||
__('This doctor is not available for booking.', 'care-booking-block')
|
||||
);
|
||||
}
|
||||
|
||||
// Check if service is blocked for this doctor
|
||||
if ($service_id && $this->is_service_blocked($service_id, $doctor_id)) {
|
||||
return new WP_Error(
|
||||
'service_blocked',
|
||||
__('This service is not available for this doctor.', 'care-booking-block')
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get integration status
|
||||
*
|
||||
* @return array Status information
|
||||
*/
|
||||
public function get_integration_status()
|
||||
{
|
||||
return [
|
||||
'kivicare_active' => $this->is_kivicare_active(),
|
||||
'hooks_registered' => [
|
||||
'doctor_filter' => has_filter('kc_get_doctors_for_booking'),
|
||||
'service_filter' => has_filter('kc_get_services_by_doctor'),
|
||||
'css_injection' => has_action('wp_head')
|
||||
],
|
||||
'cache_status' => $this->cache_manager->get_cache_stats(),
|
||||
'restrictions' => $this->restriction_model->get_statistics()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter KiviCare REST API responses for doctor and service listings
|
||||
*
|
||||
* @param mixed $served Whether the request has already been served
|
||||
* @param WP_HTTP_Response $result The response object
|
||||
* @param WP_REST_Request $request The request object
|
||||
* @param WP_REST_Server $server The REST server instance
|
||||
* @return mixed Original served value
|
||||
*/
|
||||
public function filter_rest_api_response($served, $result, $request, $server)
|
||||
{
|
||||
// Skip if already served or not a KiviCare endpoint
|
||||
if ($served || !$this->is_kivicare_rest_endpoint($request)) {
|
||||
return $served;
|
||||
}
|
||||
|
||||
// Skip filtering in admin area for administrators
|
||||
if (is_admin() && current_user_can('manage_options')) {
|
||||
return $served;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = $result->get_data();
|
||||
|
||||
if (is_array($data) && isset($data['data'])) {
|
||||
$route = $request->get_route();
|
||||
|
||||
// Filter doctors endpoint
|
||||
if (strpos($route, '/doctors') !== false && is_array($data['data'])) {
|
||||
$data['data'] = $this->filter_doctors($data['data']);
|
||||
$result->set_data($data);
|
||||
}
|
||||
|
||||
// Filter services endpoint
|
||||
if (strpos($route, '/services') !== false && is_array($data['data'])) {
|
||||
$doctor_id = $request->get_param('doctor_id') ?: null;
|
||||
$data['data'] = $this->filter_services($data['data'], $doctor_id);
|
||||
$result->set_data($data);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Log error but don't break API response
|
||||
if (function_exists('error_log')) {
|
||||
error_log('Care Booking Block: REST API filtering error - ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $served;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is for a KiviCare REST endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request The request object
|
||||
* @return bool True if KiviCare endpoint
|
||||
*/
|
||||
private function is_kivicare_rest_endpoint($request)
|
||||
{
|
||||
$route = $request->get_route();
|
||||
return strpos($route, '/kivicare/') !== false ||
|
||||
strpos($route, '/kc/') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if KiviCare plugin is active
|
||||
*
|
||||
* @return bool True if KiviCare is active, false otherwise
|
||||
*/
|
||||
private function is_kivicare_active()
|
||||
{
|
||||
if (!function_exists('is_plugin_active')) {
|
||||
include_once(ABSPATH . 'wp-admin/includes/plugin.php');
|
||||
}
|
||||
|
||||
return is_plugin_active('kivicare/kivicare.php') ||
|
||||
is_plugin_active('kivicare-clinic-management-system/kivicare.php');
|
||||
}
|
||||
}
|
||||
537
care-booking-block/includes/class-performance-monitor.php
Normal file
537
care-booking-block/includes/class-performance-monitor.php
Normal file
@@ -0,0 +1,537 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Performance Monitor for Care Booking Block plugin
|
||||
* Tracks and analyzes performance metrics to ensure <2% overhead target
|
||||
*
|
||||
* @package CareBookingBlock
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performance Monitor class for enterprise-grade optimization
|
||||
*/
|
||||
class Care_Booking_Performance_Monitor
|
||||
{
|
||||
/**
|
||||
* Performance metrics cache key
|
||||
*/
|
||||
const METRICS_CACHE_KEY = 'care_booking_performance_metrics';
|
||||
|
||||
/**
|
||||
* Performance target: <2% overhead
|
||||
*/
|
||||
const TARGET_OVERHEAD_PERCENT = 2.0;
|
||||
|
||||
/**
|
||||
* Performance target: <100ms AJAX response
|
||||
*/
|
||||
const TARGET_AJAX_RESPONSE_MS = 100;
|
||||
|
||||
/**
|
||||
* Performance target: >95% cache hit rate
|
||||
*/
|
||||
const TARGET_CACHE_HIT_RATE = 95.0;
|
||||
|
||||
/**
|
||||
* Initialize performance monitoring
|
||||
*/
|
||||
public static function init()
|
||||
{
|
||||
// Hook into WordPress performance points
|
||||
add_action('init', [__CLASS__, 'start_performance_tracking'], 1);
|
||||
add_action('wp_footer', [__CLASS__, 'end_performance_tracking'], 999);
|
||||
|
||||
// AJAX performance tracking
|
||||
add_action('wp_ajax_care_booking_get_entities', [__CLASS__, 'track_ajax_start'], 1);
|
||||
add_action('wp_ajax_nopriv_care_booking_get_entities', [__CLASS__, 'track_ajax_start'], 1);
|
||||
|
||||
// Database query performance
|
||||
add_filter('query', [__CLASS__, 'track_database_queries'], 10, 1);
|
||||
|
||||
// Cache performance tracking
|
||||
add_action('care_booking_cache_hit', [__CLASS__, 'track_cache_hit']);
|
||||
add_action('care_booking_cache_miss', [__CLASS__, 'track_cache_miss']);
|
||||
|
||||
// Memory usage tracking
|
||||
add_action('shutdown', [__CLASS__, 'track_memory_usage'], 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start performance tracking for page loads
|
||||
*/
|
||||
public static function start_performance_tracking()
|
||||
{
|
||||
if (!self::should_track_performance()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store start time and memory
|
||||
if (!defined('CARE_BOOKING_START_TIME')) {
|
||||
define('CARE_BOOKING_START_TIME', microtime(true));
|
||||
define('CARE_BOOKING_START_MEMORY', memory_get_usage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* End performance tracking and calculate metrics
|
||||
*/
|
||||
public static function end_performance_tracking()
|
||||
{
|
||||
if (!defined('CARE_BOOKING_START_TIME')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$end_time = microtime(true);
|
||||
$end_memory = memory_get_usage();
|
||||
|
||||
$execution_time = ($end_time - CARE_BOOKING_START_TIME) * 1000; // Convert to ms
|
||||
$memory_usage = $end_memory - CARE_BOOKING_START_MEMORY;
|
||||
|
||||
// Calculate overhead percentage (plugin time vs total page time)
|
||||
$total_page_time = (microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']) * 1000;
|
||||
$overhead_percent = ($execution_time / $total_page_time) * 100;
|
||||
|
||||
$metrics = [
|
||||
'execution_time_ms' => round($execution_time, 2),
|
||||
'memory_usage_bytes' => $memory_usage,
|
||||
'overhead_percent' => round($overhead_percent, 2),
|
||||
'timestamp' => time(),
|
||||
'url' => $_SERVER['REQUEST_URI'] ?? '',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? ''
|
||||
];
|
||||
|
||||
self::store_performance_metrics($metrics);
|
||||
self::check_performance_targets($metrics);
|
||||
|
||||
// Output debug info if enabled
|
||||
if (defined('WP_DEBUG') && WP_DEBUG && current_user_can('manage_options')) {
|
||||
self::output_debug_info($metrics);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track AJAX request start time
|
||||
*/
|
||||
public static function track_ajax_start()
|
||||
{
|
||||
if (!defined('CARE_BOOKING_AJAX_START')) {
|
||||
define('CARE_BOOKING_AJAX_START', microtime(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track AJAX response completion
|
||||
*
|
||||
* @param mixed $response AJAX response data
|
||||
* @return mixed Original response
|
||||
*/
|
||||
public static function track_ajax_complete($response)
|
||||
{
|
||||
if (!defined('CARE_BOOKING_AJAX_START')) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response_time = (microtime(true) - CARE_BOOKING_AJAX_START) * 1000;
|
||||
|
||||
$metrics = [
|
||||
'ajax_response_time_ms' => round($response_time, 2),
|
||||
'ajax_action' => $_POST['action'] ?? '',
|
||||
'timestamp' => time()
|
||||
];
|
||||
|
||||
self::store_ajax_metrics($metrics);
|
||||
|
||||
// Check if we're meeting AJAX performance targets
|
||||
if ($response_time > self::TARGET_AJAX_RESPONSE_MS) {
|
||||
self::log_performance_warning("AJAX response exceeded target: {$response_time}ms > " . self::TARGET_AJAX_RESPONSE_MS . "ms");
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track database queries performance
|
||||
*
|
||||
* @param string $query SQL query
|
||||
* @return string Original query
|
||||
*/
|
||||
public static function track_database_queries($query)
|
||||
{
|
||||
// Only track Care Booking related queries
|
||||
if (strpos($query, 'care_booking_restrictions') === false) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$start_time = microtime(true);
|
||||
|
||||
// Use a filter to track completion
|
||||
add_filter('query_result', function($result) use ($start_time, $query) {
|
||||
$execution_time = (microtime(true) - $start_time) * 1000;
|
||||
|
||||
if ($execution_time > 50) { // Log slow queries > 50ms
|
||||
self::log_performance_warning("Slow query detected: {$execution_time}ms - " . substr($query, 0, 100));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}, 10, 1);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track cache hit
|
||||
*
|
||||
* @param string $cache_key Cache key that was hit
|
||||
*/
|
||||
public static function track_cache_hit($cache_key = '')
|
||||
{
|
||||
$stats = get_transient('care_booking_cache_stats') ?: ['hits' => 0, 'misses' => 0];
|
||||
$stats['hits']++;
|
||||
$stats['last_hit'] = time();
|
||||
|
||||
set_transient('care_booking_cache_stats', $stats, HOUR_IN_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track cache miss
|
||||
*
|
||||
* @param string $cache_key Cache key that was missed
|
||||
*/
|
||||
public static function track_cache_miss($cache_key = '')
|
||||
{
|
||||
$stats = get_transient('care_booking_cache_stats') ?: ['hits' => 0, 'misses' => 0];
|
||||
$stats['misses']++;
|
||||
$stats['last_miss'] = time();
|
||||
|
||||
set_transient('care_booking_cache_stats', $stats, HOUR_IN_SECONDS);
|
||||
|
||||
// Log excessive cache misses
|
||||
$total = $stats['hits'] + $stats['misses'];
|
||||
if ($total > 10 && (($stats['hits'] / $total) * 100) < self::TARGET_CACHE_HIT_RATE) {
|
||||
self::log_performance_warning("Cache hit rate below target: " . round(($stats['hits'] / $total) * 100, 1) . "%");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track memory usage
|
||||
*/
|
||||
public static function track_memory_usage()
|
||||
{
|
||||
$current_memory = memory_get_usage();
|
||||
$peak_memory = memory_get_peak_usage();
|
||||
|
||||
// Target: <10MB footprint
|
||||
$target_memory = 10 * 1024 * 1024; // 10MB in bytes
|
||||
|
||||
if (defined('CARE_BOOKING_START_MEMORY')) {
|
||||
$plugin_memory = $current_memory - CARE_BOOKING_START_MEMORY;
|
||||
|
||||
if ($plugin_memory > $target_memory) {
|
||||
self::log_performance_warning("Memory usage exceeded target: " . size_format($plugin_memory) . " > 10MB");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store performance metrics
|
||||
*
|
||||
* @param array $metrics Performance metrics
|
||||
*/
|
||||
private static function store_performance_metrics($metrics)
|
||||
{
|
||||
$stored_metrics = get_transient(self::METRICS_CACHE_KEY) ?: [];
|
||||
|
||||
// Keep only last 100 measurements for performance
|
||||
if (count($stored_metrics) >= 100) {
|
||||
$stored_metrics = array_slice($stored_metrics, -99);
|
||||
}
|
||||
|
||||
$stored_metrics[] = $metrics;
|
||||
set_transient(self::METRICS_CACHE_KEY, $stored_metrics, DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store AJAX performance metrics
|
||||
*
|
||||
* @param array $metrics AJAX metrics
|
||||
*/
|
||||
private static function store_ajax_metrics($metrics)
|
||||
{
|
||||
$ajax_metrics = get_transient('care_booking_ajax_metrics') ?: [];
|
||||
|
||||
if (count($ajax_metrics) >= 50) {
|
||||
$ajax_metrics = array_slice($ajax_metrics, -49);
|
||||
}
|
||||
|
||||
$ajax_metrics[] = $metrics;
|
||||
set_transient('care_booking_ajax_metrics', $ajax_metrics, DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if performance targets are being met
|
||||
*
|
||||
* @param array $metrics Current performance metrics
|
||||
*/
|
||||
private static function check_performance_targets($metrics)
|
||||
{
|
||||
$warnings = [];
|
||||
|
||||
// Check overhead target (<2%)
|
||||
if ($metrics['overhead_percent'] > self::TARGET_OVERHEAD_PERCENT) {
|
||||
$warnings[] = "Page overhead exceeded target: {$metrics['overhead_percent']}% > " . self::TARGET_OVERHEAD_PERCENT . "%";
|
||||
}
|
||||
|
||||
// Check execution time target (<50ms for plugin operations)
|
||||
if ($metrics['execution_time_ms'] > 50) {
|
||||
$warnings[] = "Plugin execution time high: {$metrics['execution_time_ms']}ms";
|
||||
}
|
||||
|
||||
// Check memory usage target (<10MB)
|
||||
$memory_mb = $metrics['memory_usage_bytes'] / (1024 * 1024);
|
||||
if ($memory_mb > 10) {
|
||||
$warnings[] = "Memory usage exceeded target: " . round($memory_mb, 2) . "MB > 10MB";
|
||||
}
|
||||
|
||||
foreach ($warnings as $warning) {
|
||||
self::log_performance_warning($warning);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log performance warning
|
||||
*
|
||||
* @param string $message Warning message
|
||||
*/
|
||||
private static function log_performance_warning($message)
|
||||
{
|
||||
if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
|
||||
error_log("Care Booking Performance Warning: " . $message);
|
||||
}
|
||||
|
||||
// Store in admin notices if user is admin
|
||||
if (current_user_can('manage_options')) {
|
||||
$notices = get_transient('care_booking_performance_notices') ?: [];
|
||||
$notices[] = [
|
||||
'message' => $message,
|
||||
'timestamp' => time(),
|
||||
'severity' => 'warning'
|
||||
];
|
||||
|
||||
// Keep only last 10 notices
|
||||
if (count($notices) > 10) {
|
||||
$notices = array_slice($notices, -10);
|
||||
}
|
||||
|
||||
set_transient('care_booking_performance_notices', $notices, HOUR_IN_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive performance report
|
||||
*
|
||||
* @return array Performance report
|
||||
*/
|
||||
public static function get_performance_report()
|
||||
{
|
||||
$metrics = get_transient(self::METRICS_CACHE_KEY) ?: [];
|
||||
$ajax_metrics = get_transient('care_booking_ajax_metrics') ?: [];
|
||||
$cache_stats = get_transient('care_booking_cache_stats') ?: ['hits' => 0, 'misses' => 0];
|
||||
|
||||
if (empty($metrics)) {
|
||||
return ['status' => 'no_data'];
|
||||
}
|
||||
|
||||
// Calculate averages
|
||||
$avg_overhead = array_sum(array_column($metrics, 'overhead_percent')) / count($metrics);
|
||||
$avg_execution = array_sum(array_column($metrics, 'execution_time_ms')) / count($metrics);
|
||||
$avg_memory = array_sum(array_column($metrics, 'memory_usage_bytes')) / count($metrics);
|
||||
|
||||
// Calculate cache hit rate
|
||||
$total_cache_requests = $cache_stats['hits'] + $cache_stats['misses'];
|
||||
$cache_hit_rate = $total_cache_requests > 0 ? ($cache_stats['hits'] / $total_cache_requests) * 100 : 0;
|
||||
|
||||
// Calculate AJAX averages
|
||||
$avg_ajax_response = !empty($ajax_metrics)
|
||||
? array_sum(array_column($ajax_metrics, 'ajax_response_time_ms')) / count($ajax_metrics)
|
||||
: 0;
|
||||
|
||||
return [
|
||||
'status' => 'active',
|
||||
'targets' => [
|
||||
'overhead_percent' => self::TARGET_OVERHEAD_PERCENT,
|
||||
'ajax_response_ms' => self::TARGET_AJAX_RESPONSE_MS,
|
||||
'cache_hit_rate' => self::TARGET_CACHE_HIT_RATE
|
||||
],
|
||||
'current' => [
|
||||
'avg_overhead_percent' => round($avg_overhead, 2),
|
||||
'avg_execution_time_ms' => round($avg_execution, 2),
|
||||
'avg_memory_usage_mb' => round($avg_memory / (1024 * 1024), 2),
|
||||
'cache_hit_rate_percent' => round($cache_hit_rate, 2),
|
||||
'avg_ajax_response_ms' => round($avg_ajax_response, 2)
|
||||
],
|
||||
'performance_score' => self::calculate_performance_score($avg_overhead, $avg_ajax_response, $cache_hit_rate),
|
||||
'measurements_count' => count($metrics),
|
||||
'last_measurement' => max(array_column($metrics, 'timestamp'))
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate overall performance score (0-100)
|
||||
*
|
||||
* @param float $overhead_percent Current overhead percentage
|
||||
* @param float $ajax_response_ms Current AJAX response time
|
||||
* @param float $cache_hit_rate Current cache hit rate
|
||||
* @return int Performance score
|
||||
*/
|
||||
private static function calculate_performance_score($overhead_percent, $ajax_response_ms, $cache_hit_rate)
|
||||
{
|
||||
$score = 100;
|
||||
|
||||
// Deduct points for overhead (target <2%)
|
||||
if ($overhead_percent > self::TARGET_OVERHEAD_PERCENT) {
|
||||
$score -= min(30, ($overhead_percent - self::TARGET_OVERHEAD_PERCENT) * 10);
|
||||
}
|
||||
|
||||
// Deduct points for AJAX response time (target <100ms)
|
||||
if ($ajax_response_ms > self::TARGET_AJAX_RESPONSE_MS) {
|
||||
$score -= min(30, ($ajax_response_ms - self::TARGET_AJAX_RESPONSE_MS) / 10);
|
||||
}
|
||||
|
||||
// Deduct points for cache hit rate (target >95%)
|
||||
if ($cache_hit_rate < self::TARGET_CACHE_HIT_RATE) {
|
||||
$score -= min(25, (self::TARGET_CACHE_HIT_RATE - $cache_hit_rate));
|
||||
}
|
||||
|
||||
return max(0, (int) $score);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should track performance based on current context
|
||||
*
|
||||
* @return bool True if should track
|
||||
*/
|
||||
private static function should_track_performance()
|
||||
{
|
||||
// Don't track in admin area unless specifically enabled
|
||||
if (is_admin() && !defined('CARE_BOOKING_TRACK_ADMIN_PERFORMANCE')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't track for bots and crawlers
|
||||
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
if (preg_match('/bot|crawler|spider|robot/i', $user_agent)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output debug information
|
||||
*
|
||||
* @param array $metrics Performance metrics
|
||||
*/
|
||||
private static function output_debug_info($metrics)
|
||||
{
|
||||
echo "\n<!-- Care Booking Performance Debug -->\n";
|
||||
echo "<!-- Execution Time: {$metrics['execution_time_ms']}ms -->\n";
|
||||
echo "<!-- Memory Usage: " . size_format($metrics['memory_usage_bytes']) . " -->\n";
|
||||
echo "<!-- Page Overhead: {$metrics['overhead_percent']}% -->\n";
|
||||
echo "<!-- Target Overhead: " . self::TARGET_OVERHEAD_PERCENT . "% -->\n";
|
||||
|
||||
$status = $metrics['overhead_percent'] <= self::TARGET_OVERHEAD_PERCENT ? 'MEETING TARGET' : 'EXCEEDING TARGET';
|
||||
echo "<!-- Performance Status: {$status} -->\n";
|
||||
echo "<!-- End Care Booking Performance Debug -->\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance notices for admin display
|
||||
*
|
||||
* @return array Performance notices
|
||||
*/
|
||||
public static function get_performance_notices()
|
||||
{
|
||||
return get_transient('care_booking_performance_notices') ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear performance notices
|
||||
*/
|
||||
public static function clear_performance_notices()
|
||||
{
|
||||
delete_transient('care_booking_performance_notices');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset optimization statistics
|
||||
*
|
||||
* @return array Asset optimization stats
|
||||
*/
|
||||
public static function get_asset_stats()
|
||||
{
|
||||
$asset_files = [
|
||||
'admin_css' => [
|
||||
'original' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'admin/css/admin-style.css',
|
||||
'minified' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'admin/css/admin-style.min.css'
|
||||
],
|
||||
'admin_js' => [
|
||||
'original' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'admin/js/admin-script.js',
|
||||
'minified' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'admin/js/admin-script.min.js'
|
||||
],
|
||||
'frontend_css' => [
|
||||
'original' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'public/css/frontend.css',
|
||||
'minified' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'public/css/frontend.min.css'
|
||||
],
|
||||
'frontend_js' => [
|
||||
'original' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'public/js/frontend.js',
|
||||
'minified' => CARE_BOOKING_BLOCK_PLUGIN_DIR . 'public/js/frontend.min.js'
|
||||
]
|
||||
];
|
||||
|
||||
$stats = [];
|
||||
$total_original = 0;
|
||||
$total_minified = 0;
|
||||
|
||||
foreach ($asset_files as $key => $files) {
|
||||
$original_size = file_exists($files['original']) ? filesize($files['original']) : 0;
|
||||
$minified_size = file_exists($files['minified']) ? filesize($files['minified']) : 0;
|
||||
|
||||
$savings_bytes = $original_size - $minified_size;
|
||||
$savings_percent = $original_size > 0 ? ($savings_bytes / $original_size) * 100 : 0;
|
||||
|
||||
$stats[$key] = [
|
||||
'original_size' => $original_size,
|
||||
'minified_size' => $minified_size,
|
||||
'savings_bytes' => $savings_bytes,
|
||||
'savings_percent' => round($savings_percent, 1)
|
||||
];
|
||||
|
||||
$total_original += $original_size;
|
||||
$total_minified += $minified_size;
|
||||
}
|
||||
|
||||
$total_savings = $total_original - $total_minified;
|
||||
$total_savings_percent = $total_original > 0 ? ($total_savings / $total_original) * 100 : 0;
|
||||
|
||||
$stats['total'] = [
|
||||
'original_size' => $total_original,
|
||||
'minified_size' => $total_minified,
|
||||
'savings_bytes' => $total_savings,
|
||||
'savings_percent' => round($total_savings_percent, 1)
|
||||
];
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize performance monitoring
|
||||
add_action('plugins_loaded', [Care_Booking_Performance_Monitor::class, 'init'], 5);
|
||||
475
care-booking-block/includes/class-restriction-model.php
Normal file
475
care-booking-block/includes/class-restriction-model.php
Normal file
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Restriction model for Care Booking Block plugin
|
||||
*
|
||||
* @package CareBookingBlock
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restriction model class
|
||||
*/
|
||||
class Care_Booking_Restriction_Model
|
||||
{
|
||||
/**
|
||||
* Database handler instance
|
||||
*
|
||||
* @var Care_Booking_Database_Handler
|
||||
*/
|
||||
private $db_handler;
|
||||
|
||||
/**
|
||||
* Cache manager instance
|
||||
*
|
||||
* @var Care_Booking_Cache_Manager
|
||||
*/
|
||||
private $cache_manager;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->db_handler = new Care_Booking_Database_Handler();
|
||||
$this->cache_manager = new Care_Booking_Cache_Manager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new restriction
|
||||
*
|
||||
* @param array $data Restriction data
|
||||
* @return int|false Restriction ID on success, false on failure
|
||||
*/
|
||||
public function create($data)
|
||||
{
|
||||
// Validate data
|
||||
if (!$this->validate_restriction_data($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if restriction already exists
|
||||
$existing = $this->find_existing(
|
||||
$data['restriction_type'],
|
||||
$data['target_id'],
|
||||
isset($data['doctor_id']) ? $data['doctor_id'] : null
|
||||
);
|
||||
|
||||
if ($existing) {
|
||||
// Update existing restriction
|
||||
return $this->update($existing->id, $data) ? (int) $existing->id : false;
|
||||
}
|
||||
|
||||
// Create new restriction
|
||||
$result = $this->db_handler->insert($data);
|
||||
|
||||
if ($result) {
|
||||
// Invalidate cache
|
||||
$this->invalidate_cache();
|
||||
|
||||
// Trigger action
|
||||
do_action(
|
||||
'care_booking_restriction_created',
|
||||
$data['restriction_type'],
|
||||
$data['target_id'],
|
||||
isset($data['doctor_id']) ? $data['doctor_id'] : null
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restriction by ID
|
||||
*
|
||||
* @param int $id Restriction ID
|
||||
* @return object|false Restriction object or false if not found
|
||||
*/
|
||||
public function get($id)
|
||||
{
|
||||
return $this->db_handler->get($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update restriction
|
||||
*
|
||||
* @param int $id Restriction ID
|
||||
* @param array $data Update data
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function update($id, $data)
|
||||
{
|
||||
// Validate update data
|
||||
if (!$this->validate_update_data($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = $this->db_handler->update($id, $data);
|
||||
|
||||
if ($result) {
|
||||
// Invalidate cache
|
||||
$this->invalidate_cache();
|
||||
|
||||
// Get updated restriction for action
|
||||
$restriction = $this->get($id);
|
||||
if ($restriction) {
|
||||
// Trigger action
|
||||
do_action(
|
||||
'care_booking_restriction_updated',
|
||||
$restriction->restriction_type,
|
||||
$restriction->target_id,
|
||||
$restriction->doctor_id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete restriction
|
||||
*
|
||||
* @param int $id Restriction ID
|
||||
* @return bool True on success, false on failure
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
// Get restriction before deletion for action
|
||||
$restriction = $this->get($id);
|
||||
|
||||
$result = $this->db_handler->delete($id);
|
||||
|
||||
if ($result && $restriction) {
|
||||
// Invalidate cache
|
||||
$this->invalidate_cache();
|
||||
|
||||
// Trigger action
|
||||
do_action(
|
||||
'care_booking_restriction_deleted',
|
||||
$restriction->restriction_type,
|
||||
$restriction->target_id,
|
||||
$restriction->doctor_id
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get restrictions by type
|
||||
*
|
||||
* @param string $type Restriction type ('doctor' or 'service')
|
||||
* @return array Array of restriction objects
|
||||
*/
|
||||
public function get_by_type($type)
|
||||
{
|
||||
return $this->db_handler->get_by_type($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all restrictions
|
||||
*
|
||||
* @return array Array of restriction objects
|
||||
*/
|
||||
public function get_all()
|
||||
{
|
||||
return $this->db_handler->get_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked doctors (with caching)
|
||||
*
|
||||
* @return array Array of blocked doctor IDs
|
||||
*/
|
||||
public function get_blocked_doctors()
|
||||
{
|
||||
// Try to get from cache first
|
||||
$blocked_doctors = $this->cache_manager->get_blocked_doctors();
|
||||
|
||||
if ($blocked_doctors === false) {
|
||||
// Cache miss - get from database
|
||||
$blocked_doctors = $this->db_handler->get_blocked_doctors();
|
||||
|
||||
// Cache the result
|
||||
$this->cache_manager->set_blocked_doctors($blocked_doctors);
|
||||
}
|
||||
|
||||
return $blocked_doctors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blocked services for specific doctor (with caching)
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return array Array of blocked service IDs
|
||||
*/
|
||||
public function get_blocked_services($doctor_id)
|
||||
{
|
||||
// Try to get from cache first
|
||||
$blocked_services = $this->cache_manager->get_blocked_services($doctor_id);
|
||||
|
||||
if ($blocked_services === false) {
|
||||
// Cache miss - get from database
|
||||
$blocked_services = $this->db_handler->get_blocked_services($doctor_id);
|
||||
|
||||
// Cache the result
|
||||
$this->cache_manager->set_blocked_services($doctor_id, $blocked_services);
|
||||
}
|
||||
|
||||
return $blocked_services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find existing restriction
|
||||
*
|
||||
* @param string $type Restriction type
|
||||
* @param int $target_id Target ID
|
||||
* @param int $doctor_id Doctor ID (for service restrictions)
|
||||
* @return object|false Restriction object or false if not found
|
||||
*/
|
||||
public function find_existing($type, $target_id, $doctor_id = null)
|
||||
{
|
||||
return $this->db_handler->find_existing($type, $target_id, $doctor_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle restriction (create if not exists, update if exists)
|
||||
*
|
||||
* @param string $type Restriction type
|
||||
* @param int $target_id Target ID
|
||||
* @param int $doctor_id Doctor ID (for service restrictions)
|
||||
* @param bool $is_blocked Whether to block or unblock
|
||||
* @return int|bool Restriction ID if created, true if updated, false on failure
|
||||
*/
|
||||
public function toggle($type, $target_id, $doctor_id = null, $is_blocked = true)
|
||||
{
|
||||
// Validate parameters
|
||||
if (!in_array($type, ['doctor', 'service'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type === 'service' && !$doctor_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if restriction exists
|
||||
$existing = $this->find_existing($type, $target_id, $doctor_id);
|
||||
|
||||
if ($existing) {
|
||||
// Update existing restriction
|
||||
return $this->update($existing->id, ['is_blocked' => $is_blocked]);
|
||||
} else {
|
||||
// Create new restriction
|
||||
$data = [
|
||||
'restriction_type' => $type,
|
||||
'target_id' => $target_id,
|
||||
'is_blocked' => $is_blocked
|
||||
];
|
||||
|
||||
if ($doctor_id) {
|
||||
$data['doctor_id'] = $doctor_id;
|
||||
}
|
||||
|
||||
return $this->create($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk create restrictions
|
||||
*
|
||||
* @param array $restrictions Array of restriction data
|
||||
* @return array Array of results (IDs for successful, false for failed)
|
||||
*/
|
||||
public function bulk_create($restrictions)
|
||||
{
|
||||
if (!is_array($restrictions) || empty($restrictions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($restrictions as $restriction_data) {
|
||||
$result = $this->create($restriction_data);
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk toggle restrictions
|
||||
*
|
||||
* @param array $restrictions Array of restriction toggle data
|
||||
* @return array Array of results with success/error information
|
||||
*/
|
||||
public function bulk_toggle($restrictions)
|
||||
{
|
||||
if (!is_array($restrictions) || empty($restrictions)) {
|
||||
return ['updated' => 0, 'errors' => []];
|
||||
}
|
||||
|
||||
$updated = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($restrictions as $restriction_data) {
|
||||
try {
|
||||
// Validate required fields
|
||||
if (!isset($restriction_data['restriction_type']) || !isset($restriction_data['target_id'])) {
|
||||
$errors[] = [
|
||||
'restriction' => $restriction_data,
|
||||
'error' => 'Missing required fields'
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = $this->toggle(
|
||||
$restriction_data['restriction_type'],
|
||||
$restriction_data['target_id'],
|
||||
isset($restriction_data['doctor_id']) ? $restriction_data['doctor_id'] : null,
|
||||
isset($restriction_data['is_blocked']) ? $restriction_data['is_blocked'] : true
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$updated++;
|
||||
} else {
|
||||
$errors[] = [
|
||||
'restriction' => $restriction_data,
|
||||
'error' => 'Failed to update restriction'
|
||||
];
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errors[] = [
|
||||
'restriction' => $restriction_data,
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'updated' => $updated,
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if doctor is blocked
|
||||
*
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return bool True if blocked, false otherwise
|
||||
*/
|
||||
public function is_doctor_blocked($doctor_id)
|
||||
{
|
||||
$blocked_doctors = $this->get_blocked_doctors();
|
||||
return in_array((int) $doctor_id, $blocked_doctors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if service is blocked for specific doctor
|
||||
*
|
||||
* @param int $service_id Service ID
|
||||
* @param int $doctor_id Doctor ID
|
||||
* @return bool True if blocked, false otherwise
|
||||
*/
|
||||
public function is_service_blocked($service_id, $doctor_id)
|
||||
{
|
||||
$blocked_services = $this->get_blocked_services($doctor_id);
|
||||
return in_array((int) $service_id, $blocked_services);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate restriction data
|
||||
*
|
||||
* @param array $data Restriction data to validate
|
||||
* @return bool True if valid, false otherwise
|
||||
*/
|
||||
private function validate_restriction_data($data)
|
||||
{
|
||||
// Check required fields
|
||||
if (!isset($data['restriction_type']) || !isset($data['target_id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate restriction type
|
||||
if (!in_array($data['restriction_type'], ['doctor', 'service'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate target_id
|
||||
if (!is_numeric($data['target_id']) || (int) $data['target_id'] <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Service restrictions require doctor_id
|
||||
if ($data['restriction_type'] === 'service') {
|
||||
if (!isset($data['doctor_id']) || !is_numeric($data['doctor_id']) || (int) $data['doctor_id'] <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate update data
|
||||
*
|
||||
* @param array $data Update data to validate
|
||||
* @return bool True if valid, false otherwise
|
||||
*/
|
||||
private function validate_update_data($data)
|
||||
{
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate restriction_type if provided
|
||||
if (isset($data['restriction_type']) && !in_array($data['restriction_type'], ['doctor', 'service'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate target_id if provided
|
||||
if (isset($data['target_id']) && (!is_numeric($data['target_id']) || (int) $data['target_id'] <= 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate doctor_id if provided
|
||||
if (isset($data['doctor_id']) && (!is_numeric($data['doctor_id']) || (int) $data['doctor_id'] <= 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all related caches
|
||||
*/
|
||||
private function invalidate_cache()
|
||||
{
|
||||
$this->cache_manager->invalidate_all();
|
||||
|
||||
// Trigger cache invalidation action
|
||||
do_action('care_booking_cache_invalidated');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics
|
||||
*
|
||||
* @return array Array of statistics
|
||||
*/
|
||||
public function get_statistics()
|
||||
{
|
||||
return [
|
||||
'total_restrictions' => count($this->get_all()),
|
||||
'doctor_restrictions' => count($this->get_by_type('doctor')),
|
||||
'service_restrictions' => count($this->get_by_type('service')),
|
||||
'blocked_doctors' => count($this->get_blocked_doctors())
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user