- 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>
543 lines
15 KiB
PHP
543 lines
15 KiB
PHP
/**
|
|
* 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;
|
|
}
|
|
} |