/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ 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; } }