ID; $user_name = $current_user->display_name ?: $current_user->user_login; // Get IP address $ip_address = $this->getClientIp(); // Get user agent $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; // Create log entry $log_entry = [ 'action' => $action, 'description' => $description, 'user_id' => $user_id, 'user_name' => $user_name, 'ip_address' => $ip_address, 'user_agent' => $user_agent, 'additional_data' => json_encode($additional_data), 'created_date' => current_time('mysql'), ]; // Validate log entry structure if (!isset($log_entry['created_date']) || empty($log_entry['created_date'])) { $this->log("Invalid log entry structure for quote {$quote_id}", 'error'); return false; } // Get existing logs data (raw array, not QuoteLog objects) $existing_logs_data = get_post_meta($quote_id, self::LOG_META_KEY, true); if (!is_array($existing_logs_data)) { $existing_logs_data = []; } // Add new log entry $existing_logs_data[] = $log_entry; // Store logs in post meta $result = update_post_meta($quote_id, self::LOG_META_KEY, $existing_logs_data); if ($result) { $this->log("Quote activity logged: {$action} for quote {$quote_id}"); // Allow plugins to perform actions after logging do_action('easy_invoice_quote_activity_logged', $quote_id, $action, $description, $additional_data); return true; } $this->log("Failed to log quote activity: {$action} for quote {$quote_id}", 'error'); return false; } catch (\Exception $e) { $this->log("Exception while logging quote activity: " . $e->getMessage(), 'error'); return false; } } /** * Get logs for a quote * * @since 1.0.0 * @param int $quote_id Quote ID * @param int $limit Number of logs to return (0 for all) * @return array Array of QuoteLog objects */ public function getLogs(int $quote_id, int $limit = 0): array { $logs_data = get_post_meta($quote_id, self::LOG_META_KEY, true); if (!is_array($logs_data)) { return []; } // Filter out any non-array entries and ensure proper structure $valid_logs_data = []; $has_corruption = false; foreach ($logs_data as $log_data) { if (is_array($log_data) && isset($log_data['created_date']) && !empty($log_data['created_date'])) { $valid_logs_data[] = $log_data; } else { $has_corruption = true; } } // If we found corruption, clean it up if ($has_corruption) { $this->cleanupCorruptedLogs($quote_id); } if (empty($valid_logs_data)) { return []; } // Sort by created date (newest first) - sort the raw data before converting to objects usort($valid_logs_data, function($a, $b) { return strtotime($b['created_date']) - strtotime($a['created_date']); }); // Apply limit if specified if ($limit > 0) { $valid_logs_data = array_slice($valid_logs_data, 0, $limit); } // Convert to QuoteLog objects $logs = []; foreach ($valid_logs_data as $log_data) { try { $logs[] = new QuoteLog($log_data); } catch (\Exception $e) { // Skip invalid log entries continue; } } return $logs; } /** * Get latest log for a quote * * @since 1.0.0 * @param int $quote_id Quote ID * @return QuoteLog|null */ public function getLatestLog(int $quote_id): ?QuoteLog { $logs = $this->getLogs($quote_id, 1); return !empty($logs) ? $logs[0] : null; } /** * Get logs by action * * @since 1.0.0 * @param int $quote_id Quote ID * @param string $action Action type * @return array Array of QuoteLog objects */ public function getLogsByAction(int $quote_id, string $action): array { $all_logs = $this->getLogs($quote_id); $filtered_logs = []; foreach ($all_logs as $log) { if ($log->getAction() === $action) { $filtered_logs[] = $log; } } return $filtered_logs; } /** * Clear logs for a quote * * @since 1.0.0 * @param int $quote_id Quote ID * @return bool True if cleared successfully */ public function clearLogs(int $quote_id): bool { $result = delete_post_meta($quote_id, self::LOG_META_KEY); if ($result) { $this->log("Quote logs cleared for quote {$quote_id}"); do_action('easy_invoice_quote_logs_cleared', $quote_id); } return $result; } /** * Clean up corrupted log data * * @since 1.0.0 * @param int $quote_id Quote ID * @return bool True if cleaned successfully */ public function cleanupCorruptedLogs(int $quote_id): bool { $logs_data = get_post_meta($quote_id, self::LOG_META_KEY, true); if (!is_array($logs_data)) { return true; // No data to clean } $valid_logs_data = []; foreach ($logs_data as $log_data) { if (is_array($log_data) && isset($log_data['created_date']) && !empty($log_data['created_date'])) { $valid_logs_data[] = $log_data; } } // Update with only valid log entries $result = update_post_meta($quote_id, self::LOG_META_KEY, $valid_logs_data); if ($result) { $this->log("Cleaned up corrupted logs for quote {$quote_id}"); } return $result; } /** * Get client IP address * * @since 1.0.0 * @return string */ private function getClientIp(): string { $ip_keys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR']; foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return $_SERVER['REMOTE_ADDR'] ?? ''; } /** * Log quote creation * * @since 1.0.0 * @param int $quote_id Quote ID * @param array $additional_data Additional data * @return bool */ public function logCreation(int $quote_id, array $additional_data = []): bool { return $this->logActivity( $quote_id, 'created', __('Quote created', 'easy-invoice'), $additional_data ); } /** * Log quote update * * @since 1.0.0 * @param int $quote_id Quote ID * @param array $changes Array of changes made * @param array $additional_data Additional data * @return bool */ public function logUpdate(int $quote_id, array $changes = [], array $additional_data = []): bool { $description = __('Quote updated', 'easy-invoice'); if (!empty($changes)) { $change_descriptions = []; foreach ($changes as $field => $value) { $change_descriptions[] = ucfirst(easy_invoice_str_replace('_', ' ', $field)) . ': ' . $value; } $description .= ' - ' . implode(', ', $change_descriptions); } return $this->logActivity( $quote_id, 'updated', $description, array_merge(['changes' => $changes], $additional_data) ); } /** * Log quote acceptance * * @since 1.0.0 * @param int $quote_id Quote ID * @param array $additional_data Additional data * @return bool */ public function logAcceptance(int $quote_id, array $additional_data = []): bool { return $this->logActivity( $quote_id, 'accepted', __('Quote accepted by client', 'easy-invoice'), $additional_data ); } /** * Log quote decline * * @since 1.0.0 * @param int $quote_id Quote ID * @param string $reason Decline reason * @param array $additional_data Additional data * @return bool */ public function logDecline(int $quote_id, string $reason = '', array $additional_data = []): bool { $description = __('Quote declined by client', 'easy-invoice'); if (!empty($reason)) { $description .= ' - ' . $reason; } return $this->logActivity( $quote_id, 'declined', $description, array_merge(['reason' => $reason], $additional_data) ); } /** * Log quote sent * * @since 1.0.0 * @param int $quote_id Quote ID * @param string $email Email address * @param array $additional_data Additional data * @return bool */ public function logSent(int $quote_id, string $email = '', array $additional_data = []): bool { $description = __('Quote sent to client', 'easy-invoice'); if (!empty($email)) { $description .= ' - ' . $email; } return $this->logActivity( $quote_id, 'sent', $description, array_merge(['email' => $email], $additional_data) ); } /** * Log quote viewed * * @since 1.0.0 * @param int $quote_id Quote ID * @param array $additional_data Additional data * @return bool */ public function logViewed(int $quote_id, array $additional_data = []): bool { return $this->logActivity( $quote_id, 'viewed', __('Quote viewed by client', 'easy-invoice'), $additional_data ); } /** * Log status change * * @since 1.0.0 * @param int $quote_id Quote ID * @param string $old_status Old status * @param string $new_status New status * @param array $additional_data Additional data * @return bool */ public function logStatusChange(int $quote_id, string $old_status, string $new_status, array $additional_data = []): bool { return $this->logActivity( $quote_id, 'status_changed', sprintf(__('Status changed from %s to %s', 'easy-invoice'), $old_status, $new_status), array_merge([ 'old_status' => $old_status, 'new_status' => $new_status ], $additional_data) ); } /** * Log quote to invoice conversion * * @since 1.0.0 * @param int $quote_id Quote ID * @param int $invoice_id Invoice ID * @param array $additional_data Additional data * @return bool */ public function logConversionToInvoice(int $quote_id, int $invoice_id, array $additional_data = []): bool { return $this->logActivity( $quote_id, 'converted_to_invoice', sprintf(__('Quote converted to invoice #%d', 'easy-invoice'), $invoice_id), array_merge(['invoice_id' => $invoice_id], $additional_data) ); } /** * Log quote to invoice duplication * * @since 1.0.0 * @param int $quote_id Quote ID * @param int $invoice_id Invoice ID * @param array $additional_data Additional data * @return bool */ public function logDuplicationToInvoice(int $quote_id, int $invoice_id, array $additional_data = []): bool { return $this->logActivity( $quote_id, 'duplicated_to_invoice', sprintf(__('Quote duplicated to invoice #%d', 'easy-invoice'), $invoice_id), array_merge(['invoice_id' => $invoice_id], $additional_data) ); } /** * Log quote deletion * * @since 1.0.0 * @param int $quote_id Quote ID * @param array $additional_data Additional data * @return bool */ public function logDeletion(int $quote_id, array $additional_data = []): bool { return $this->logActivity( $quote_id, 'deleted', __('Quote deleted', 'easy-invoice'), $additional_data ); } /** * Log quote restoration * * @since 1.0.0 * @param int $quote_id Quote ID * @param array $additional_data Additional data * @return bool */ public function logRestoration(int $quote_id, array $additional_data = []): bool { return $this->logActivity( $quote_id, 'restored', __('Quote restored', 'easy-invoice'), $additional_data ); } }