false, 'error' => 'Invalid CSRF token']); exit; } } $currentUser = $_SESSION['user']; $userId = $currentUser['user_id']; $isAdmin = $currentUser['is_admin'] ?? false; // Updated controller class that handles partial updates class ApiTicketController { private $ticketModel; private $commentModel; private $auditLog; private $workflowModel; private $envVars; private $userId; private $isAdmin; public function __construct($conn, $envVars = [], $userId = null, $isAdmin = false) { $this->ticketModel = new TicketModel($conn); $this->commentModel = new CommentModel($conn); $this->auditLog = new AuditLogModel($conn); $this->workflowModel = new WorkflowModel($conn); $this->envVars = $envVars; $this->userId = $userId; $this->isAdmin = $isAdmin; } public function update($id, $data) { // First, get the current ticket data to fill in missing fields $currentTicket = $this->ticketModel->getTicketById($id); if (!$currentTicket) { return [ 'success' => false, 'error' => 'Ticket not found' ]; } // Merge current data with updates, keeping existing values for missing fields $updateData = [ 'ticket_id' => $id, 'title' => $data['title'] ?? $currentTicket['title'], 'description' => $data['description'] ?? $currentTicket['description'], 'category' => $data['category'] ?? $currentTicket['category'], 'type' => $data['type'] ?? $currentTicket['type'], 'status' => $data['status'] ?? $currentTicket['status'], 'priority' => isset($data['priority']) ? (int)$data['priority'] : (int)$currentTicket['priority'] ]; // Validate required fields if (empty($updateData['title'])) { return [ 'success' => false, 'error' => 'Title cannot be empty' ]; } // Validate priority range if ($updateData['priority'] < 1 || $updateData['priority'] > 5) { return [ 'success' => false, 'error' => 'Priority must be between 1 and 5' ]; } // Validate status transition using workflow model if ($currentTicket['status'] !== $updateData['status']) { $allowed = $this->workflowModel->isTransitionAllowed( $currentTicket['status'], $updateData['status'], $this->isAdmin ); if (!$allowed) { return [ 'success' => false, 'error' => 'Status transition not allowed: ' . $currentTicket['status'] . ' → ' . $updateData['status'] ]; } } // Update ticket with user tracking $result = $this->ticketModel->updateTicket($updateData, $this->userId); // Handle visibility update if provided if ($result && isset($data['visibility'])) { $visibilityGroups = $data['visibility_groups'] ?? null; // Convert array to comma-separated string if needed if (is_array($visibilityGroups)) { $visibilityGroups = implode(',', array_map('trim', $visibilityGroups)); } $this->ticketModel->updateVisibility($id, $data['visibility'], $visibilityGroups, $this->userId); } if ($result) { // Log ticket update to audit log if ($this->userId) { $this->auditLog->logTicketUpdate($this->userId, $id, $data); } // Discord webhook disabled for updates - only send for new tickets // $this->sendDiscordWebhook($id, $currentTicket, $updateData, $data); return [ 'success' => true, 'status' => $updateData['status'], 'priority' => $updateData['priority'], 'message' => 'Ticket updated successfully' ]; } else { return [ 'success' => false, 'error' => 'Failed to update ticket in database' ]; } } private function sendDiscordWebhook($ticketId, $oldData, $newData, $changedFields) { if (!isset($this->envVars['DISCORD_WEBHOOK_URL']) || empty($this->envVars['DISCORD_WEBHOOK_URL'])) { return; } $webhookUrl = $this->envVars['DISCORD_WEBHOOK_URL']; // Determine what fields actually changed $changes = []; foreach ($changedFields as $field => $newValue) { if ($field === 'ticket_id') continue; // Skip ticket_id $oldValue = $oldData[$field] ?? 'N/A'; if ($oldValue != $newValue) { $changes[] = [ 'name' => ucfirst($field), 'value' => "$oldValue → $newValue", 'inline' => true ]; } } if (empty($changes)) { return; } // Create ticket URL $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 't.lotusguild.org'; $ticketUrl = "{$protocol}://{$host}/ticket/{$ticketId}"; // Determine embed color based on priority $colors = [ 1 => 0xff4d4d, // Red 2 => 0xffa726, // Orange 3 => 0x42a5f5, // Blue 4 => 0x66bb6a, // Green 5 => 0x9e9e9e // Gray ]; $color = $colors[$newData['priority']] ?? 0x3498db; $embed = [ 'title' => '🔄 Ticket Updated', 'description' => "**#{$ticketId}** - " . $newData['title'], 'color' => $color, 'fields' => array_merge($changes, [ [ 'name' => '🔗 View Ticket', 'value' => "[Click here to view]($ticketUrl)", 'inline' => false ] ]), 'footer' => [ 'text' => 'Tinker Tickets' ], 'timestamp' => date('c') ]; $payload = [ 'embeds' => [$embed] ]; // Send webhook $ch = curl_init($webhookUrl); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 10); $webhookResult = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); // Silently handle errors - webhook is optional } } // Create database connection $conn = new mysqli( $GLOBALS['config']['DB_HOST'], $GLOBALS['config']['DB_USER'], $GLOBALS['config']['DB_PASS'], $GLOBALS['config']['DB_NAME'] ); if ($conn->connect_error) { throw new Exception("Database connection failed: " . $conn->connect_error); } // Check request method if ($_SERVER['REQUEST_METHOD'] !== 'POST') { throw new Exception("Method not allowed. Expected POST, got " . $_SERVER['REQUEST_METHOD']); } // Get POST data $input = file_get_contents('php://input'); $data = json_decode($input, true); if (!$data) { throw new Exception("Invalid JSON data received: " . $input); } if (!isset($data['ticket_id'])) { throw new Exception("Missing ticket_id parameter"); } $ticketId = (int)$data['ticket_id']; // Initialize controller $controller = new ApiTicketController($conn, $envVars, $userId, $isAdmin); // Update ticket $result = $controller->update($ticketId, $data); // Close database connection $conn->close(); // Discard any output that might have been generated ob_end_clean(); // Return response header('Content-Type: application/json'); echo json_encode($result); } catch (Exception $e) { // Discard any output that might have been generated ob_end_clean(); // Return error response header('Content-Type: application/json'); http_response_code(500); echo json_encode([ 'success' => false, 'error' => $e->getMessage() ]); } ?>