1, self::SEVERITY_HIGH => 3, self::SEVERITY_MEDIUM => 10, self::SEVERITY_LOW => 50 ]; public function __construct() { $this->CI = &get_instance(); $this->CI->load->model('desk_moloni_model'); $this->model = $this->CI->desk_moloni_model; log_activity('ErrorHandler initialized'); } /** * Log error with context and severity * * @param string $category * @param string $error_code * @param string $message * @param array $context * @param string $severity * @return int Error log ID */ public function log_error($category, $error_code, $message, $context = [], $severity = self::SEVERITY_MEDIUM) { try { // Validate inputs if (!$this->is_valid_category($category)) { $category = self::CATEGORY_SYSTEM; } if (!$this->is_valid_severity($severity)) { $severity = self::SEVERITY_MEDIUM; } // Prepare error data $error_data = [ 'category' => $category, 'error_code' => $error_code, 'severity' => $severity, 'message' => $this->sanitize_message($message), 'context' => json_encode($this->sanitize_context($context)), 'stack_trace' => $this->get_sanitized_stack_trace(), 'occurred_at' => date('Y-m-d H:i:s'), 'user_id' => get_staff_user_id() ?: null, 'ip_address' => $this->CI->input->ip_address(), 'user_agent' => $this->CI->input->user_agent(), 'request_uri' => $this->CI->uri->uri_string(), 'memory_usage' => memory_get_usage(true), 'peak_memory' => memory_get_peak_usage(true), 'processing_time' => $this->get_processing_time() ]; // Store error in database $error_id = $this->model->log_error($error_data); // Log to file system as backup $this->log_to_file($error_data); // Check if notification is needed $this->check_notification_threshold($category, $severity, $error_code); // Trigger hooks for error handling hooks()->do_action('desk_moloni_error_logged', $error_id, $error_data); return $error_id; } catch (\Exception $e) { // Fallback error logging log_message('error', 'ErrorHandler failed: ' . $e->getMessage()); error_log("DeskMoloni Error Handler Failure: {$e->getMessage()}"); return false; } } /** * Log API error with specific handling * * @param string $endpoint * @param int $status_code * @param string $response_body * @param array $request_data * @param string $error_message * @return int */ public function log_api_error($endpoint, $status_code, $response_body, $request_data = [], $error_message = '') { $error_code = $this->determine_api_error_code($status_code, $response_body); $severity = $this->determine_api_error_severity($status_code, $error_code); $context = [ 'endpoint' => $endpoint, 'status_code' => $status_code, 'response_body' => $this->truncate_response_body($response_body), 'request_data' => $this->sanitize_request_data($request_data), 'response_headers' => $this->get_last_response_headers() ]; $message = $error_message ?: "API request failed: {$endpoint} returned {$status_code}"; return $this->log_error(self::CATEGORY_API, $error_code, $message, $context, $severity); } /** * Log sync error with entity context * * @param string $entity_type * @param int $entity_id * @param string $direction * @param string $error_message * @param array $additional_context * @return int */ public function log_sync_error($entity_type, $entity_id, $direction, $error_message, $additional_context = []) { $error_code = $this->determine_sync_error_code($error_message); $severity = $this->determine_sync_error_severity($error_code, $entity_type); $context = array_merge([ 'entity_type' => $entity_type, 'entity_id' => $entity_id, 'sync_direction' => $direction, 'sync_attempt' => $additional_context['attempt'] ?? 1 ], $additional_context); return $this->log_error(self::CATEGORY_SYNC, $error_code, $error_message, $context, $severity); } /** * Log validation error * * @param string $field_name * @param mixed $field_value * @param string $validation_rule * @param string $entity_type * @return int */ public function log_validation_error($field_name, $field_value, $validation_rule, $entity_type = null) { $context = [ 'field_name' => $field_name, 'field_value' => $this->sanitize_field_value($field_value), 'validation_rule' => $validation_rule, 'entity_type' => $entity_type ]; $message = "Validation failed for field '{$field_name}' with rule '{$validation_rule}'"; return $this->log_error( self::CATEGORY_VALIDATION, self::ERROR_SYNC_VALIDATION, $message, $context, self::SEVERITY_LOW ); } /** * Get error statistics * * @param array $filters * @return array */ public function get_error_statistics($filters = []) { return [ 'total_errors' => $this->model->count_errors($filters), 'by_category' => $this->model->count_errors_by_category($filters), 'by_severity' => $this->model->count_errors_by_severity($filters), 'by_error_code' => $this->model->count_errors_by_code($filters), 'recent_errors' => $this->model->get_recent_errors(10, $filters), 'error_trends' => $this->model->get_error_trends($filters), 'top_error_codes' => $this->model->get_top_error_codes(10, $filters) ]; } /** * Get errors by criteria * * @param array $criteria * @param int $limit * @param int $offset * @return array */ public function get_errors($criteria = [], $limit = 50, $offset = 0) { return $this->model->get_errors($criteria, $limit, $offset); } /** * Mark error as resolved * * @param int $error_id * @param string $resolution_notes * @param int $resolved_by * @return bool */ public function mark_error_resolved($error_id, $resolution_notes = '', $resolved_by = null) { $resolution_data = [ 'resolved' => 1, 'resolved_at' => date('Y-m-d H:i:s'), 'resolved_by' => $resolved_by ?: get_staff_user_id(), 'resolution_notes' => $resolution_notes ]; $result = $this->model->update_error($error_id, $resolution_data); if ($result) { log_activity("Error #{$error_id} marked as resolved"); hooks()->do_action('desk_moloni_error_resolved', $error_id, $resolution_data); } return $result; } /** * Bulk mark errors as resolved * * @param array $error_ids * @param string $resolution_notes * @return array */ public function bulk_mark_resolved($error_ids, $resolution_notes = '') { $results = [ 'total' => count($error_ids), 'success' => 0, 'errors' => 0 ]; foreach ($error_ids as $error_id) { if ($this->mark_error_resolved($error_id, $resolution_notes)) { $results['success']++; } else { $results['errors']++; } } return $results; } /** * Clean up old errors * * @param int $retention_days * @param bool $keep_critical * @return int */ public function cleanup_old_errors($retention_days = 90, $keep_critical = true) { $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$retention_days} days")); $criteria = [ 'occurred_before' => $cutoff_date, 'resolved' => 1 ]; if ($keep_critical) { $criteria['exclude_severity'] = self::SEVERITY_CRITICAL; } $deleted = $this->model->delete_errors($criteria); if ($deleted > 0) { log_activity("Cleaned up {$deleted} old error logs older than {$retention_days} days"); } return $deleted; } /** * Export errors to CSV * * @param array $filters * @param int $limit * @return string */ public function export_errors_csv($filters = [], $limit = 1000) { $errors = $this->model->get_errors($filters, $limit); $output = fopen('php://temp', 'r+'); // CSV Header fputcsv($output, [ 'ID', 'Category', 'Error Code', 'Severity', 'Message', 'Occurred At', 'Resolved', 'User ID', 'IP Address', 'Request URI', 'Memory Usage', 'Context' ]); foreach ($errors as $error) { fputcsv($output, [ $error->id, $error->category, $error->error_code, $error->severity, $error->message, $error->occurred_at, $error->resolved ? 'Yes' : 'No', $error->user_id, $error->ip_address, $error->request_uri, $this->format_memory_usage($error->memory_usage), $this->sanitize_context_for_export($error->context) ]); } rewind($output); $csv_content = stream_get_contents($output); fclose($output); return $csv_content; } /** * Check if notification threshold is reached * * @param string $category * @param string $severity * @param string $error_code */ protected function check_notification_threshold($category, $severity, $error_code) { $threshold = $this->notification_thresholds[$severity] ?? 10; // Count recent errors of same type $recent_count = $this->model->count_recent_errors($category, $error_code, 3600); // Last hour if ($recent_count >= $threshold) { $this->trigger_error_notification($category, $severity, $error_code, $recent_count); } } /** * Trigger error notification * * @param string $category * @param string $severity * @param string $error_code * @param int $error_count */ protected function trigger_error_notification($category, $severity, $error_code, $error_count) { $notification_data = [ 'category' => $category, 'severity' => $severity, 'error_code' => $error_code, 'error_count' => $error_count, 'time_period' => '1 hour' ]; // Send email notification if configured if (get_option('desk_moloni_error_notifications') == '1') { $this->send_error_notification_email($notification_data); } // Trigger webhook if configured if (get_option('desk_moloni_error_webhooks') == '1') { $this->trigger_error_webhook($notification_data); } hooks()->do_action('desk_moloni_error_threshold_reached', $notification_data); } /** * Send error notification email * * @param array $notification_data */ protected function send_error_notification_email($notification_data) { $admin_emails = explode(',', get_option('desk_moloni_admin_emails', '')); if (empty($admin_emails)) { return; } $subject = "Desk-Moloni Error Threshold Reached: {$notification_data['error_code']}"; $message = $this->build_error_notification_message($notification_data); foreach ($admin_emails as $email) { $email = trim($email); if (filter_var($email, FILTER_VALIDATE_EMAIL)) { send_mail_template('desk_moloni_error_notification', $email, [ 'subject' => $subject, 'message' => $message, 'notification_data' => $notification_data ]); } } } /** * Determine API error code from response * * @param int $status_code * @param string $response_body * @return string */ protected function determine_api_error_code($status_code, $response_body) { switch ($status_code) { case 401: case 403: return self::ERROR_API_AUTHENTICATION; case 429: return self::ERROR_API_RATE_LIMIT; case 408: case 504: return self::ERROR_API_TIMEOUT; case 0: return self::ERROR_API_CONNECTION; default: if ($status_code >= 500) { return self::ERROR_API_CONNECTION; } elseif ($status_code >= 400) { return self::ERROR_API_INVALID_RESPONSE; } return 'API_UNKNOWN_ERROR'; } } /** * Determine API error severity * * @param int $status_code * @param string $error_code * @return string */ protected function determine_api_error_severity($status_code, $error_code) { if (in_array($error_code, [self::ERROR_API_AUTHENTICATION, self::ERROR_API_CONNECTION])) { return self::SEVERITY_CRITICAL; } if ($error_code === self::ERROR_API_RATE_LIMIT) { return self::SEVERITY_HIGH; } if ($status_code >= 500) { return self::SEVERITY_HIGH; } return self::SEVERITY_MEDIUM; } /** * Sanitize error message * * @param string $message * @return string */ protected function sanitize_message($message) { // Remove sensitive information patterns $patterns = [ '/password[\'"\s]*[:=][\'"\s]*[^\s\'",}]+/i', '/token[\'"\s]*[:=][\'"\s]*[^\s\'",}]+/i', '/key[\'"\s]*[:=][\'"\s]*[^\s\'",}]+/i', '/secret[\'"\s]*[:=][\'"\s]*[^\s\'",}]+/i' ]; $message = preg_replace($patterns, '[REDACTED]', $message); return substr(trim($message), 0, 1000); } /** * Sanitize context data * * @param array $context * @return array */ protected function sanitize_context($context) { $sensitive_keys = ['password', 'token', 'key', 'secret', 'auth', 'credential']; array_walk_recursive($context, function(&$value, $key) use ($sensitive_keys) { if (is_string($key) && in_array(strtolower($key), $sensitive_keys)) { $value = '[REDACTED]'; } }); return $context; } /** * Get sanitized stack trace * * @return string */ protected function get_sanitized_stack_trace() { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10); $clean_trace = []; foreach ($trace as $frame) { $clean_frame = [ 'file' => basename($frame['file'] ?? 'unknown'), 'line' => $frame['line'] ?? 0, 'function' => $frame['function'] ?? 'unknown' ]; if (isset($frame['class'])) { $clean_frame['class'] = $frame['class']; } $clean_trace[] = $clean_frame; } return json_encode($clean_trace); } /** * Validate error category * * @param string $category * @return bool */ protected function is_valid_category($category) { return in_array($category, [ self::CATEGORY_SYNC, self::CATEGORY_API, self::CATEGORY_QUEUE, self::CATEGORY_MAPPING, self::CATEGORY_VALIDATION, self::CATEGORY_AUTHENTICATION, self::CATEGORY_SYSTEM ]); } /** * Validate error severity * * @param string $severity * @return bool */ protected function is_valid_severity($severity) { return in_array($severity, [ self::SEVERITY_LOW, self::SEVERITY_MEDIUM, self::SEVERITY_HIGH, self::SEVERITY_CRITICAL ]); } /** * Log error to file as backup * * @param array $error_data */ protected function log_to_file($error_data) { $log_file = FCPATH . 'uploads/desk_moloni/logs/errors_' . date('Y-m-d') . '.log'; $log_entry = sprintf( "[%s] %s/%s: %s\n", $error_data['occurred_at'], $error_data['category'], $error_data['severity'], $error_data['message'] ); if (!empty($error_data['context'])) { $log_entry .= "Context: " . $error_data['context'] . "\n"; } $log_entry .= "---\n"; file_put_contents($log_file, $log_entry, FILE_APPEND | LOCK_EX); } /** * Get current processing time * * @return float */ protected function get_processing_time() { if (defined('APP_START_TIME')) { return microtime(true) - APP_START_TIME; } return 0; } }