conn = $conn; $this->ticketModel = new TicketModel($conn); $this->commentModel = new CommentModel($conn); $this->auditLogModel = new AuditLogModel($conn); $this->userModel = new UserModel($conn); $this->workflowModel = new WorkflowModel($conn); $this->templateModel = new TemplateModel($conn); // Load environment variables for Discord webhook $envPath = dirname(__DIR__) . '/.env'; $this->envVars = []; if (file_exists($envPath)) { $lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { if (strpos($line, '=') !== false && strpos($line, '#') !== 0) { list($key, $value) = explode('=', $line, 2); $key = trim($key); $value = trim($value); // Remove surrounding quotes if present if ((substr($value, 0, 1) === '"' && substr($value, -1) === '"') || (substr($value, 0, 1) === "'" && substr($value, -1) === "'")) { $value = substr($value, 1, -1); } $this->envVars[$key] = $value; } } } } public function view($id) { // Get current user $currentUser = $GLOBALS['currentUser'] ?? null; $userId = $currentUser['user_id'] ?? null; // Get ticket data $ticket = $this->ticketModel->getTicketById($id); if (!$ticket) { header("HTTP/1.0 404 Not Found"); echo "Ticket not found"; return; } // Check visibility access if (!$this->ticketModel->canUserAccessTicket($ticket, $currentUser)) { header("HTTP/1.0 403 Forbidden"); echo "Access denied: You do not have permission to view this ticket"; return; } // Get comments for this ticket using CommentModel $comments = $this->commentModel->getCommentsByTicketId($id); // Get timeline for this ticket $timeline = $this->auditLogModel->getTicketTimeline($id); // Get all users for assignment dropdown $allUsers = $this->userModel->getAllUsers(); // Get allowed status transitions for this ticket $allowedTransitions = $this->workflowModel->getAllowedTransitions($ticket['status']); // Make $conn available to view for visibility groups $conn = $this->conn; // Load the view include dirname(__DIR__) . '/views/TicketView.php'; } public function create() { // Get current user $currentUser = $GLOBALS['currentUser'] ?? null; $userId = $currentUser['user_id'] ?? null; // Check if form was submitted if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Handle visibility groups (comes as array from checkboxes) $visibilityGroups = null; if (isset($_POST['visibility_groups']) && is_array($_POST['visibility_groups'])) { $visibilityGroups = implode(',', array_map('trim', $_POST['visibility_groups'])); } $ticketData = [ 'title' => $_POST['title'] ?? '', 'description' => $_POST['description'] ?? '', 'priority' => $_POST['priority'] ?? '4', 'category' => $_POST['category'] ?? 'General', 'type' => $_POST['type'] ?? 'Issue', 'visibility' => $_POST['visibility'] ?? 'public', 'visibility_groups' => $visibilityGroups ]; // Validate input if (empty($ticketData['title'])) { $error = "Title is required"; $templates = $this->templateModel->getAllTemplates(); $conn = $this->conn; // Make $conn available to view include dirname(__DIR__) . '/views/CreateTicketView.php'; return; } // Create ticket with user tracking $result = $this->ticketModel->createTicket($ticketData, $userId); if ($result['success']) { // Log ticket creation to audit log if (isset($GLOBALS['auditLog']) && $userId) { $GLOBALS['auditLog']->logTicketCreate($userId, $result['ticket_id'], $ticketData); } // Send Discord webhook notification for new ticket $this->sendDiscordWebhook($result['ticket_id'], $ticketData); // Redirect to the new ticket header("Location: " . $GLOBALS['config']['BASE_URL'] . "/ticket/" . $result['ticket_id']); exit; } else { $error = $result['error']; $templates = $this->templateModel->getAllTemplates(); $conn = $this->conn; // Make $conn available to view include dirname(__DIR__) . '/views/CreateTicketView.php'; return; } } else { // Get all templates for the template selector $templates = $this->templateModel->getAllTemplates(); $conn = $this->conn; // Make $conn available to view // Display the create ticket form include dirname(__DIR__) . '/views/CreateTicketView.php'; } } public function update($id) { // Get current user $currentUser = $GLOBALS['currentUser'] ?? null; $userId = $currentUser['user_id'] ?? null; // Check if this is an AJAX request if ($_SERVER['REQUEST_METHOD'] === 'POST') { // For AJAX requests, get JSON data $input = file_get_contents('php://input'); $data = json_decode($input, true); // Add ticket_id to the data $data['ticket_id'] = $id; // Validate input data if (empty($data['title'])) { header('Content-Type: application/json'); echo json_encode([ 'success' => false, 'error' => 'Title cannot be empty' ]); return; } // Update ticket with user tracking // Pass expected_updated_at for optimistic locking if provided $expectedUpdatedAt = $data['expected_updated_at'] ?? null; $result = $this->ticketModel->updateTicket($data, $userId, $expectedUpdatedAt); // Log ticket update to audit log if ($result['success'] && isset($GLOBALS['auditLog']) && $userId) { $GLOBALS['auditLog']->logTicketUpdate($userId, $id, $data); } // Return JSON response header('Content-Type: application/json'); if ($result['success']) { echo json_encode([ 'success' => true, 'status' => $data['status'] ]); } else { $response = [ 'success' => false, 'error' => $result['error'] ?? 'Failed to update ticket' ]; if (!empty($result['conflict'])) { $response['conflict'] = true; $response['current_updated_at'] = $result['current_updated_at'] ?? null; } echo json_encode($response); } } else { // For direct access, redirect to view header("Location: " . $GLOBALS['config']['BASE_URL'] . "/ticket/$id"); exit; } } private function sendDiscordWebhook($ticketId, $ticketData) { if (!isset($this->envVars['DISCORD_WEBHOOK_URL']) || empty($this->envVars['DISCORD_WEBHOOK_URL'])) { error_log("Discord webhook URL not configured, skipping webhook for ticket creation"); return; } $webhookUrl = $this->envVars['DISCORD_WEBHOOK_URL']; // Create ticket URL $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 't.lotusguild.org'; $ticketUrl = "{$protocol}://{$host}/ticket/{$ticketId}"; // Map priorities to Discord colors (matching API endpoint) $priorityColors = [ 1 => 0xDC3545, // P1 Critical - Red 2 => 0xFD7E14, // P2 High - Orange 3 => 0x0DCAF0, // P3 Medium - Cyan 4 => 0x198754, // P4 Low - Green 5 => 0x6C757D // P5 Info - Gray ]; // Priority labels for display $priorityLabels = [ 1 => "P1 - Critical", 2 => "P2 - High", 3 => "P3 - Medium", 4 => "P4 - Low", 5 => "P5 - Info" ]; $priority = (int)($ticketData['priority'] ?? 4); $color = $priorityColors[$priority] ?? 0x6C757D; $priorityLabel = $priorityLabels[$priority] ?? "P{$priority}"; $title = $ticketData['title'] ?? 'Untitled'; $category = $ticketData['category'] ?? 'General'; $type = $ticketData['type'] ?? 'Issue'; $status = $ticketData['status'] ?? 'Open'; // Extract hostname from title for cleaner display preg_match('/^\[([^\]]+)\]/', $title, $hostnameMatch); $sourceHost = $hostnameMatch[1] ?? 'Manual'; $embed = [ 'title' => 'New Ticket Created', 'description' => "**#{$ticketId}** - {$title}", 'url' => $ticketUrl, 'color' => $color, 'fields' => [ [ 'name' => 'Priority', 'value' => $priorityLabel, 'inline' => true ], [ 'name' => 'Category', 'value' => $category, 'inline' => true ], [ 'name' => 'Type', 'value' => $type, 'inline' => true ], [ 'name' => 'Status', 'value' => $status, 'inline' => true ], [ 'name' => 'Source', 'value' => $sourceHost, 'inline' => true ] ], 'footer' => [ 'text' => 'Tinker Tickets | Manual Entry' ], '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); if ($curlError) { error_log("Discord webhook cURL error: {$curlError}"); } elseif ($httpCode !== 204 && $httpCode !== 200) { error_log("Discord webhook failed for ticket #{$ticketId}. HTTP Code: {$httpCode}"); } } } ?>