From c90bdc8ac81a91215e980e7393a0a4f755bee11f Mon Sep 17 00:00:00 2001 From: jared Date: Mon, 13 Apr 2026 20:56:10 -0400 Subject: [PATCH] style: auto-fix 1340 phpcs PSR-12 violations via phpcbf; exclude MissingNamespace and SideEffects --- .phpcs.xml | 7 +- api/add_comment.php | 11 +- api/assign_ticket.php | 1 + api/audit_log.php | 57 +++-- api/bootstrap.php | 4 +- api/bulk_operation.php | 3 +- api/check_duplicates.php | 3 +- api/clone_ticket.php | 2 +- api/custom_fields.php | 6 +- api/delete_attachment.php | 2 +- api/delete_comment.php | 2 +- api/download_attachment.php | 2 +- api/export_tickets.php | 15 +- api/generate_api_key.php | 6 +- api/get_comments.php | 1 + api/get_template.php | 6 +- api/get_users.php | 2 +- api/health.php | 1 + api/manage_recurring.php | 9 +- api/manage_templates.php | 12 +- api/manage_workflows.php | 6 +- api/notifications.php | 24 +- api/revoke_api_key.php | 6 +- api/saved_filters.php | 3 +- api/ticket_dependencies.php | 267 +++++++++++---------- api/update_comment.php | 2 +- api/update_ticket.php | 29 ++- api/upload_attachment.php | 2 +- api/user_avatar.php | 1 + api/user_preferences.php | 5 +- api/watch_ticket.php | 2 + config/config.php | 23 +- controllers/CommentController.php | 27 ++- controllers/DashboardController.php | 72 ++++-- controllers/TicketController.php | 17 +- create_ticket_api.php | 29 ++- cron/cleanup_ratelimit.php | 9 +- cron/create_recurring_tickets.php | 15 +- generate_api_key.php | 2 +- helpers/CacheHelper.php | 28 ++- helpers/Database.php | 31 ++- helpers/ErrorHandler.php | 40 +++- helpers/NotificationHelper.php | 30 ++- helpers/OutputHelper.php | 40 +++- helpers/ResponseHelper.php | 34 ++- helpers/SynapseHelper.php | 12 +- helpers/UrlHelper.php | 19 +- index.php | 32 ++- middleware/ApiKeyAuth.php | 20 +- middleware/AuthMiddleware.php | 36 ++- middleware/CsrfMiddleware.php | 19 +- middleware/RateLimitMiddleware.php | 28 ++- middleware/SecurityHeadersMiddleware.php | 10 +- models/ApiKeyModel.php | 31 ++- models/AttachmentModel.php | 38 ++- models/AuditLogModel.php | 82 ++++--- models/BulkOperationsModel.php | 175 ++++++++------ models/CommentModel.php | 56 +++-- models/CustomFieldModel.php | 41 ++-- models/DependencyModel.php | 31 ++- models/RecurringTicketModel.php | 41 ++-- models/SavedFiltersModel.php | 35 ++- models/StatsModel.php | 21 +- models/TemplateModel.php | 28 ++- models/TicketModel.php | 74 +++--- models/UserModel.php | 46 ++-- models/UserPreferencesModel.php | 28 ++- models/WorkflowModel.php | 30 ++- views/CreateTicketView.php | 33 +-- views/DashboardView.php | 293 ++++++++++++----------- views/TicketView.php | 293 +++++++++++++---------- views/admin/ApiKeysView.php | 26 +- views/admin/AuditLogView.php | 112 +++++---- views/admin/CustomFieldsView.php | 14 +- views/admin/RecurringTicketsView.php | 38 +-- views/admin/TemplatesView.php | 19 +- views/admin/UserActivityView.php | 16 +- views/admin/WorkflowDesignerView.php | 41 ++-- views/layout_footer.php | 27 ++- views/layout_header.php | 25 +- 80 files changed, 1674 insertions(+), 1092 deletions(-) diff --git a/.phpcs.xml b/.phpcs.xml index 0ae38c7..1259672 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -11,6 +11,11 @@ - + + + + + + diff --git a/api/add_comment.php b/api/add_comment.php index cc306c5..0b13d40 100644 --- a/api/add_comment.php +++ b/api/add_comment.php @@ -1,4 +1,5 @@ $authorDisplay, 'preview' => mb_strimwidth($commentText, 0, 200, '…')], (int)$userId ); // Add mentioned users to result for frontend - $result['mentions'] = array_map(function($u) { + $result['mentions'] = array_map(function ($u) { return $u['username']; }, $mentionedUsers); } @@ -172,7 +176,6 @@ try { } header('Content-Type: application/json'); echo json_encode($result); - } catch (Exception $e) { // Discard any unexpected output ob_end_clean(); @@ -187,4 +190,4 @@ try { 'success' => false, 'error' => 'An internal error occurred' ]); -} \ No newline at end of file +} diff --git a/api/assign_ticket.php b/api/assign_ticket.php index 7c34d04..c616de5 100644 --- a/api/assign_ticket.php +++ b/api/assign_ticket.php @@ -1,4 +1,5 @@ getFilteredLogs($filters, 10000, 0); @@ -77,13 +92,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { // Build filters $filters = []; - if (isset($_GET['action_type'])) $filters['action_type'] = $_GET['action_type']; - if (isset($_GET['entity_type'])) $filters['entity_type'] = $_GET['entity_type']; - if (isset($_GET['user_id'])) $filters['user_id'] = $_GET['user_id']; - if (isset($_GET['entity_id'])) $filters['entity_id'] = $_GET['entity_id']; - if (isset($_GET['date_from'])) $filters['date_from'] = $_GET['date_from']; - if (isset($_GET['date_to'])) $filters['date_to'] = $_GET['date_to']; - if (isset($_GET['ip_address'])) $filters['ip_address'] = $_GET['ip_address']; + if (isset($_GET['action_type'])) { + $filters['action_type'] = $_GET['action_type']; + } + if (isset($_GET['entity_type'])) { + $filters['entity_type'] = $_GET['entity_type']; + } + if (isset($_GET['user_id'])) { + $filters['user_id'] = $_GET['user_id']; + } + if (isset($_GET['entity_id'])) { + $filters['entity_id'] = $_GET['entity_id']; + } + if (isset($_GET['date_from'])) { + $filters['date_from'] = $_GET['date_from']; + } + if (isset($_GET['date_to'])) { + $filters['date_to'] = $_GET['date_to']; + } + if (isset($_GET['ip_address'])) { + $filters['ip_address'] = $_GET['ip_address']; + } // Get filtered logs $result = $auditLogModel->getFilteredLogs($filters, $limit, $offset); diff --git a/api/bootstrap.php b/api/bootstrap.php index 0f67234..6256ffa 100644 --- a/api/bootstrap.php +++ b/api/bootstrap.php @@ -1,4 +1,5 @@ 0) ? $s : null; }, $ticketIds))); diff --git a/api/check_duplicates.php b/api/check_duplicates.php index 91110d6..19b4d08 100644 --- a/api/check_duplicates.php +++ b/api/check_duplicates.php @@ -1,4 +1,5 @@ fetch_assoc()) { $stmt->close(); // Sort by similarity descending -usort($duplicates, function($a, $b) { +usort($duplicates, function ($a, $b) { return $b['similarity'] - $a['similarity']; }); diff --git a/api/clone_ticket.php b/api/clone_ticket.php index 4d900cd..bb41a16 100644 --- a/api/clone_ticket.php +++ b/api/clone_ticket.php @@ -1,4 +1,5 @@ $result['error'] ?? 'Failed to create cloned ticket' ]); } - } catch (Exception $e) { error_log("Clone ticket API error: " . $e->getMessage()); http_response_code(500); diff --git a/api/custom_fields.php b/api/custom_fields.php index b16bc14..50ca414 100644 --- a/api/custom_fields.php +++ b/api/custom_fields.php @@ -1,4 +1,5 @@ false, 'error' => 'Authentication required']); @@ -107,7 +110,6 @@ try { http_response_code(405); echo json_encode(['success' => false, 'error' => 'Method not allowed']); } - } catch (Exception $e) { error_log("Custom fields API error: " . $e->getMessage()); http_response_code(500); diff --git a/api/delete_attachment.php b/api/delete_attachment.php index 9a111da..3a8ade7 100644 --- a/api/delete_attachment.php +++ b/api/delete_attachment.php @@ -1,4 +1,5 @@ getMessage()); diff --git a/api/download_attachment.php b/api/download_attachment.php index 35763ed..c4f14cf 100644 --- a/api/download_attachment.php +++ b/api/download_attachment.php @@ -1,4 +1,5 @@ date('c'), 'total_tickets' => count($tickets), - 'tickets' => array_map(function($t) { + 'tickets' => array_map(function ($t) { return [ 'ticket_id' => $t['ticket_id'], 'title' => $t['title'], @@ -152,7 +154,6 @@ try { }, $tickets) ], JSON_PRETTY_PRINT); exit; - } elseif ($format === 'full') { // Full single-ticket export: ticket + all comments + audit timeline if (!$singleId) { @@ -177,7 +178,7 @@ try { $rawComments = $commentModel->getCommentsByTicketId($ticket['ticket_id'], false); $timeline = $auditLogModel->getTicketTimeline((string)$ticket['ticket_id']); - $comments = array_map(function($c) { + $comments = array_map(function ($c) { return [ 'comment_id' => $c['comment_id'], 'author' => $c['display_name'] ?? $c['username'] ?? 'Unknown', @@ -188,7 +189,7 @@ try { ]; }, $rawComments); - $timelineOut = array_map(function($row) { + $timelineOut = array_map(function ($row) { $details = $row['details']; if (is_string($details)) { $details = json_decode($details, true) ?? $details; @@ -228,14 +229,12 @@ try { 'timeline' => $timelineOut, ], JSON_PRETTY_PRINT); exit; - } else { header('Content-Type: application/json'); http_response_code(400); echo json_encode(['success' => false, 'error' => 'Invalid format. Use csv, json, or full.']); exit; } - } catch (Exception $e) { error_log("Export tickets API error: " . $e->getMessage()); header('Content-Type: application/json'); diff --git a/api/generate_api_key.php b/api/generate_api_key.php index ed67f47..faca757 100644 --- a/api/generate_api_key.php +++ b/api/generate_api_key.php @@ -1,4 +1,5 @@ $result['key_id'], 'expires_at' => $result['expires_at'] ]); - } catch (Exception $e) { ob_end_clean(); error_log("Generate API key error: " . $e->getMessage()); diff --git a/api/get_comments.php b/api/get_comments.php index 373af4a..26c5756 100644 --- a/api/get_comments.php +++ b/api/get_comments.php @@ -1,4 +1,5 @@ getMessage(), E_ERROR); ErrorHandler::sendErrorResponse('Failed to retrieve template', 500, $e); diff --git a/api/get_users.php b/api/get_users.php index 905a89c..77e0afd 100644 --- a/api/get_users.php +++ b/api/get_users.php @@ -1,4 +1,5 @@ true, 'users' => $users]); - } catch (Exception $e) { error_log("Get users API error: " . $e->getMessage()); http_response_code(500); diff --git a/api/health.php b/api/health.php index c7892b3..60a8677 100644 --- a/api/health.php +++ b/api/health.php @@ -1,4 +1,5 @@ false, 'error' => 'Authentication required']); @@ -130,14 +133,14 @@ try { http_response_code(405); echo json_encode(['success' => false, 'error' => 'Method not allowed']); } - } catch (Exception $e) { error_log("Recurring tickets API error: " . $e->getMessage()); http_response_code(500); echo json_encode(['success' => false, 'error' => 'An internal error occurred']); } -function calculateNextRun($scheduleType, $scheduleDay, $scheduleTime) { +function calculateNextRun($scheduleType, $scheduleDay, $scheduleTime) +{ $now = new DateTime(); $time = $scheduleTime ?: '09:00'; diff --git a/api/manage_templates.php b/api/manage_templates.php index 96e185c..abd9f00 100644 --- a/api/manage_templates.php +++ b/api/manage_templates.php @@ -1,4 +1,5 @@ false, 'error' => 'Authentication required']); @@ -95,7 +98,8 @@ try { $stmt = $conn->prepare("INSERT INTO ticket_templates (template_name, title_template, description_template, category, type, default_priority, is_active) VALUES (?, ?, ?, ?, ?, ?, ?)"); - $stmt->bind_param('sssssii', + $stmt->bind_param( + 'sssssii', $templateName, $titleTemplate, $description, @@ -145,7 +149,8 @@ try { template_name = ?, title_template = ?, description_template = ?, category = ?, type = ?, default_priority = ?, is_active = ? WHERE template_id = ?"); - $stmt->bind_param('sssssiii', + $stmt->bind_param( + 'sssssiii', $templateName, $titleTemplate, $description, @@ -176,7 +181,6 @@ try { http_response_code(405); echo json_encode(['success' => false, 'error' => 'Method not allowed']); } - } catch (Exception $e) { error_log("Template API error: " . $e->getMessage()); http_response_code(500); diff --git a/api/manage_workflows.php b/api/manage_workflows.php index de27db5..9dbeec4 100644 --- a/api/manage_workflows.php +++ b/api/manage_workflows.php @@ -1,4 +1,5 @@ false, 'error' => 'Authentication required']); @@ -188,7 +191,6 @@ try { http_response_code(405); echo json_encode(['success' => false, 'error' => 'Method not allowed']); } - } catch (Exception $e) { error_log("Workflow API error: " . $e->getMessage()); http_response_code(500); diff --git a/api/notifications.php b/api/notifications.php index be7b162..737b2b4 100644 --- a/api/notifications.php +++ b/api/notifications.php @@ -1,4 +1,5 @@ prepare($myTicketsSql); $stmt->bind_param('ii', $userId, $userId); $stmt->execute(); $mtResult = $stmt->get_result(); -while ($mtRow = $mtResult->fetch_assoc()) { $myTicketIds[(int)$mtRow['ticket_id']] = true; $myTicketIds[$mtRow['ticket_id']] = true; } +while ($mtRow = $mtResult->fetch_assoc()) { + $myTicketIds[(int)$mtRow['ticket_id']] = true; + $myTicketIds[$mtRow['ticket_id']] = true; +} $stmt->close(); $watchedSql = "SELECT ticket_id FROM ticket_watchers WHERE user_id = ?"; @@ -83,7 +88,10 @@ $stmt = $conn->prepare($watchedSql); $stmt->bind_param('i', $userId); $stmt->execute(); $wResult = $stmt->get_result(); -while ($wRow = $wResult->fetch_assoc()) { $myTicketIds[(int)$wRow['ticket_id']] = true; $myTicketIds[$wRow['ticket_id']] = true; } +while ($wRow = $wResult->fetch_assoc()) { + $myTicketIds[(int)$wRow['ticket_id']] = true; + $myTicketIds[$wRow['ticket_id']] = true; +} $stmt->close(); // Step B: fetch recent comment audit events not by the current user @@ -113,7 +121,9 @@ foreach ($rawCommentRows as $rawRow) { $tid = (int)$tidRaw; if ($tid > 0 && (isset($myTicketIds[$tid]) || isset($myTicketIds[$tidRaw]))) { $commentRows[] = $rawRow; - if (count($commentRows) >= 15) break; + if (count($commentRows) >= 15) { + break; + } } } @@ -143,7 +153,9 @@ $all = []; $seen = []; foreach (array_merge($assignRows, $commentRows, $statusRows) as $row) { $id = (int)$row['log_id']; - if (isset($seen[$id])) continue; + if (isset($seen[$id])) { + continue; + } $seen[$id] = true; $all[] = $row; } @@ -164,10 +176,10 @@ foreach ($all as $row) { $isRead = $lastSeen && $row['created_at'] <= $lastSeen; // Build human-readable title - $title = match($actionType) { + $title = match ($actionType) { 'assign' => "{$row['actor_name']} assigned ticket #{$ticketId} to you", 'comment' => "{$row['actor_name']} commented on ticket #{$ticketId}", - 'update' => (function() use ($row, $details, $ticketId) { + 'update' => (function () use ($row, $details, $ticketId) { // logTicketUpdate stores delta as {"status": {"from": "Open", "to": "In Progress"}} $from = $details['status']['from'] ?? ($details['old_value'] ?? '?'); $to = $details['status']['to'] ?? ($details['new_value'] ?? '?'); diff --git a/api/revoke_api_key.php b/api/revoke_api_key.php index c980ae3..e7f8d40 100644 --- a/api/revoke_api_key.php +++ b/api/revoke_api_key.php @@ -1,4 +1,5 @@ true, 'message' => 'API key revoked successfully' ]); - } catch (Exception $e) { ob_end_clean(); error_log("Revoke API key error: " . $e->getMessage()); diff --git a/api/saved_filters.php b/api/saved_filters.php index ab2a7cf..a795dac 100644 --- a/api/saved_filters.php +++ b/api/saved_filters.php @@ -1,4 +1,5 @@ false, 'error' => 'Filter not found']); } - } else if (isset($_GET['default'])) { + } elseif (isset($_GET['default'])) { // Get default filter $filter = $filtersModel->getDefaultFilter($userId); apiRespond(['success' => true, 'filter' => $filter]); diff --git a/api/ticket_dependencies.php b/api/ticket_dependencies.php index 17ecc96..945cd3d 100644 --- a/api/ticket_dependencies.php +++ b/api/ticket_dependencies.php @@ -1,4 +1,5 @@ getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); ob_end_clean(); @@ -110,151 +111,151 @@ try { $method = $_SERVER['REQUEST_METHOD']; try { -switch ($method) { - case 'GET': - // Get dependencies for a ticket - $ticketId = $_GET['ticket_id'] ?? null; + switch ($method) { + case 'GET': + // Get dependencies for a ticket + $ticketId = $_GET['ticket_id'] ?? null; - if (!$ticketId) { - ResponseHelper::error('Ticket ID required'); - } - - // Verify user can access this ticket - $ticket = $ticketModel->getTicketById((int)$ticketId); - if (!$ticket || !$ticketModel->canUserAccessTicket($ticket, $currentUser)) { - ResponseHelper::notFound('Ticket not found'); - } - - try { - $dependencies = $dependencyModel->getDependencies($ticketId); - $dependents = $dependencyModel->getDependentTickets($ticketId); - } catch (Exception $e) { - error_log('Query error in ticket_dependencies.php GET: ' . $e->getMessage()); - ResponseHelper::serverError('Failed to retrieve dependencies'); - } - - ResponseHelper::success([ - 'dependencies' => $dependencies, - 'dependents' => $dependents - ]); - break; - - case 'POST': - // Add a new dependency - $data = json_decode(file_get_contents('php://input'), true); - - if (!is_array($data)) { - ResponseHelper::error('Invalid JSON'); - } - - $ticketId = $data['ticket_id'] ?? null; - $dependsOnId = $data['depends_on_id'] ?? null; - $type = $data['dependency_type'] ?? 'blocks'; - - if (!$ticketId || !$dependsOnId) { - ResponseHelper::error('Both ticket_id and depends_on_id are required'); - } - - // Verify user can access both tickets before creating dependency - $srcTicket = $ticketModel->getTicketById((int)$ticketId); - if (!$srcTicket || !$ticketModel->canUserAccessTicket($srcTicket, $currentUser)) { - ResponseHelper::notFound('Ticket not found'); - } - $tgtTicket = $ticketModel->getTicketById((int)$dependsOnId); - if (!$tgtTicket || !$ticketModel->canUserAccessTicket($tgtTicket, $currentUser)) { - ResponseHelper::notFound('Target ticket not found'); - } - - $result = $dependencyModel->addDependency($ticketId, $dependsOnId, $type, $userId); - - if ($result['success']) { - // Log to audit - $auditLog->log($userId, 'create', 'dependency', (string)$result['dependency_id'], [ - 'ticket_id' => $ticketId, - 'depends_on_id' => $dependsOnId, - 'type' => $type - ]); - - ResponseHelper::created($result); - } else { - ResponseHelper::error($result['error']); - } - break; - - case 'DELETE': - // Remove a dependency - $data = json_decode(file_get_contents('php://input'), true); - - if (!is_array($data)) { - ResponseHelper::error('Invalid JSON'); - } - - $dependencyId = $data['dependency_id'] ?? null; - - // Alternative: delete by ticket IDs - if (!$dependencyId && isset($data['ticket_id']) && isset($data['depends_on_id'])) { - $ticketId = $data['ticket_id']; - $dependsOnId = $data['depends_on_id']; - $type = $data['dependency_type'] ?? 'blocks'; - - // Validate dependency type - $validTypes = ['blocks', 'blocked_by', 'relates_to', 'duplicates']; - if (!in_array($type, $validTypes, true)) { - ResponseHelper::error('Invalid dependency type'); + if (!$ticketId) { + ResponseHelper::error('Ticket ID required'); } - // Verify user can access the source ticket + // Verify user can access this ticket + $ticket = $ticketModel->getTicketById((int)$ticketId); + if (!$ticket || !$ticketModel->canUserAccessTicket($ticket, $currentUser)) { + ResponseHelper::notFound('Ticket not found'); + } + + try { + $dependencies = $dependencyModel->getDependencies($ticketId); + $dependents = $dependencyModel->getDependentTickets($ticketId); + } catch (Exception $e) { + error_log('Query error in ticket_dependencies.php GET: ' . $e->getMessage()); + ResponseHelper::serverError('Failed to retrieve dependencies'); + } + + ResponseHelper::success([ + 'dependencies' => $dependencies, + 'dependents' => $dependents + ]); + break; + + case 'POST': + // Add a new dependency + $data = json_decode(file_get_contents('php://input'), true); + + if (!is_array($data)) { + ResponseHelper::error('Invalid JSON'); + } + + $ticketId = $data['ticket_id'] ?? null; + $dependsOnId = $data['depends_on_id'] ?? null; + $type = $data['dependency_type'] ?? 'blocks'; + + if (!$ticketId || !$dependsOnId) { + ResponseHelper::error('Both ticket_id and depends_on_id are required'); + } + + // Verify user can access both tickets before creating dependency $srcTicket = $ticketModel->getTicketById((int)$ticketId); if (!$srcTicket || !$ticketModel->canUserAccessTicket($srcTicket, $currentUser)) { ResponseHelper::notFound('Ticket not found'); } + $tgtTicket = $ticketModel->getTicketById((int)$dependsOnId); + if (!$tgtTicket || !$ticketModel->canUserAccessTicket($tgtTicket, $currentUser)) { + ResponseHelper::notFound('Target ticket not found'); + } - $result = $dependencyModel->removeDependencyByTickets($ticketId, $dependsOnId, $type); + $result = $dependencyModel->addDependency($ticketId, $dependsOnId, $type, $userId); - if ($result) { - $auditLog->log($userId, 'delete', 'dependency', null, [ + if ($result['success']) { + // Log to audit + $auditLog->log($userId, 'create', 'dependency', (string)$result['dependency_id'], [ + 'ticket_id' => $ticketId, + 'depends_on_id' => $dependsOnId, + 'type' => $type + ]); + + ResponseHelper::created($result); + } else { + ResponseHelper::error($result['error']); + } + break; + + case 'DELETE': + // Remove a dependency + $data = json_decode(file_get_contents('php://input'), true); + + if (!is_array($data)) { + ResponseHelper::error('Invalid JSON'); + } + + $dependencyId = $data['dependency_id'] ?? null; + + // Alternative: delete by ticket IDs + if (!$dependencyId && isset($data['ticket_id']) && isset($data['depends_on_id'])) { + $ticketId = $data['ticket_id']; + $dependsOnId = $data['depends_on_id']; + $type = $data['dependency_type'] ?? 'blocks'; + + // Validate dependency type + $validTypes = ['blocks', 'blocked_by', 'relates_to', 'duplicates']; + if (!in_array($type, $validTypes, true)) { + ResponseHelper::error('Invalid dependency type'); + } + + // Verify user can access the source ticket + $srcTicket = $ticketModel->getTicketById((int)$ticketId); + if (!$srcTicket || !$ticketModel->canUserAccessTicket($srcTicket, $currentUser)) { + ResponseHelper::notFound('Ticket not found'); + } + + $result = $dependencyModel->removeDependencyByTickets($ticketId, $dependsOnId, $type); + + if ($result) { + $auditLog->log($userId, 'delete', 'dependency', null, [ 'ticket_id' => $ticketId, 'depends_on_id' => $dependsOnId, 'type' => $type - ]); - ResponseHelper::success([], 'Dependency removed'); + ]); + ResponseHelper::success([], 'Dependency removed'); + } else { + ResponseHelper::error('Failed to remove dependency'); + } + } elseif ($dependencyId) { + // Look up dependency to verify ticket access before deletion + $depLookupSql = "SELECT ticket_id FROM ticket_dependencies WHERE dependency_id = ?"; + $depLookupStmt = $conn->prepare($depLookupSql); + $depLookupStmt->bind_param("i", $dependencyId); + $depLookupStmt->execute(); + $depRow = $depLookupStmt->get_result()->fetch_assoc(); + $depLookupStmt->close(); + + if (!$depRow) { + ResponseHelper::notFound('Dependency not found'); + } + + $depTicket = $ticketModel->getTicketById((int)$depRow['ticket_id']); + if (!$depTicket || !$ticketModel->canUserAccessTicket($depTicket, $currentUser)) { + ResponseHelper::forbidden('Access denied'); + } + + $result = $dependencyModel->removeDependency($dependencyId); + + if ($result) { + $auditLog->log($userId, 'delete', 'dependency', (string)$dependencyId); + ResponseHelper::success([], 'Dependency removed'); + } else { + ResponseHelper::error('Failed to remove dependency'); + } } else { - ResponseHelper::error('Failed to remove dependency'); + ResponseHelper::error('Dependency ID or ticket IDs required'); } - } elseif ($dependencyId) { - // Look up dependency to verify ticket access before deletion - $depLookupSql = "SELECT ticket_id FROM ticket_dependencies WHERE dependency_id = ?"; - $depLookupStmt = $conn->prepare($depLookupSql); - $depLookupStmt->bind_param("i", $dependencyId); - $depLookupStmt->execute(); - $depRow = $depLookupStmt->get_result()->fetch_assoc(); - $depLookupStmt->close(); + break; - if (!$depRow) { - ResponseHelper::notFound('Dependency not found'); - } - - $depTicket = $ticketModel->getTicketById((int)$depRow['ticket_id']); - if (!$depTicket || !$ticketModel->canUserAccessTicket($depTicket, $currentUser)) { - ResponseHelper::forbidden('Access denied'); - } - - $result = $dependencyModel->removeDependency($dependencyId); - - if ($result) { - $auditLog->log($userId, 'delete', 'dependency', (string)$dependencyId); - ResponseHelper::success([], 'Dependency removed'); - } else { - ResponseHelper::error('Failed to remove dependency'); - } - } else { - ResponseHelper::error('Dependency ID or ticket IDs required'); - } - break; - - default: - ResponseHelper::error('Method not allowed', 405); -} + default: + ResponseHelper::error('Method not allowed', 405); + } } catch (Exception $e) { // Log detailed error server-side error_log('Ticket dependencies API error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); diff --git a/api/update_comment.php b/api/update_comment.php index 043b960..6dc1081 100644 --- a/api/update_comment.php +++ b/api/update_comment.php @@ -1,4 +1,5 @@ getMessage()); diff --git a/api/update_ticket.php b/api/update_ticket.php index 29c32c7..e8ae0f5 100644 --- a/api/update_ticket.php +++ b/api/update_ticket.php @@ -1,4 +1,5 @@ conn = $conn; $this->ticketModel = new TicketModel($conn); $this->commentModel = new CommentModel($conn); @@ -74,7 +77,8 @@ try { $this->currentUser = $currentUser; } - public function update($id, $data) { + public function update($id, $data) + { // First, get the current ticket data to fill in missing fields $currentTicket = $this->ticketModel->getTicketById($id); if (!$currentTicket) { @@ -114,7 +118,7 @@ try { 'error' => 'Title cannot be empty' ]; } - + // Validate priority range if ($updateData['priority'] < 1 || $updateData['priority'] > 5) { return [ @@ -122,7 +126,7 @@ try { 'error' => 'Priority must be between 1 and 5' ]; } - + // Validate status transition using workflow model if ($currentTicket['status'] !== $updateData['status']) { $allowed = $this->workflowModel->isTransitionAllowed( @@ -175,7 +179,10 @@ try { $visResult = $this->ticketModel->updateVisibility($id, $data['visibility'], $visibilityGroups, $this->userId); if ($visResult && $this->userId) { $this->auditLog->log( - $this->userId, 'update', 'ticket', (string)$id, + $this->userId, + 'update', + 'ticket', + (string)$id, [ 'field' => 'visibility', 'from' => $currentTicket['visibility'] ?? 'public', @@ -239,7 +246,7 @@ try { 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); @@ -247,11 +254,11 @@ try { if (!$data) { throw new Exception("Invalid JSON data received: " . $input); } - + if (!isset($data['ticket_id'])) { throw new Exception("Missing ticket_id parameter"); } - + $ticketId = trim((string)$data['ticket_id']); // Initialize controller @@ -259,7 +266,7 @@ try { // Update ticket $result = $controller->update($ticketId, $data); - + // Discard any output that might have been generated ob_end_clean(); @@ -276,7 +283,6 @@ try { } header('Content-Type: application/json'); echo json_encode($result); - } catch (Exception $e) { // Discard any output that might have been generated ob_end_clean(); @@ -292,4 +298,3 @@ try { 'error' => 'An internal error occurred' ]); } -?> \ No newline at end of file diff --git a/api/upload_attachment.php b/api/upload_attachment.php index ebc6172..ad60213 100644 --- a/api/upload_attachment.php +++ b/api/upload_attachment.php @@ -1,4 +1,5 @@ $_SESSION['user']['display_name'] ?? $_SESSION['user']['username'], 'uploaded_at' => date('Y-m-d H:i:s') ], 'File uploaded successfully'); - } catch (Exception $e) { // Clean up file on error if (file_exists($targetPath)) { diff --git a/api/user_avatar.php b/api/user_avatar.php index a472a30..0519037 100644 --- a/api/user_avatar.php +++ b/api/user_avatar.php @@ -1,4 +1,5 @@ $value) { $key = trim($key); - if (!in_array($key, $validKeys)) continue; + if (!in_array($key, $validKeys)) { + continue; + } $prefsModel->setPreference($userId, $key, (string)$value); if ($key === 'rows_per_page') { setcookie('ticketsPerPage', $value, ['expires' => time() + (86400 * 365), 'path' => '/', 'httponly' => true, 'secure' => true, 'samesite' => 'Lax']); diff --git a/api/watch_ticket.php b/api/watch_ticket.php index 2e2be25..3571f0a 100644 --- a/api/watch_ticket.php +++ b/api/watch_ticket.php @@ -1,10 +1,12 @@ $value) { if (is_string($value)) { - if ((substr($value, 0, 1) === '"' && substr($value, -1) === '"') || - (substr($value, 0, 1) === "'" && substr($value, -1) === "'")) { + if ( + (substr($value, 0, 1) === '"' && substr($value, -1) === '"') || + (substr($value, 0, 1) === "'" && substr($value, -1) === "'") + ) { $envVars[$key] = substr($value, 1, -1); } } @@ -27,8 +30,10 @@ $GLOBALS['config'] = [ // Asset cache-busting version — auto-computed from key asset mtimes so // browsers always pick up changes on deploy. Override via ASSET_VERSION in .env. - 'ASSET_VERSION' => (function() use ($envVars) { - if (!empty($envVars['ASSET_VERSION'])) return $envVars['ASSET_VERSION']; + 'ASSET_VERSION' => (function () use ($envVars) { + if (!empty($envVars['ASSET_VERSION'])) { + return $envVars['ASSET_VERSION']; + } $files = [ __DIR__ . '/../assets/css/base.css', __DIR__ . '/../assets/css/dashboard.css', @@ -38,7 +43,11 @@ $GLOBALS['config'] = [ __DIR__ . '/../assets/js/ticket.js', ]; $mtime = 0; - foreach ($files as $f) { if (file_exists($f)) $mtime = max($mtime, filemtime($f)); } + foreach ($files as $f) { + if (file_exists($f)) { + $mtime = max($mtime, filemtime($f)); + } + } return $mtime ?: '20260329'; })(), @@ -75,7 +84,8 @@ $GLOBALS['config'] = [ // Set APP_DOMAIN in .env to override 'APP_DOMAIN' => $envVars['APP_DOMAIN'] ?? null, // Allowed hosts for HTTP_HOST validation (comma-separated in .env) - 'ALLOWED_HOSTS' => array_filter(array_map('trim', + 'ALLOWED_HOSTS' => array_filter(array_map( + 'trim', explode(',', $envVars['ALLOWED_HOSTS'] ?? 'localhost,127.0.0.1') )), @@ -143,4 +153,3 @@ date_default_timezone_set($GLOBALS['config']['TIMEZONE']); $now = new DateTime('now', new DateTimeZone($GLOBALS['config']['TIMEZONE'])); $GLOBALS['config']['TIMEZONE_OFFSET'] = $now->getOffset() / 60; // Convert seconds to minutes $GLOBALS['config']['TIMEZONE_ABBREV'] = $now->format('T'); // e.g., "EST", "EDT" -?> \ No newline at end of file diff --git a/controllers/CommentController.php b/controllers/CommentController.php index 2e1b99c..f6d4290 100644 --- a/controllers/CommentController.php +++ b/controllers/CommentController.php @@ -1,23 +1,28 @@ commentModel = new CommentModel($conn); } - - public function getCommentsByTicketId($ticketId) { + + public function getCommentsByTicketId($ticketId) + { return $this->commentModel->getCommentsByTicketId($ticketId); } - - public function addComment($ticketId) { + + public function addComment($ticketId) + { // Check if this is an AJAX request if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Get JSON data $data = json_decode(file_get_contents('php://input'), true); - + // Validate input if (empty($data['comment_text'])) { header('Content-Type: application/json'); @@ -27,10 +32,10 @@ class CommentController { ]); return; } - + // Add comment $result = $this->commentModel->addComment($ticketId, $data); - + // Return JSON response header('Content-Type: application/json'); echo json_encode($result); @@ -40,4 +45,4 @@ class CommentController { exit; } } -} \ No newline at end of file +} diff --git a/controllers/DashboardController.php b/controllers/DashboardController.php index 03c9df2..9fb6fd5 100644 --- a/controllers/DashboardController.php +++ b/controllers/DashboardController.php @@ -1,9 +1,11 @@ conn = $conn; $this->ticketModel = new TicketModel($conn); $this->prefsModel = new UserPreferencesModel($conn); @@ -28,7 +31,8 @@ class DashboardController { /** * Validate and sanitize a date string */ - private function validateDate(?string $date): ?string { + private function validateDate(?string $date): ?string + { if (empty($date)) { return null; } @@ -42,7 +46,8 @@ class DashboardController { /** * Validate priority value (1-5) */ - private function validatePriority($priority): ?int { + private function validatePriority($priority): ?int + { if ($priority === null || $priority === '') { return null; } @@ -53,7 +58,8 @@ class DashboardController { /** * Validate user ID */ - private function validateUserId($userId): ?int { + private function validateUserId($userId): ?int + { if ($userId === null || $userId === '') { return null; } @@ -61,7 +67,8 @@ class DashboardController { return ($val > 0) ? $val : null; } - public function index() { + public function index() + { // Get user ID for preferences $userId = isset($_SESSION['user']['user_id']) ? $_SESSION['user']['user_id'] : null; @@ -73,7 +80,7 @@ class DashboardController { $limit = 15; if ($userId) { $limit = (int)$this->prefsModel->getPreference($userId, 'rows_per_page', 15); - } else if (isset($_COOKIE['ticketsPerPage'])) { + } elseif (isset($_COOKIE['ticketsPerPage'])) { $limit = (int)$_COOKIE['ticketsPerPage']; } $limit = max(1, min(100, $limit)); @@ -98,11 +105,11 @@ class DashboardController { if (isset($_GET['status']) && !empty($_GET['status'])) { // Validate each status in the comma-separated list $requestedStatuses = array_map('trim', explode(',', $_GET['status'])); - $validStatuses = array_filter($requestedStatuses, function($s) { + $validStatuses = array_filter($requestedStatuses, function ($s) { return in_array($s, self::VALID_STATUSES, true); }); $status = !empty($validStatuses) ? implode(',', $validStatuses) : null; - } else if (!isset($_GET['show_all'])) { + } elseif (!isset($_GET['show_all'])) { // Get default status filters from user preferences if ($userId) { $status = $this->prefsModel->getPreference($userId, 'default_status_filters', 'Open,Pending,In Progress'); @@ -124,24 +131,42 @@ class DashboardController { $closedFrom = $this->validateDate($_GET['closed_from'] ?? null); $closedTo = $this->validateDate($_GET['closed_to'] ?? null); - if ($createdFrom) $filters['created_from'] = $createdFrom; - if ($createdTo) $filters['created_to'] = $createdTo; - if ($updatedFrom) $filters['updated_from'] = $updatedFrom; - if ($updatedTo) $filters['updated_to'] = $updatedTo; - if ($closedFrom) $filters['closed_from'] = $closedFrom; - if ($closedTo) $filters['closed_to'] = $closedTo; + if ($createdFrom) { + $filters['created_from'] = $createdFrom; + } + if ($createdTo) { + $filters['created_to'] = $createdTo; + } + if ($updatedFrom) { + $filters['updated_from'] = $updatedFrom; + } + if ($updatedTo) { + $filters['updated_to'] = $updatedTo; + } + if ($closedFrom) { + $filters['closed_from'] = $closedFrom; + } + if ($closedTo) { + $filters['closed_to'] = $closedTo; + } // Validate priority filters; ?priority=N sets exact match (min=max=N) $prioritySingle = $this->validatePriority($_GET['priority'] ?? null); $priorityMin = $prioritySingle ?? $this->validatePriority($_GET['priority_min'] ?? null); $priorityMax = $prioritySingle ?? $this->validatePriority($_GET['priority_max'] ?? null); - if ($priorityMin !== null) $filters['priority_min'] = $priorityMin; - if ($priorityMax !== null) $filters['priority_max'] = $priorityMax; + if ($priorityMin !== null) { + $filters['priority_min'] = $priorityMin; + } + if ($priorityMax !== null) { + $filters['priority_max'] = $priorityMax; + } // Validate user ID filters $createdBy = $this->validateUserId($_GET['created_by'] ?? null); - if ($createdBy !== null) $filters['created_by'] = $createdBy; + if ($createdBy !== null) { + $filters['created_by'] = $createdBy; + } // assigned_to accepts a numeric user ID, 'unassigned', or the special string 'me' $assignedToRaw = $_GET['assigned_to'] ?? null; @@ -151,7 +176,9 @@ class DashboardController { $filters['assigned_to'] = (int)$userId; } else { $assignedTo = $this->validateUserId($assignedToRaw); - if ($assignedTo !== null) $filters['assigned_to'] = $assignedTo; + if ($assignedTo !== null) { + $filters['assigned_to'] = $assignedTo; + } } // Get tickets with pagination, sorting, search, and advanced filters @@ -161,7 +188,7 @@ class DashboardController { $filterOptions = $this->getCategoriesAndTypes(); $categories = $filterOptions['categories']; $types = $filterOptions['types']; - + // Extract data for the view $tickets = $result['tickets']; $totalTickets = $result['total']; @@ -179,7 +206,8 @@ class DashboardController { * * @return array ['categories' => [...], 'types' => [...]] */ - private function getCategoriesAndTypes(): array { + private function getCategoriesAndTypes(): array + { $sql = "SELECT 'category' as field, category as value FROM tickets WHERE category IS NOT NULL UNION SELECT 'type' as field, type as value FROM tickets WHERE type IS NOT NULL @@ -203,6 +231,4 @@ class DashboardController { return ['categories' => $categories, 'types' => $types]; } - } -?> \ No newline at end of file diff --git a/controllers/TicketController.php b/controllers/TicketController.php index 1076220..2e2c0d4 100644 --- a/controllers/TicketController.php +++ b/controllers/TicketController.php @@ -1,4 +1,5 @@ conn = $conn; $this->ticketModel = new TicketModel($conn); $this->commentModel = new CommentModel($conn); @@ -27,8 +30,9 @@ class TicketController { $this->workflowModel = new WorkflowModel($conn); $this->templateModel = new TemplateModel($conn); } - - public function view($id) { + + public function view($id) + { // Get current user $currentUser = $GLOBALS['currentUser'] ?? null; $userId = $currentUser['user_id'] ?? null; @@ -63,7 +67,8 @@ class TicketController { include dirname(__DIR__) . '/views/TicketView.php'; } - public function create() { + public function create() + { // Get current user $currentUser = $GLOBALS['currentUser'] ?? null; $userId = $currentUser['user_id'] ?? null; @@ -154,6 +159,4 @@ class TicketController { include dirname(__DIR__) . '/views/CreateTicketView.php'; } } - } -?> \ No newline at end of file diff --git a/create_ticket_api.php b/create_ticket_api.php index f6e3c48..8ae57af 100644 --- a/create_ticket_api.php +++ b/create_ticket_api.php @@ -1,4 +1,5 @@ $value) { if (is_string($value)) { - if ((substr($value, 0, 1) === '"' && substr($value, -1) === '"') || - (substr($value, 0, 1) === "'" && substr($value, -1) === "'")) { + if ( + (substr($value, 0, 1) === '"' && substr($value, -1) === '"') || + (substr($value, 0, 1) === "'" && substr($value, -1) === "'") + ) { $envVars[$key] = substr($value, 1, -1); } } @@ -101,7 +104,8 @@ if (!is_array($data) || empty($data['title'])) { } // Generate hash from stable components -function generateTicketHash($data) { +function generateTicketHash($data) +{ $title = (string)($data['title'] ?? ''); // Prefer explicit serial from payload; fall back to extracting device path from title @@ -139,10 +143,12 @@ function generateTicketHash($data) { $issueCategory = 'network'; } elseif (stripos($title, 'Ceph') !== false || stripos($title, '[ceph]') !== false) { $issueCategory = 'ceph'; - if (stripos($title, '[cluster-wide]') !== false || + if ( + stripos($title, '[cluster-wide]') !== false || stripos($title, 'HEALTH_ERR') !== false || stripos($title, 'HEALTH_WARN') !== false || - stripos($title, 'cluster usage') !== false) { + stripos($title, 'cluster usage') !== false + ) { $isClusterWide = true; } // Normalize the specific Ceph warning type so different warnings get distinct tickets @@ -343,8 +349,17 @@ $insertStmt = $conn->prepare( "INSERT INTO tickets (ticket_id, title, description, status, priority, category, type, hash, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" ); -$insertStmt->bind_param("ssssssssi", - $ticket_id, $title, $description, $status, $priority, $category, $type, $ticketHash, $userId +$insertStmt->bind_param( + "ssssssssi", + $ticket_id, + $title, + $description, + $status, + $priority, + $category, + $type, + $ticketHash, + $userId ); try { diff --git a/cron/cleanup_ratelimit.php b/cron/cleanup_ratelimit.php index cb346a9..db37f64 100644 --- a/cron/cleanup_ratelimit.php +++ b/cron/cleanup_ratelimit.php @@ -1,14 +1,17 @@ #!/usr/bin/env php > /var/log/recurring_tickets.log 2>&1 - */ + * */ + +10 * * * * / usr / bin / php / path / to / cron / create_recurring_tickets . php >> / var / log / recurring_tickets . log 2 > & 1 + * / // Change to project root directory chdir(dirname(__DIR__)); @@ -20,7 +23,8 @@ require_once 'models/TicketModel.php'; require_once 'models/AuditLogModel.php'; // Log function -function logMessage($message) { +function logMessage($message) +{ echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n"; } @@ -94,7 +98,6 @@ try { logMessage("ERROR: Failed to create ticket - " . ($result['error'] ?? 'Unknown error')); $errors++; } - } catch (Exception $e) { logMessage("ERROR: Exception processing recurring ticket - " . $e->getMessage()); $errors++; @@ -104,7 +107,6 @@ try { logMessage("Completed: Created $created tickets, $errors errors"); $conn->close(); - } catch (Exception $e) { logMessage("FATAL ERROR: " . $e->getMessage()); exit(1); @@ -113,7 +115,8 @@ try { /** * Process template variables */ -function processTemplate($template) { +function processTemplate($template) +{ if (empty($template)) { return $template; } diff --git a/generate_api_key.php b/generate_api_key.php index ab47fd6..d72ca4b 100644 --- a/generate_api_key.php +++ b/generate_api_key.php @@ -1,4 +1,5 @@ close(); echo "Done! Delete this script after use:\n"; echo " rm " . __FILE__ . "\n\n"; -?> diff --git a/helpers/CacheHelper.php b/helpers/CacheHelper.php index 0c13214..de4a22a 100644 --- a/helpers/CacheHelper.php +++ b/helpers/CacheHelper.php @@ -1,11 +1,13 @@ time(), @@ -110,7 +116,8 @@ class CacheHelper { * @param mixed $identifier Unique identifier (null to delete all with prefix) * @return bool Success */ - public static function delete(string $prefix, $identifier = null): bool { + public static function delete(string $prefix, $identifier = null): bool + { if ($identifier !== null) { $key = self::makeKey($prefix, $identifier); unset(self::$memoryCache[$key]); @@ -140,7 +147,8 @@ class CacheHelper { * * @return bool Success */ - public static function clearAll(): bool { + public static function clearAll(): bool + { self::$memoryCache = []; $files = glob(self::getCacheDir() . '/*.json'); @@ -160,7 +168,8 @@ class CacheHelper { * @param int $ttl Time-to-live in seconds * @return mixed Cached or freshly fetched data */ - public static function remember(string $prefix, $identifier, callable $callback, int $ttl = 300) { + public static function remember(string $prefix, $identifier, callable $callback, int $ttl = 300) + { $data = self::get($prefix, $identifier, $ttl); if ($data === null) { @@ -178,7 +187,8 @@ class CacheHelper { * * @param int $maxAge Maximum age in seconds (default 1 hour) */ - public static function cleanup(int $maxAge = 3600): void { + public static function cleanup(int $maxAge = 3600): void + { $files = glob(self::getCacheDir() . '/*.json'); $now = time(); diff --git a/helpers/Database.php b/helpers/Database.php index 211872d..7b75de9 100644 --- a/helpers/Database.php +++ b/helpers/Database.php @@ -1,11 +1,13 @@ close(); self::$connection = null; @@ -71,7 +76,8 @@ class Database { * * @return bool Success */ - public static function beginTransaction(): bool { + public static function beginTransaction(): bool + { return self::getConnection()->begin_transaction(); } @@ -80,7 +86,8 @@ class Database { * * @return bool Success */ - public static function commit(): bool { + public static function commit(): bool + { return self::getConnection()->commit(); } @@ -89,7 +96,8 @@ class Database { * * @return bool Success */ - public static function rollback(): bool { + public static function rollback(): bool + { return self::getConnection()->rollback(); } @@ -101,7 +109,8 @@ class Database { * @param array $params Parameters to bind * @return mysqli_result|bool Query result */ - public static function query(string $sql, string $types = '', array $params = []) { + public static function query(string $sql, string $types = '', array $params = []) + { $conn = self::getConnection(); if (empty($types) || empty($params)) { @@ -130,7 +139,8 @@ class Database { * @param array $params Parameters to bind * @return int Affected rows (-1 on failure) */ - public static function execute(string $sql, string $types = '', array $params = []): int { + public static function execute(string $sql, string $types = '', array $params = []): int + { $conn = self::getConnection(); $stmt = $conn->prepare($sql); @@ -158,7 +168,8 @@ class Database { * * @return int Last insert ID */ - public static function lastInsertId(): int { + public static function lastInsertId(): int + { return self::getConnection()->insert_id; } diff --git a/helpers/ErrorHandler.php b/helpers/ErrorHandler.php index 0c1fe52..e12a0cd 100644 --- a/helpers/ErrorHandler.php +++ b/helpers/ErrorHandler.php @@ -1,11 +1,13 @@ 'ERROR', E_WARNING => 'WARNING', @@ -248,7 +261,8 @@ class ErrorHandler { * @param int $lines Number of lines to return * @return array Log entries */ - public static function getRecentErrors(int $lines = 50): array { + public static function getRecentErrors(int $lines = 50): array + { if (self::$logFile === null || !file_exists(self::$logFile)) { return []; } diff --git a/helpers/NotificationHelper.php b/helpers/NotificationHelper.php index 48dcced..4bece23 100644 --- a/helpers/NotificationHelper.php +++ b/helpers/NotificationHelper.php @@ -1,12 +1,14 @@ 'status_changed', 'ticket_id' => $ticketId, @@ -92,7 +97,8 @@ class NotificationHelper { * @param string|null $authorDisplay Display name of commenter * @param bool $isInternal True if the comment is internal-only */ - public static function sendCommentNotification($ticketId, string $ticketTitle, string $commentText, ?string $authorDisplay = null, bool $isInternal = false): void { + public static function sendCommentNotification($ticketId, string $ticketTitle, string $commentText, ?string $authorDisplay = null, bool $isInternal = false): void + { // Skip if this is an internal-only comment — only the assignee/admin need to know $notifyUsers = self::notifyUsers(); if (empty($notifyUsers)) { @@ -120,7 +126,8 @@ class NotificationHelper { * @param string|null $authorDisplay * @param array $mentionedMatrixIds Matrix user IDs derived from @usernames */ - public static function sendMentionNotification($ticketId, string $ticketTitle, string $commentText, ?string $authorDisplay, array $mentionedMatrixIds): void { + public static function sendMentionNotification($ticketId, string $ticketTitle, string $commentText, ?string $authorDisplay, array $mentionedMatrixIds): void + { if (empty($mentionedMatrixIds)) { return; } @@ -149,7 +156,8 @@ class NotificationHelper { * @param array $extraData Merged into the payload (old_status/new_status, author, etc.) * @param int|null $excludeUserId Don't notify the actor themselves */ - public static function notifyWatchers(\mysqli $conn, $ticketId, string $ticketTitle, string $event, array $extraData = [], ?int $excludeUserId = null): void { + public static function notifyWatchers(\mysqli $conn, $ticketId, string $ticketTitle, string $event, array $extraData = [], ?int $excludeUserId = null): void + { $webhookUrl = $GLOBALS['config']['MATRIX_WEBHOOK_URL'] ?? null; $domain = $GLOBALS['config']['MATRIX_DOMAIN'] ?? null; if (!$webhookUrl || !$domain) { @@ -208,7 +216,8 @@ class NotificationHelper { * @param string|null $assigneeMatrix Matrix user ID of new assignee (to DM) * @param string|null $changedByDisplay */ - public static function sendAssignmentNotification($ticketId, string $ticketTitle, ?string $assigneeName, ?string $assigneeMatrix, ?string $changedByDisplay = null): void { + public static function sendAssignmentNotification($ticketId, string $ticketTitle, ?string $assigneeName, ?string $assigneeMatrix, ?string $changedByDisplay = null): void + { $notifyUsers = self::notifyUsers(); // Also notify the assignee directly if we know their Matrix ID if ($assigneeMatrix && !in_array($assigneeMatrix, $notifyUsers, true)) { @@ -229,4 +238,3 @@ class NotificationHelper { ]); } } -?> diff --git a/helpers/OutputHelper.php b/helpers/OutputHelper.php index 0dcde49..54f7201 100644 --- a/helpers/OutputHelper.php +++ b/helpers/OutputHelper.php @@ -1,11 +1,13 @@ $errors]); } @@ -81,7 +89,8 @@ class ResponseHelper { * * @param string $message Error message */ - public static function serverError($message = 'Internal server error') { + public static function serverError($message = 'Internal server error') + { self::error($message, 500); } @@ -91,7 +100,8 @@ class ResponseHelper { * @param int $retryAfter Seconds until retry is allowed * @param string $message Error message */ - public static function rateLimitExceeded($retryAfter = 60, $message = 'Rate limit exceeded') { + public static function rateLimitExceeded($retryAfter = 60, $message = 'Rate limit exceeded') + { header('Retry-After: ' . $retryAfter); self::error($message, 429, ['retry_after' => $retryAfter]); } @@ -102,14 +112,16 @@ class ResponseHelper { * @param array $data Resource data * @param string $message Success message */ - public static function created($data = [], $message = 'Resource created') { + public static function created($data = [], $message = 'Resource created') + { self::success($data, $message, 201); } /** * Send a no content response (204) */ - public static function noContent() { + public static function noContent() + { http_response_code(204); exit; } diff --git a/helpers/SynapseHelper.php b/helpers/SynapseHelper.php index 7a14dd0..0792503 100644 --- a/helpers/SynapseHelper.php +++ b/helpers/SynapseHelper.php @@ -1,4 +1,5 @@ diff --git a/helpers/UrlHelper.php b/helpers/UrlHelper.php index b2614d1..20ef93b 100644 --- a/helpers/UrlHelper.php +++ b/helpers/UrlHelper.php @@ -1,10 +1,12 @@ index(); break; - + case preg_match('/^\/ticket\/(\d+)$/', $requestPath, $matches): require_once 'controllers/TicketController.php'; $controller = new TicketController($conn); $controller->view($matches[1]); break; - + case $requestPath == '/ticket/create': require_once 'controllers/TicketController.php'; $controller = new TicketController($conn); $controller->create(); break; - + // API Routes - these handle their own database connections case $requestPath == '/api/update_ticket.php': require_once 'api/update_ticket.php'; break; - + case $requestPath == '/api/add_comment.php': require_once 'api/add_comment.php'; break; @@ -376,11 +378,16 @@ switch (true) { ORDER BY tickets_created DESC, tickets_resolved DESC"; $stmt = $conn->prepare($sql); - $stmt->bind_param('ssssssss', - $dateRange['from'], $dateRange['to'], - $dateRange['from'], $dateRange['to'], - $dateRange['from'], $dateRange['to'], - $dateRange['from'], $dateRange['to'] + $stmt->bind_param( + 'ssssssss', + $dateRange['from'], + $dateRange['to'], + $dateRange['from'], + $dateRange['to'], + $dateRange['from'], + $dateRange['to'], + $dateRange['from'], + $dateRange['to'] ); $stmt->execute(); $result = $stmt->get_result(); @@ -398,7 +405,7 @@ switch (true) { case $requestPath == '/dashboard.php': header("Location: /"); exit; - + case preg_match('/^\/ticket\.php/', $requestPath) && isset($_GET['id']): $legacyId = (string)$_GET['id']; if (ctype_digit($legacyId) && (int)$legacyId > 0) { @@ -407,7 +414,7 @@ switch (true) { header("Location: /"); } exit; - + default: http_response_code(404); include __DIR__ . '/views/error_404.php'; @@ -418,4 +425,3 @@ switch (true) { if (isset($conn)) { $conn->close(); } -?> \ No newline at end of file diff --git a/middleware/ApiKeyAuth.php b/middleware/ApiKeyAuth.php index eb47e5d..6b1517a 100644 --- a/middleware/ApiKeyAuth.php +++ b/middleware/ApiKeyAuth.php @@ -1,16 +1,20 @@ conn = $conn; $this->apiKeyModel = new ApiKeyModel($conn); $this->userModel = new UserModel($conn); @@ -22,7 +26,8 @@ class ApiKeyAuth { * @return array User data for system user * @throws Exception if authentication fails */ - public function authenticate() { + public function authenticate() + { // Get Authorization header $authHeader = $this->getAuthorizationHeader(); @@ -67,7 +72,8 @@ class ApiKeyAuth { * * @return string|null Authorization header value */ - private function getAuthorizationHeader() { + private function getAuthorizationHeader() + { // Try different header formats if (isset($_SERVER['HTTP_AUTHORIZATION'])) { return $_SERVER['HTTP_AUTHORIZATION']; @@ -96,7 +102,8 @@ class ApiKeyAuth { * * @param string $message Error message */ - private function sendUnauthorized($message) { + private function sendUnauthorized($message) + { header('HTTP/1.1 401 Unauthorized'); header('Content-Type: application/json'); echo json_encode([ @@ -111,7 +118,8 @@ class ApiKeyAuth { * * @return array|null User data or null if not authenticated */ - public function verifyOptional() { + public function verifyOptional() + { $authHeader = $this->getAuthorizationHeader(); if (empty($authHeader)) { diff --git a/middleware/AuthMiddleware.php b/middleware/AuthMiddleware.php index 2297c4b..79e1dbb 100644 --- a/middleware/AuthMiddleware.php +++ b/middleware/AuthMiddleware.php @@ -1,14 +1,18 @@ conn = $conn; $this->userModel = new UserModel($conn); } @@ -19,7 +23,8 @@ class AuthMiddleware { * @param string $event Event type (e.g., 'auth_required', 'access_denied', 'session_expired') * @param array $context Additional context data */ - private function logSecurityEvent(string $event, array $context = []): void { + private function logSecurityEvent(string $event, array $context = []): void + { $logData = [ 'event' => $event, 'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown', @@ -52,7 +57,8 @@ class AuthMiddleware { * @return array User data array * @throws Exception if authentication fails */ - public function authenticate() { + public function authenticate() + { // Start session if not already started with secure settings if (session_status() === PHP_SESSION_NONE) { // Configure secure session settings @@ -136,7 +142,8 @@ class AuthMiddleware { * @param string $header Header name * @return string|null Header value or null if not set */ - private function getHeader($header) { + private function getHeader($header) + { if (isset($_SERVER[$header])) { return $_SERVER[$header]; } @@ -149,7 +156,8 @@ class AuthMiddleware { * @param string $groups Comma-separated group names * @return bool True if user has access */ - private function checkGroupAccess($groups) { + private function checkGroupAccess($groups) + { if (empty($groups)) { return false; } @@ -158,7 +166,9 @@ class AuthMiddleware { // Filter to safe characters only to prevent header injection attacks $userGroups = array_filter( array_map('trim', explode(',', strtolower($groups))), - function($g) { return preg_match('/^[a-z0-9_\-]+$/', $g); } + function ($g) { + return preg_match('/^[a-z0-9_\-]+$/', $g); + } ); $requiredGroups = ['admin', 'employee']; @@ -168,7 +178,8 @@ class AuthMiddleware { /** * Redirect to Authelia login */ - private function redirectToAuth() { + private function redirectToAuth() + { // Log unauthenticated access attempt $this->logSecurityEvent('auth_required', [ 'reason' => 'no_auth_headers' @@ -237,7 +248,8 @@ class AuthMiddleware { * @param string $username Username * @param string $groups User groups */ - private function showAccessDenied($username, $groups) { + private function showAccessDenied($username, $groups) + { // Log access denied event with user details $this->logSecurityEvent('access_denied', [ 'username' => $username, @@ -308,7 +320,8 @@ class AuthMiddleware { * * @return array|null User data or null if not authenticated */ - public static function getCurrentUser() { + public static function getCurrentUser() + { if (session_status() === PHP_SESSION_NONE) { session_start(); } @@ -319,7 +332,8 @@ class AuthMiddleware { /** * Logout current user */ - public static function logout() { + public static function logout() + { if (session_status() === PHP_SESSION_NONE) { session_start(); } diff --git a/middleware/CsrfMiddleware.php b/middleware/CsrfMiddleware.php index 22ba9f1..0b86554 100644 --- a/middleware/CsrfMiddleware.php +++ b/middleware/CsrfMiddleware.php @@ -1,9 +1,11 @@ self::$tokenLifetime; } diff --git a/middleware/RateLimitMiddleware.php b/middleware/RateLimitMiddleware.php index 94065b9..b9521b3 100644 --- a/middleware/RateLimitMiddleware.php +++ b/middleware/RateLimitMiddleware.php @@ -1,11 +1,13 @@ conn = $conn; } @@ -17,7 +20,8 @@ class ApiKeyModel { * @param int|null $expiresInDays Number of days until expiration (null for no expiration) * @return array Array with 'success', 'api_key' (plaintext), 'key_prefix', 'error' */ - public function createKey($keyName, $createdBy, $expiresInDays = null) { + public function createKey($keyName, $createdBy, $expiresInDays = null) + { // Generate random API key (32 bytes = 64 hex characters) $apiKey = bin2hex(random_bytes(32)); @@ -67,7 +71,8 @@ class ApiKeyModel { * @param string $apiKey Plaintext API key to validate * @return array|null API key record if valid, null if invalid */ - public function validateKey($apiKey) { + public function validateKey($apiKey) + { if (empty($apiKey)) { return null; } @@ -111,7 +116,8 @@ class ApiKeyModel { * @param int $keyId API key ID * @return bool Success status */ - private function updateLastUsed($keyId) { + private function updateLastUsed($keyId) + { $stmt = $this->conn->prepare("UPDATE api_keys SET last_used = NOW() WHERE api_key_id = ?"); $stmt->bind_param("i", $keyId); $success = $stmt->execute(); @@ -125,7 +131,8 @@ class ApiKeyModel { * @param int $keyId API key ID * @return bool Success status */ - public function revokeKey($keyId) { + public function revokeKey($keyId) + { $stmt = $this->conn->prepare("UPDATE api_keys SET is_active = 0 WHERE api_key_id = ?"); $stmt->bind_param("i", $keyId); $success = $stmt->execute(); @@ -139,7 +146,8 @@ class ApiKeyModel { * @param int $keyId API key ID * @return bool Success status */ - public function deleteKey($keyId) { + public function deleteKey($keyId) + { $stmt = $this->conn->prepare("DELETE FROM api_keys WHERE api_key_id = ?"); $stmt->bind_param("i", $keyId); $success = $stmt->execute(); @@ -152,7 +160,8 @@ class ApiKeyModel { * * @return array Array of API key records (without hashes) */ - public function getAllKeys() { + public function getAllKeys() + { $stmt = $this->conn->prepare( "SELECT ak.*, u.username, u.display_name FROM api_keys ak @@ -179,7 +188,8 @@ class ApiKeyModel { * @param int $keyId API key ID * @return array|null API key record (without hash) or null if not found */ - public function getKeyById($keyId) { + public function getKeyById($keyId) + { $stmt = $this->conn->prepare( "SELECT ak.*, u.username, u.display_name FROM api_keys ak @@ -208,7 +218,8 @@ class ApiKeyModel { * @param int $userId User ID * @return array Array of API key records */ - public function getKeysByUser($userId) { + public function getKeysByUser($userId) + { $stmt = $this->conn->prepare( "SELECT * FROM api_keys WHERE created_by = ? ORDER BY created_at DESC" ); diff --git a/models/AttachmentModel.php b/models/AttachmentModel.php index 0ae5783..104e79d 100644 --- a/models/AttachmentModel.php +++ b/models/AttachmentModel.php @@ -1,19 +1,23 @@ conn = $conn; } /** * Get all attachments for a ticket */ - public function getAttachments($ticketId) { + public function getAttachments($ticketId) + { $sql = "SELECT a.*, u.username, u.display_name FROM ticket_attachments a LEFT JOIN users u ON a.uploaded_by = u.user_id @@ -37,7 +41,8 @@ class AttachmentModel { /** * Get a single attachment by ID */ - public function getAttachment($attachmentId) { + public function getAttachment($attachmentId) + { $sql = "SELECT a.*, u.username, u.display_name FROM ticket_attachments a LEFT JOIN users u ON a.uploaded_by = u.user_id @@ -56,7 +61,8 @@ class AttachmentModel { /** * Add a new attachment record */ - public function addAttachment($ticketId, $filename, $originalFilename, $fileSize, $mimeType, $uploadedBy) { + public function addAttachment($ticketId, $filename, $originalFilename, $fileSize, $mimeType, $uploadedBy) + { $sql = "INSERT INTO ticket_attachments (ticket_id, filename, original_filename, file_size, mime_type, uploaded_by) VALUES (?, ?, ?, ?, ?, ?)"; @@ -77,7 +83,8 @@ class AttachmentModel { /** * Delete an attachment record */ - public function deleteAttachment($attachmentId) { + public function deleteAttachment($attachmentId) + { $sql = "DELETE FROM ticket_attachments WHERE attachment_id = ?"; $stmt = $this->conn->prepare($sql); @@ -91,7 +98,8 @@ class AttachmentModel { /** * Get total attachment size for a ticket */ - public function getTotalSizeForTicket($ticketId) { + public function getTotalSizeForTicket($ticketId) + { $sql = "SELECT COALESCE(SUM(file_size), 0) as total_size FROM ticket_attachments WHERE ticket_id = ?"; @@ -109,7 +117,8 @@ class AttachmentModel { /** * Get attachment count for a ticket */ - public function getAttachmentCount($ticketId) { + public function getAttachmentCount($ticketId) + { $sql = "SELECT COUNT(*) as count FROM ticket_attachments WHERE ticket_id = ?"; $stmt = $this->conn->prepare($sql); @@ -125,7 +134,8 @@ class AttachmentModel { /** * Check if user can delete attachment (owner or admin) */ - public function canUserDelete($attachmentId, $userId, $isAdmin = false) { + public function canUserDelete($attachmentId, $userId, $isAdmin = false) + { if ($isAdmin) { return true; } @@ -137,7 +147,8 @@ class AttachmentModel { /** * Format file size for display */ - public static function formatFileSize($bytes) { + public static function formatFileSize($bytes) + { if ($bytes >= 1073741824) { return number_format($bytes / 1073741824, 2) . ' GB'; } elseif ($bytes >= 1048576) { @@ -152,7 +163,8 @@ class AttachmentModel { /** * Get file icon based on mime type */ - public static function getFileIcon($mimeType) { + public static function getFileIcon($mimeType) + { if (strpos($mimeType, 'image/') === 0) { return '🖼️'; } elseif (strpos($mimeType, 'video/') === 0) { @@ -177,7 +189,8 @@ class AttachmentModel { /** * Validate file type against allowed types */ - public static function isAllowedType($mimeType) { + public static function isAllowedType($mimeType) + { $allowedTypes = $GLOBALS['config']['ALLOWED_FILE_TYPES'] ?? [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf', @@ -192,5 +205,4 @@ class AttachmentModel { return in_array($mimeType, $allowedTypes); } - } diff --git a/models/AuditLogModel.php b/models/AuditLogModel.php index 4f0acef..627e36b 100644 --- a/models/AuditLogModel.php +++ b/models/AuditLogModel.php @@ -1,8 +1,10 @@ conn = $conn; } @@ -33,7 +36,8 @@ class AuditLogModel { * @param int $limit Requested limit * @return int Validated limit */ - private function validateLimit(int $limit): int { + private function validateLimit(int $limit): int + { if ($limit < 1) { return self::DEFAULT_LIMIT; } @@ -46,7 +50,8 @@ class AuditLogModel { * @param int $offset Requested offset * @return int Validated offset (non-negative) */ - private function validateOffset(int $offset): int { + private function validateOffset(int $offset): int + { return max(0, $offset); } @@ -56,7 +61,8 @@ class AuditLogModel { * @param string $date Date string * @return string|null Validated date or null if invalid */ - private function validateDate(string $date): ?string { + private function validateDate(string $date): ?string + { // Check format if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { return null; @@ -77,7 +83,8 @@ class AuditLogModel { * @param string $actionType Action type to validate * @return bool True if valid */ - private function isValidActionType(string $actionType): bool { + private function isValidActionType(string $actionType): bool + { return in_array($actionType, self::VALID_ACTION_TYPES, true); } @@ -87,7 +94,8 @@ class AuditLogModel { * @param string $entityType Entity type to validate * @return bool True if valid */ - private function isValidEntityType(string $entityType): bool { + private function isValidEntityType(string $entityType): bool + { return in_array($entityType, self::VALID_ENTITY_TYPES, true); } @@ -102,7 +110,8 @@ class AuditLogModel { * @param string|null $ipAddress IP address of the user * @return bool Success status */ - public function log($userId, $actionType, $entityType, $entityId = null, $details = null, $ipAddress = null) { + public function log($userId, $actionType, $entityType, $entityId = null, $details = null, $ipAddress = null) + { // Convert details array to JSON $detailsJson = null; if ($details !== null) { @@ -134,7 +143,8 @@ class AuditLogModel { * @param int $limit Maximum number of logs to return * @return array Array of audit log records */ - public function getLogsByEntity($entityType, $entityId, $limit = 100) { + public function getLogsByEntity($entityType, $entityId, $limit = 100) + { $limit = $this->validateLimit((int)$limit); $stmt = $this->conn->prepare( @@ -169,7 +179,8 @@ class AuditLogModel { * @param int $limit Maximum number of logs to return * @return array Array of audit log records */ - public function getLogsByUser($userId, $limit = 100) { + public function getLogsByUser($userId, $limit = 100) + { $limit = $this->validateLimit((int)$limit); $userId = max(0, (int)$userId); @@ -205,7 +216,8 @@ class AuditLogModel { * @param int $offset Offset for pagination * @return array Array of audit log records */ - public function getRecentLogs($limit = 50, $offset = 0) { + public function getRecentLogs($limit = 50, $offset = 0) + { $limit = $this->validateLimit((int)$limit); $offset = $this->validateOffset((int)$offset); @@ -240,7 +252,8 @@ class AuditLogModel { * @param int $limit Maximum number of logs to return * @return array Array of audit log records */ - public function getLogsByAction($actionType, $limit = 100) { + public function getLogsByAction($actionType, $limit = 100) + { $limit = $this->validateLimit((int)$limit); // Validate action type to prevent unexpected queries @@ -278,7 +291,8 @@ class AuditLogModel { * * @return int Total count */ - public function getTotalCount() { + public function getTotalCount() + { $result = $this->conn->query("SELECT COUNT(*) as count FROM audit_log"); $row = $result->fetch_assoc(); return (int)$row['count']; @@ -290,7 +304,8 @@ class AuditLogModel { * @param int $daysToKeep Number of days of logs to keep * @return int Number of deleted records */ - public function deleteOldLogs($daysToKeep = 90) { + public function deleteOldLogs($daysToKeep = 90) + { $stmt = $this->conn->prepare( "DELETE FROM audit_log WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)" ); @@ -307,7 +322,8 @@ class AuditLogModel { * * @return string Client IP address */ - private function getClientIP() { + private function getClientIP() + { $ipAddress = ''; // Check for proxy headers @@ -336,7 +352,8 @@ class AuditLogModel { * @param array $ticketData Ticket data * @return bool Success status */ - public function logTicketCreate($userId, $ticketId, $ticketData) { + public function logTicketCreate($userId, $ticketId, $ticketData) + { return $this->log( $userId, 'create', @@ -354,7 +371,8 @@ class AuditLogModel { * @param array $changes Array of changed fields * @return bool Success status */ - public function logTicketUpdate($userId, $ticketId, $changes) { + public function logTicketUpdate($userId, $ticketId, $changes) + { return $this->log($userId, 'update', 'ticket', $ticketId, $changes); } @@ -366,7 +384,8 @@ class AuditLogModel { * @param string $ticketId Associated ticket ID * @return bool Success status */ - public function logCommentCreate($userId, $commentId, $ticketId) { + public function logCommentCreate($userId, $commentId, $ticketId) + { return $this->log( $userId, 'comment', @@ -383,7 +402,8 @@ class AuditLogModel { * @param string $ticketId Ticket ID * @return bool Success status */ - public function logTicketView($userId, $ticketId) { + public function logTicketView($userId, $ticketId) + { return $this->log($userId, 'view', 'ticket', $ticketId); } @@ -399,7 +419,8 @@ class AuditLogModel { * @param int|null $userId User ID if known * @return bool Success status */ - public function logSecurityEvent($eventType, $details = [], $userId = null) { + public function logSecurityEvent($eventType, $details = [], $userId = null) + { $details['event_type'] = $eventType; $details['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown'; return $this->log($userId, 'security_event', 'security', null, $details); @@ -412,7 +433,8 @@ class AuditLogModel { * @param string $reason Reason for failure * @return bool Success status */ - public function logFailedAuth($username, $reason = 'Invalid credentials') { + public function logFailedAuth($username, $reason = 'Invalid credentials') + { return $this->logSecurityEvent('failed_auth', [ 'username' => $username, 'reason' => $reason @@ -426,7 +448,8 @@ class AuditLogModel { * @param int|null $userId User ID if session exists * @return bool Success status */ - public function logCsrfFailure($endpoint, $userId = null) { + public function logCsrfFailure($endpoint, $userId = null) + { return $this->logSecurityEvent('csrf_failure', [ 'endpoint' => $endpoint, 'method' => $_SERVER['REQUEST_METHOD'] ?? 'Unknown' @@ -440,7 +463,8 @@ class AuditLogModel { * @param int|null $userId User ID if session exists * @return bool Success status */ - public function logRateLimitExceeded($endpoint, $userId = null) { + public function logRateLimitExceeded($endpoint, $userId = null) + { return $this->logSecurityEvent('rate_limit_exceeded', [ 'endpoint' => $endpoint ], $userId); @@ -453,7 +477,8 @@ class AuditLogModel { * @param int|null $userId User ID if session exists * @return bool Success status */ - public function logUnauthorizedAccess($resource, $userId = null) { + public function logUnauthorizedAccess($resource, $userId = null) + { return $this->logSecurityEvent('unauthorized_access', [ 'resource' => $resource ], $userId); @@ -466,7 +491,8 @@ class AuditLogModel { * @param int $offset Offset for pagination * @return array Security events */ - public function getSecurityEvents($limit = 100, $offset = 0) { + public function getSecurityEvents($limit = 100, $offset = 0) + { $limit = $this->validateLimit((int)$limit); $offset = $this->validateOffset((int)$offset); @@ -501,7 +527,8 @@ class AuditLogModel { * @param string $ticketId Ticket ID * @return array Timeline events */ - public function getTicketTimeline($ticketId) { + public function getTicketTimeline($ticketId) + { $stmt = $this->conn->prepare( "SELECT al.*, u.username, u.display_name FROM audit_log al @@ -534,7 +561,8 @@ class AuditLogModel { * @param int $offset Offset for pagination * @return array Array containing logs and total count */ - public function getFilteredLogs($filters = [], $limit = 50, $offset = 0) { + public function getFilteredLogs($filters = [], $limit = 50, $offset = 0) + { // Validate pagination parameters $limit = $this->validateLimit((int)$limit); $offset = $this->validateOffset((int)$offset); diff --git a/models/BulkOperationsModel.php b/models/BulkOperationsModel.php index 8a4419e..7e6d283 100644 --- a/models/BulkOperationsModel.php +++ b/models/BulkOperationsModel.php @@ -1,11 +1,14 @@ conn = $conn; } @@ -18,7 +21,8 @@ class BulkOperationsModel { * @param array|null $parameters Operation parameters * @return int|false Operation ID or false on failure */ - public function createBulkOperation($type, $ticketIds, $userId, $parameters = null) { + public function createBulkOperation($type, $ticketIds, $userId, $parameters = null) + { // Validate ticket IDs to prevent injection via implode $ticketIds = array_values(array_filter( array_map('strval', $ticketIds), @@ -56,7 +60,8 @@ class BulkOperationsModel { * @param bool $atomic If true, rollback all changes on any failure * @return array Result with processed and failed counts */ - public function processBulkOperation($operationId, bool $atomic = false) { + public function processBulkOperation($operationId, bool $atomic = false) + { // Get operation details $sql = "SELECT * FROM bulk_operations WHERE operation_id = ?"; $stmt = $this->conn->prepare($sql); @@ -91,16 +96,16 @@ class BulkOperationsModel { try { foreach ($ticketIds as $ticketId) { - $ticketId = trim($ticketId); - $success = false; + $ticketId = trim($ticketId); + $success = false; - try { - switch ($operation['operation_type']) { - case 'bulk_close': - // Get current ticket from pre-loaded batch - $currentTicket = $ticketsById[$ticketId] ?? null; - if ($currentTicket) { - $updateResult = $ticketModel->updateTicket([ + try { + switch ($operation['operation_type']) { + case 'bulk_close': + // Get current ticket from pre-loaded batch + $currentTicket = $ticketsById[$ticketId] ?? null; + if ($currentTicket) { + $updateResult = $ticketModel->updateTicket([ 'ticket_id' => $ticketId, 'title' => $currentTicket['title'], 'description' => $currentTicket['description'], @@ -108,31 +113,41 @@ class BulkOperationsModel { 'type' => $currentTicket['type'], 'status' => 'Closed', 'priority' => $currentTicket['priority'] - ], $operation['performed_by']); - $success = $updateResult['success']; + ], $operation['performed_by']); + $success = $updateResult['success']; - if ($success) { - $auditLogModel->log($operation['performed_by'], 'update', 'ticket', $ticketId, - ['status' => 'Closed', 'bulk_operation_id' => $operationId]); + if ($success) { + $auditLogModel->log( + $operation['performed_by'], + 'update', + 'ticket', + $ticketId, + ['status' => 'Closed', 'bulk_operation_id' => $operationId] + ); + } } - } - break; + break; - case 'bulk_assign': - if (isset($parameters['assigned_to'])) { - $success = $ticketModel->assignTicket($ticketId, $parameters['assigned_to'], $operation['performed_by']); - if ($success) { - $auditLogModel->log($operation['performed_by'], 'assign', 'ticket', $ticketId, - ['assigned_to' => $parameters['assigned_to'], 'bulk_operation_id' => $operationId]); + case 'bulk_assign': + if (isset($parameters['assigned_to'])) { + $success = $ticketModel->assignTicket($ticketId, $parameters['assigned_to'], $operation['performed_by']); + if ($success) { + $auditLogModel->log( + $operation['performed_by'], + 'assign', + 'ticket', + $ticketId, + ['assigned_to' => $parameters['assigned_to'], 'bulk_operation_id' => $operationId] + ); + } } - } - break; + break; - case 'bulk_priority': - if (isset($parameters['priority'])) { - $currentTicket = $ticketsById[$ticketId] ?? null; - if ($currentTicket) { - $updateResult = $ticketModel->updateTicket([ + case 'bulk_priority': + if (isset($parameters['priority'])) { + $currentTicket = $ticketsById[$ticketId] ?? null; + if ($currentTicket) { + $updateResult = $ticketModel->updateTicket([ 'ticket_id' => $ticketId, 'title' => $currentTicket['title'], 'description' => $currentTicket['description'], @@ -140,22 +155,27 @@ class BulkOperationsModel { 'type' => $currentTicket['type'], 'status' => $currentTicket['status'], 'priority' => $parameters['priority'] - ], $operation['performed_by']); - $success = $updateResult['success']; + ], $operation['performed_by']); + $success = $updateResult['success']; - if ($success) { - $auditLogModel->log($operation['performed_by'], 'update', 'ticket', $ticketId, - ['priority' => $parameters['priority'], 'bulk_operation_id' => $operationId]); + if ($success) { + $auditLogModel->log( + $operation['performed_by'], + 'update', + 'ticket', + $ticketId, + ['priority' => $parameters['priority'], 'bulk_operation_id' => $operationId] + ); + } } } - } - break; + break; - case 'bulk_status': - if (isset($parameters['status'])) { - $currentTicket = $ticketsById[$ticketId] ?? null; - if ($currentTicket) { - $updateResult = $ticketModel->updateTicket([ + case 'bulk_status': + if (isset($parameters['status'])) { + $currentTicket = $ticketsById[$ticketId] ?? null; + if ($currentTicket) { + $updateResult = $ticketModel->updateTicket([ 'ticket_id' => $ticketId, 'title' => $currentTicket['title'], 'description' => $currentTicket['description'], @@ -163,37 +183,47 @@ class BulkOperationsModel { 'type' => $currentTicket['type'], 'status' => $parameters['status'], 'priority' => $currentTicket['priority'] - ], $operation['performed_by']); - $success = $updateResult['success']; + ], $operation['performed_by']); + $success = $updateResult['success']; - if ($success) { - $auditLogModel->log($operation['performed_by'], 'update', 'ticket', $ticketId, - ['status' => $parameters['status'], 'bulk_operation_id' => $operationId]); + if ($success) { + $auditLogModel->log( + $operation['performed_by'], + 'update', + 'ticket', + $ticketId, + ['status' => $parameters['status'], 'bulk_operation_id' => $operationId] + ); + } } } - } - break; + break; - case 'bulk_delete': - $success = $ticketModel->deleteTicket($ticketId); - if ($success) { - $auditLogModel->log($operation['performed_by'], 'delete', 'ticket', $ticketId, - ['bulk_operation_id' => $operationId]); - } - break; - } + case 'bulk_delete': + $success = $ticketModel->deleteTicket($ticketId); + if ($success) { + $auditLogModel->log( + $operation['performed_by'], + 'delete', + 'ticket', + $ticketId, + ['bulk_operation_id' => $operationId] + ); + } + break; + } - if ($success) { - $processed++; - } else { + if ($success) { + $processed++; + } else { + $failed++; + $errors[] = "Ticket $ticketId: Update failed"; + } + } catch (Exception $e) { $failed++; - $errors[] = "Ticket $ticketId: Update failed"; + $errors[] = "Ticket $ticketId: " . $e->getMessage(); + error_log("Bulk operation error for ticket $ticketId: " . $e->getMessage()); } - } catch (Exception $e) { - $failed++; - $errors[] = "Ticket $ticketId: " . $e->getMessage(); - error_log("Bulk operation error for ticket $ticketId: " . $e->getMessage()); - } } // If atomic mode and any failures, rollback everything @@ -219,7 +249,6 @@ class BulkOperationsModel { // Commit the transaction $this->conn->commit(); - } catch (Exception $e) { // Rollback on any unexpected error $this->conn->rollback(); @@ -255,7 +284,8 @@ class BulkOperationsModel { * @param int $operationId Operation ID * @return array|null Operation record or null */ - public function getOperationById($operationId) { + public function getOperationById($operationId) + { $sql = "SELECT * FROM bulk_operations WHERE operation_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("i", $operationId); @@ -273,7 +303,8 @@ class BulkOperationsModel { * @param int $limit Result limit * @return array Array of operations */ - public function getOperationsByUser($userId, $limit = 50) { + public function getOperationsByUser($userId, $limit = 50) + { $sql = "SELECT * FROM bulk_operations WHERE performed_by = ? ORDER BY created_at DESC LIMIT ?"; $stmt = $this->conn->prepare($sql); diff --git a/models/CommentModel.php b/models/CommentModel.php index 50cbaf2..5013c72 100644 --- a/models/CommentModel.php +++ b/models/CommentModel.php @@ -1,8 +1,11 @@ conn = $conn; } @@ -12,7 +15,8 @@ class CommentModel { * @param string $text Comment text * @return array Array of mentioned usernames */ - public function extractMentions($text) { + public function extractMentions($text) + { $mentions = []; // Match @username patterns (alphanumeric, underscores, hyphens) if (preg_match_all('/@([a-zA-Z0-9_-]+)/', $text, $matches)) { @@ -27,7 +31,8 @@ class CommentModel { * @param array $usernames Array of usernames * @return array Array of user records with user_id, username, display_name */ - public function getMentionedUsers($usernames) { + public function getMentionedUsers($usernames) + { if (empty($usernames)) { return []; } @@ -49,11 +54,12 @@ class CommentModel { return $users; } - + /** * Get total comment count for a ticket */ - public function getCommentCount(int $ticketId): int { + public function getCommentCount(int $ticketId): int + { $stmt = $this->conn->prepare( "SELECT COUNT(*) as total FROM ticket_comments WHERE ticket_id = ?" ); @@ -70,7 +76,8 @@ class CommentModel { * @param int $limit Max root-level comments to return (0 = all) * @param int $offset Root-level comment offset for pagination */ - public function getCommentsByTicketId($ticketId, $threaded = true, int $limit = 0, int $offset = 0) { + public function getCommentsByTicketId($ticketId, $threaded = true, int $limit = 0, int $offset = 0) + { $hasThreading = $this->hasThreadingSupport(); // When paginating with threading we fetch root comments page first, @@ -139,7 +146,8 @@ class CommentModel { /** * Paginated threaded comments: fetch one page of root comments + all their replies. */ - private function getThreadedCommentsPaged(int $ticketId, int $limit, int $offset): array { + private function getThreadedCommentsPaged(int $ticketId, int $limit, int $offset): array + { // Page of root comments $rootSql = "SELECT tc.*, u.display_name, u.username FROM ticket_comments tc @@ -203,7 +211,8 @@ class CommentModel { /** * Check if threading columns exist */ - private function hasThreadingSupport() { + private function hasThreadingSupport() + { static $hasSupport = null; if ($hasSupport !== null) { return $hasSupport; @@ -217,16 +226,19 @@ class CommentModel { /** * Recursively build comment thread */ - private function buildCommentThread($comment, &$allComments) { + private function buildCommentThread($comment, &$allComments) + { $comment['replies'] = []; foreach ($allComments as $c) { - if ((int)$c['parent_comment_id'] === (int)$comment['comment_id'] - && isset($allComments[$c['comment_id']])) { + if ( + (int)$c['parent_comment_id'] === (int)$comment['comment_id'] + && isset($allComments[$c['comment_id']]) + ) { $comment['replies'][] = $this->buildCommentThread($c, $allComments); } } // Sort replies by date ascending - usort($comment['replies'], function($a, $b) { + usort($comment['replies'], function ($a, $b) { return strtotime($a['created_at']) - strtotime($b['created_at']); }); return $comment; @@ -235,11 +247,13 @@ class CommentModel { /** * Get flat list of comments (for backward compatibility) */ - public function getCommentsByTicketIdFlat($ticketId) { + public function getCommentsByTicketIdFlat($ticketId) + { return $this->getCommentsByTicketId($ticketId, false); } - - public function addComment($ticketId, $commentData, $userId = null) { + + public function addComment($ticketId, $commentData, $userId = null) + { // Check if threading is supported $hasThreading = $this->hasThreadingSupport(); @@ -310,7 +324,8 @@ class CommentModel { /** * Get a single comment by ID */ - public function getCommentById($commentId) { + public function getCommentById($commentId) + { $sql = "SELECT tc.*, u.display_name, u.username FROM ticket_comments tc LEFT JOIN users u ON tc.user_id = u.user_id @@ -326,7 +341,8 @@ class CommentModel { * Update an existing comment * Only the comment owner or an admin can update */ - public function updateComment($commentId, $commentText, $markdownEnabled, $userId, $isAdmin = false) { + public function updateComment($commentId, $commentText, $markdownEnabled, $userId, $isAdmin = false) + { // First check if user owns this comment or is admin $comment = $this->getCommentById($commentId); @@ -372,7 +388,8 @@ class CommentModel { * Delete a comment * Only the comment owner or an admin can delete */ - public function deleteComment($commentId, $userId, $isAdmin = false) { + public function deleteComment($commentId, $userId, $isAdmin = false) + { // First check if user owns this comment or is admin $comment = $this->getCommentById($commentId); @@ -401,4 +418,3 @@ class CommentModel { } } } -?> \ No newline at end of file diff --git a/models/CustomFieldModel.php b/models/CustomFieldModel.php index 7312958..d4af64d 100644 --- a/models/CustomFieldModel.php +++ b/models/CustomFieldModel.php @@ -1,12 +1,15 @@ conn = $conn; } @@ -17,7 +20,8 @@ class CustomFieldModel { /** * Get all field definitions */ - public function getAllDefinitions($category = null, $activeOnly = true) { + public function getAllDefinitions($category = null, $activeOnly = true) + { $sql = "SELECT * FROM custom_field_definitions WHERE 1=1"; $params = []; $types = ''; @@ -61,7 +65,8 @@ class CustomFieldModel { /** * Get a single field definition */ - public function getDefinition($fieldId) { + public function getDefinition($fieldId) + { $sql = "SELECT * FROM custom_field_definitions WHERE field_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param('i', $fieldId); @@ -80,7 +85,8 @@ class CustomFieldModel { /** * Create a new field definition */ - public function createDefinition($data) { + public function createDefinition($data) + { $options = null; if (isset($data['field_options']) && !empty($data['field_options'])) { $options = json_encode($data['field_options']); @@ -91,7 +97,8 @@ class CustomFieldModel { VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; $stmt = $this->conn->prepare($sql); - $stmt->bind_param('sssssiii', + $stmt->bind_param( + 'sssssiii', $data['field_name'], $data['field_label'], $data['field_type'], @@ -116,7 +123,8 @@ class CustomFieldModel { /** * Update a field definition */ - public function updateDefinition($fieldId, $data) { + public function updateDefinition($fieldId, $data) + { $options = null; if (isset($data['field_options']) && !empty($data['field_options'])) { $options = json_encode($data['field_options']); @@ -128,7 +136,8 @@ class CustomFieldModel { WHERE field_id = ?"; $stmt = $this->conn->prepare($sql); - $stmt->bind_param('sssssiiii', + $stmt->bind_param( + 'sssssiiii', $data['field_name'], $data['field_label'], $data['field_type'], @@ -148,7 +157,8 @@ class CustomFieldModel { /** * Delete a field definition */ - public function deleteDefinition($fieldId) { + public function deleteDefinition($fieldId) + { // This will cascade delete all values due to FK constraint $sql = "DELETE FROM custom_field_definitions WHERE field_id = ?"; $stmt = $this->conn->prepare($sql); @@ -165,7 +175,8 @@ class CustomFieldModel { /** * Get all field values for a ticket */ - public function getValuesForTicket($ticketId) { + public function getValuesForTicket($ticketId) + { $sql = "SELECT cfv.*, cfd.field_name, cfd.field_label, cfd.field_type, cfd.field_options FROM custom_field_values cfv JOIN custom_field_definitions cfd ON cfv.field_id = cfd.field_id @@ -192,7 +203,8 @@ class CustomFieldModel { /** * Set a field value for a ticket (insert or update) */ - public function setValue($ticketId, $fieldId, $value) { + public function setValue($ticketId, $fieldId, $value) + { $sql = "INSERT INTO custom_field_values (ticket_id, field_id, field_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE field_value = VALUES(field_value), updated_at = CURRENT_TIMESTAMP"; @@ -207,7 +219,8 @@ class CustomFieldModel { /** * Set multiple field values for a ticket */ - public function setValues($ticketId, $values) { + public function setValues($ticketId, $values) + { $results = []; foreach ($values as $fieldId => $value) { $results[$fieldId] = $this->setValue($ticketId, $fieldId, $value); @@ -218,7 +231,8 @@ class CustomFieldModel { /** * Delete all field values for a ticket */ - public function deleteValuesForTicket($ticketId) { + public function deleteValuesForTicket($ticketId) + { $sql = "DELETE FROM custom_field_values WHERE ticket_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param('s', $ticketId); @@ -227,4 +241,3 @@ class CustomFieldModel { return ['success' => $success]; } } -?> diff --git a/models/DependencyModel.php b/models/DependencyModel.php index c14622d..ba7395c 100644 --- a/models/DependencyModel.php +++ b/models/DependencyModel.php @@ -1,11 +1,14 @@ conn = $conn; } @@ -15,7 +18,8 @@ class DependencyModel { * @param string $ticketId Ticket ID * @return array Dependencies grouped by type */ - public function getDependencies($ticketId) { + public function getDependencies($ticketId) + { $sql = "SELECT d.*, t.title, t.status, t.priority FROM ticket_dependencies d LEFT JOIN tickets t ON d.depends_on_id = t.ticket_id @@ -53,7 +57,8 @@ class DependencyModel { * @param string $ticketId Ticket ID * @return array Dependent tickets */ - public function getDependentTickets($ticketId) { + public function getDependentTickets($ticketId) + { $sql = "SELECT d.*, t.title, t.status, t.priority FROM ticket_dependencies d LEFT JOIN tickets t ON d.ticket_id = t.ticket_id @@ -88,7 +93,8 @@ class DependencyModel { * @param int $createdBy User ID who created the dependency * @return array Result with success status */ - public function addDependency($ticketId, $dependsOnId, $type = 'blocks', $createdBy = null) { + public function addDependency($ticketId, $dependsOnId, $type = 'blocks', $createdBy = null) + { // Validate dependency type $validTypes = ['blocks', 'blocked_by', 'relates_to', 'duplicates']; if (!in_array($type, $validTypes)) { @@ -142,7 +148,8 @@ class DependencyModel { * @param int $dependencyId Dependency ID * @return bool Success status */ - public function removeDependency($dependencyId) { + public function removeDependency($dependencyId) + { $sql = "DELETE FROM ticket_dependencies WHERE dependency_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("i", $dependencyId); @@ -159,7 +166,8 @@ class DependencyModel { * @param string $type Dependency type * @return bool Success status */ - public function removeDependencyByTickets($ticketId, $dependsOnId, $type) { + public function removeDependencyByTickets($ticketId, $dependsOnId, $type) + { $sql = "DELETE FROM ticket_dependencies WHERE ticket_id = ? AND depends_on_id = ? AND dependency_type = ?"; $stmt = $this->conn->prepare($sql); @@ -180,7 +188,8 @@ class DependencyModel { * @param string $type Dependency type * @return bool True if it would create a cycle */ - private function wouldCreateCycle($ticketId, $dependsOnId, $type): bool { + private function wouldCreateCycle($ticketId, $dependsOnId, $type): bool + { // Only check for cycles in blocking relationships if (!in_array($type, ['blocks', 'blocked_by'])) { return false; @@ -203,7 +212,8 @@ class DependencyModel { * @param int $depth Current recursion depth * @return bool True if path exists */ - private function hasDependencyPath($source, $target, array &$visited, int $depth): bool { + private function hasDependencyPath($source, $target, array &$visited, int $depth): bool + { // Depth limit to prevent DoS and stack overflow if ($depth >= self::MAX_DEPENDENCY_DEPTH) { error_log("Dependency cycle detection hit max depth ({$depth}) from {$source} to {$target}"); @@ -250,7 +260,8 @@ class DependencyModel { * @param array $ticketIds Array of ticket IDs * @return array Dependencies indexed by ticket ID */ - public function getDependenciesBatch($ticketIds) { + public function getDependenciesBatch($ticketIds) + { if (empty($ticketIds)) { return []; } diff --git a/models/RecurringTicketModel.php b/models/RecurringTicketModel.php index a066bca..6eeecd0 100644 --- a/models/RecurringTicketModel.php +++ b/models/RecurringTicketModel.php @@ -1,19 +1,23 @@ conn = $conn; } /** * Get all recurring tickets */ - public function getAll($includeInactive = false) { + public function getAll($includeInactive = false) + { $sql = "SELECT rt.*, u1.display_name as assigned_name, u1.username as assigned_username, u2.display_name as creator_name, u2.username as creator_username FROM recurring_tickets rt @@ -37,7 +41,8 @@ class RecurringTicketModel { /** * Get a single recurring ticket by ID */ - public function getById($recurringId) { + public function getById($recurringId) + { $sql = "SELECT * FROM recurring_tickets WHERE recurring_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param('i', $recurringId); @@ -51,14 +56,16 @@ class RecurringTicketModel { /** * Create a new recurring ticket */ - public function create($data) { + public function create($data) + { $sql = "INSERT INTO recurring_tickets (title_template, description_template, category, type, priority, assigned_to, schedule_type, schedule_day, schedule_time, next_run_at, is_active, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; $stmt = $this->conn->prepare($sql); - $stmt->bind_param('ssssiiisssii', + $stmt->bind_param( + 'ssssiiisssii', $data['title_template'], $data['description_template'], $data['category'], @@ -87,7 +94,8 @@ class RecurringTicketModel { /** * Update a recurring ticket */ - public function update($recurringId, $data) { + public function update($recurringId, $data) + { $sql = "UPDATE recurring_tickets SET title_template = ?, description_template = ?, category = ?, type = ?, priority = ?, assigned_to = ?, schedule_type = ?, schedule_day = ?, @@ -95,7 +103,8 @@ class RecurringTicketModel { WHERE recurring_id = ?"; $stmt = $this->conn->prepare($sql); - $stmt->bind_param('ssssiissssii', + $stmt->bind_param( + 'ssssiissssii', $data['title_template'], $data['description_template'], $data['category'], @@ -118,7 +127,8 @@ class RecurringTicketModel { /** * Delete a recurring ticket */ - public function delete($recurringId) { + public function delete($recurringId) + { $sql = "DELETE FROM recurring_tickets WHERE recurring_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param('i', $recurringId); @@ -130,7 +140,8 @@ class RecurringTicketModel { /** * Get recurring tickets due for execution */ - public function getDueRecurringTickets() { + public function getDueRecurringTickets() + { $sql = "SELECT * FROM recurring_tickets WHERE is_active = 1 AND next_run_at <= NOW()"; $result = $this->conn->query($sql); $items = []; @@ -143,7 +154,8 @@ class RecurringTicketModel { /** * Update last run and calculate next run time */ - public function updateAfterRun($recurringId) { + public function updateAfterRun($recurringId) + { $recurring = $this->getById($recurringId); if (!$recurring) { return false; @@ -166,7 +178,8 @@ class RecurringTicketModel { /** * Calculate the next run time based on schedule */ - private function calculateNextRunTime($scheduleType, $scheduleDay, $scheduleTime) { + private function calculateNextRunTime($scheduleType, $scheduleDay, $scheduleTime) + { $now = new DateTime(); $time = new DateTime($scheduleTime); @@ -202,7 +215,8 @@ class RecurringTicketModel { /** * Toggle active status */ - public function toggleActive($recurringId) { + public function toggleActive($recurringId) + { $sql = "UPDATE recurring_tickets SET is_active = NOT is_active WHERE recurring_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param('i', $recurringId); @@ -211,4 +225,3 @@ class RecurringTicketModel { return ['success' => $success]; } } -?> diff --git a/models/SavedFiltersModel.php b/models/SavedFiltersModel.php index 980e95e..3051961 100644 --- a/models/SavedFiltersModel.php +++ b/models/SavedFiltersModel.php @@ -1,19 +1,23 @@ conn = $conn; } /** * Get all saved filters for a user */ - public function getUserFilters($userId) { + public function getUserFilters($userId) + { $sql = "SELECT filter_id, filter_name, filter_criteria, is_default, created_at, updated_at FROM saved_filters WHERE user_id = ? @@ -34,7 +38,8 @@ class SavedFiltersModel { /** * Get a specific saved filter */ - public function getFilter($filterId, $userId) { + public function getFilter($filterId, $userId) + { $sql = "SELECT filter_id, filter_name, filter_criteria, is_default FROM saved_filters WHERE filter_id = ? AND user_id = ?"; @@ -53,7 +58,8 @@ class SavedFiltersModel { /** * Save a new filter */ - public function saveFilter($userId, $filterName, $filterCriteria, $isDefault = false) { + public function saveFilter($userId, $filterName, $filterCriteria, $isDefault = false) + { $this->conn->begin_transaction(); try { // If this is set as default, unset all other defaults for this user @@ -89,7 +95,8 @@ class SavedFiltersModel { /** * Update an existing filter */ - public function updateFilter($filterId, $userId, $filterName, $filterCriteria, $isDefault = false) { + public function updateFilter($filterId, $userId, $filterName, $filterCriteria, $isDefault = false) + { // Verify ownership $existing = $this->getFilter($filterId, $userId); if (!$existing) { @@ -118,7 +125,8 @@ class SavedFiltersModel { /** * Delete a saved filter */ - public function deleteFilter($filterId, $userId) { + public function deleteFilter($filterId, $userId) + { $sql = "DELETE FROM saved_filters WHERE filter_id = ? AND user_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("ii", $filterId, $userId); @@ -132,7 +140,8 @@ class SavedFiltersModel { /** * Set a filter as default */ - public function setDefaultFilter($filterId, $userId) { + public function setDefaultFilter($filterId, $userId) + { $this->conn->begin_transaction(); try { $this->clearDefaultFilters($userId); @@ -157,7 +166,8 @@ class SavedFiltersModel { /** * Get the default filter for a user */ - public function getDefaultFilter($userId) { + public function getDefaultFilter($userId) + { $sql = "SELECT filter_id, filter_name, filter_criteria FROM saved_filters WHERE user_id = ? AND is_default = 1 @@ -177,7 +187,8 @@ class SavedFiltersModel { /** * Clear all default filters for a user (helper method) */ - private function clearDefaultFilters($userId) { + private function clearDefaultFilters($userId) + { $sql = "UPDATE saved_filters SET is_default = 0 WHERE user_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("i", $userId); @@ -187,7 +198,8 @@ class SavedFiltersModel { /** * Get filter ID by name (helper method) */ - private function getFilterIdByName($userId, $filterName) { + private function getFilterIdByName($userId, $filterName) + { $sql = "SELECT filter_id FROM saved_filters WHERE user_id = ? AND filter_name = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("is", $userId, $filterName); @@ -200,4 +212,3 @@ class SavedFiltersModel { return null; } } -?> diff --git a/models/StatsModel.php b/models/StatsModel.php index f173c03..0e9133c 100644 --- a/models/StatsModel.php +++ b/models/StatsModel.php @@ -1,4 +1,5 @@ conn = $conn; } /** * Get tickets by assignee (top 5) */ - public function getTicketsByAssignee(int $limit = 8): array { + public function getTicketsByAssignee(int $limit = 8): array + { $sql = "SELECT u.user_id, u.display_name, @@ -64,7 +68,8 @@ class StatsModel { * @param bool $forceRefresh Force a cache refresh * @return array All dashboard statistics */ - public function getAllStats(array $user = [], bool $forceRefresh = false): array { + public function getAllStats(array $user = [], bool $forceRefresh = false): array + { $isAdmin = !empty($user['is_admin']); // Admins share one cache entry; non-admins get a per-user cache entry $cacheKey = $isAdmin ? 'dashboard_all' : 'dashboard_user_' . ($user['user_id'] ?? 'anon'); @@ -76,7 +81,7 @@ class StatsModel { return CacheHelper::remember( self::CACHE_PREFIX, $cacheKey, - function() use ($user) { + function () use ($user) { return $this->fetchAllStats($user); }, self::STATS_CACHE_TTL @@ -91,7 +96,8 @@ class StatsModel { * @param array $user Current user array * @return array All dashboard statistics */ - private function fetchAllStats(array $user = []): array { + private function fetchAllStats(array $user = []): array + { $ticketModel = new TicketModel($this->conn); $visFilter = $ticketModel->getVisibilityFilter($user); $visSQL = $visFilter['sql']; @@ -191,7 +197,8 @@ class StatsModel { * * Call this method when ticket data changes to ensure fresh stats. */ - public function invalidateCache(): void { + public function invalidateCache(): void + { CacheHelper::delete(self::CACHE_PREFIX, null); } } diff --git a/models/TemplateModel.php b/models/TemplateModel.php index c82d832..ebd9a25 100644 --- a/models/TemplateModel.php +++ b/models/TemplateModel.php @@ -1,11 +1,14 @@ conn = $conn; } @@ -14,7 +17,8 @@ class TemplateModel { * * @return array Array of template records */ - public function getAllTemplates(): array { + public function getAllTemplates(): array + { $sql = "SELECT * FROM ticket_templates WHERE is_active = TRUE ORDER BY template_name"; $result = $this->conn->query($sql); @@ -31,7 +35,8 @@ class TemplateModel { * @param int $templateId Template ID * @return array|null Template record or null if not found */ - public function getTemplateById(int $templateId): ?array { + public function getTemplateById(int $templateId): ?array + { $sql = "SELECT * FROM ticket_templates WHERE template_id = ? AND is_active = TRUE"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("i", $templateId); @@ -50,12 +55,14 @@ class TemplateModel { * @param int $createdBy User ID creating the template * @return bool Success status */ - public function createTemplate(array $data, int $createdBy): bool { + public function createTemplate(array $data, int $createdBy): bool + { $sql = "INSERT INTO ticket_templates (template_name, title_template, description_template, category, type, default_priority, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)"; $stmt = $this->conn->prepare($sql); - $stmt->bind_param("sssssii", + $stmt->bind_param( + "sssssii", $data['template_name'], $data['title_template'], $data['description_template'], @@ -77,7 +84,8 @@ class TemplateModel { * @param array $data Template data to update * @return bool Success status */ - public function updateTemplate(int $templateId, array $data): bool { + public function updateTemplate(int $templateId, array $data): bool + { $sql = "UPDATE ticket_templates SET template_name = ?, title_template = ?, @@ -87,7 +95,8 @@ class TemplateModel { default_priority = ? WHERE template_id = ?"; $stmt = $this->conn->prepare($sql); - $stmt->bind_param("sssssii", + $stmt->bind_param( + "sssssii", $data['template_name'], $data['title_template'], $data['description_template'], @@ -108,7 +117,8 @@ class TemplateModel { * @param int $templateId Template ID * @return bool Success status */ - public function deactivateTemplate(int $templateId): bool { + public function deactivateTemplate(int $templateId): bool + { $sql = "UPDATE ticket_templates SET is_active = FALSE WHERE template_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("i", $templateId); diff --git a/models/TicketModel.php b/models/TicketModel.php index 2bc0cb6..c56f976 100644 --- a/models/TicketModel.php +++ b/models/TicketModel.php @@ -1,12 +1,16 @@ conn = $conn; } - public function getTicketById(int $id): ?array { + public function getTicketById(int $id): ?array + { $sql = "SELECT t.*, u_created.username as creator_username, u_created.display_name as creator_display_name, @@ -30,8 +34,9 @@ class TicketModel { return $result->fetch_assoc(); } - - public function getAllTickets(int $page = 1, int $limit = 15, ?string $status = 'Open', string $sortColumn = 'ticket_id', string $sortDirection = 'desc', ?string $category = null, ?string $type = null, ?string $search = null, array $filters = [], ?array $user = null): array { + + public function getAllTickets(int $page = 1, int $limit = 15, ?string $status = 'Open', string $sortColumn = 'ticket_id', string $sortDirection = 'desc', ?string $category = null, ?string $type = null, ?string $search = null, array $filters = [], ?array $user = null): array + { // Calculate offset $offset = ($page - 1) * $limit; @@ -162,12 +167,12 @@ class TicketModel { $paramTypes .= 'i'; } } - + $whereClause = ''; if (!empty($whereConditions)) { $whereClause = 'WHERE ' . implode(' AND ', $whereConditions); } - + // Validate sort column to prevent SQL injection $allowedColumns = ['ticket_id', 'title', 'status', 'priority', 'category', 'type', 'created_at', 'updated_at', 'created_by', 'assigned_to']; if (!in_array($sortColumn, $allowedColumns)) { @@ -230,7 +235,7 @@ class TicketModel { 'current_page' => $page ]; } - + /** * Update a ticket with optional optimistic locking * @@ -239,7 +244,8 @@ class TicketModel { * @param string|null $expectedUpdatedAt If provided, update will fail if ticket was modified since this timestamp * @return array ['success' => bool, 'error' => string|null, 'conflict' => bool] */ - public function updateTicket(array $ticketData, ?int $updatedBy = null, ?string $expectedUpdatedAt = null): array { + public function updateTicket(array $ticketData, ?int $updatedBy = null, ?string $expectedUpdatedAt = null): array + { // closed_at: set on close (preserve if already set), clear on reopen $closedAtClause = "closed_at = CASE WHEN ? = 'Closed' THEN COALESCE(closed_at, NOW()) ELSE NULL END"; @@ -332,8 +338,9 @@ class TicketModel { return ['success' => true, 'error' => null, 'conflict' => false]; } - - public function createTicket(array $ticketData, ?int $createdBy = null): array { + + public function createTicket(array $ticketData, ?int $createdBy = null): array + { // Generate unique ticket ID (9-digit format with leading zeros) // Uses cryptographically secure random numbers for better distribution // Includes exponential backoff and fallback for reliability under high load @@ -486,16 +493,17 @@ class TicketModel { } } - public function addComment(int $ticketId, array $commentData): array { + public function addComment(int $ticketId, array $commentData): array + { $sql = "INSERT INTO ticket_comments (ticket_id, user_name, comment_text, markdown_enabled) VALUES (?, ?, ?, ?)"; - + $stmt = $this->conn->prepare($sql); - + // Set default username $username = $commentData['user_name'] ?? 'User'; $markdownEnabled = $commentData['markdown_enabled'] ? 1 : 0; - + $stmt->bind_param( "issi", $ticketId, @@ -503,7 +511,7 @@ class TicketModel { $commentData['comment_text'], $markdownEnabled ); - + if ($stmt->execute()) { return [ 'success' => true, @@ -526,7 +534,8 @@ class TicketModel { * @param int $assignedBy User ID performing the assignment * @return bool Success status */ - public function assignTicket(int $ticketId, int $userId, int $assignedBy): bool { + public function assignTicket(int $ticketId, int $userId, int $assignedBy): bool + { $sql = "UPDATE tickets SET assigned_to = ?, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("iii", $userId, $assignedBy, $ticketId); @@ -542,7 +551,8 @@ class TicketModel { * @param int $updatedBy User ID performing the unassignment * @return bool Success status */ - public function unassignTicket(int $ticketId, int $updatedBy): bool { + public function unassignTicket(int $ticketId, int $updatedBy): bool + { $sql = "UPDATE tickets SET assigned_to = NULL, updated_by = ?, updated_at = NOW() WHERE ticket_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("ii", $updatedBy, $ticketId); @@ -558,7 +568,8 @@ class TicketModel { * @param array $ticketIds Array of ticket IDs * @return array Associative array keyed by ticket_id */ - public function getTicketsByIds(array $ticketIds): array { + public function getTicketsByIds(array $ticketIds): array + { if (empty($ticketIds)) { return []; } @@ -604,7 +615,8 @@ class TicketModel { * @param array $user The user data (must include user_id, is_admin, groups) * @return bool True if user can access the ticket */ - public function canUserAccessTicket(array $ticket, array $user): bool { + public function canUserAccessTicket(array $ticket, array $user): bool + { // Admins can access all tickets if (!empty($user['is_admin'])) { return true; @@ -644,7 +656,8 @@ class TicketModel { * @param array $user The current user * @return array ['sql' => string, 'params' => array, 'types' => string] */ - public function getVisibilityFilter(array $user): array { + public function getVisibilityFilter(array $user): array + { // Admins see all tickets if (!empty($user['is_admin'])) { return ['sql' => '1=1', 'params' => [], 'types' => '']; @@ -697,7 +710,8 @@ class TicketModel { * @param int $updatedBy User ID * @return bool */ - public function updateVisibility(int $ticketId, string $visibility, ?string $visibilityGroups, int $updatedBy): bool { + public function updateVisibility(int $ticketId, string $visibility, ?string $visibilityGroups, int $updatedBy): bool + { $allowedVisibilities = ['public', 'internal', 'confidential']; if (!in_array($visibility, $allowedVisibilities)) { $visibility = 'public'; @@ -728,7 +742,8 @@ class TicketModel { * @param string $ticketId Ticket ID * @return bool Success status */ - public function deleteTicket(string $ticketId): bool { + public function deleteTicket(string $ticketId): bool + { // Collect attachment filenames before deleting DB rows $attachmentFiles = []; $attStmt = $this->conn->prepare("SELECT filename FROM ticket_attachments WHERE ticket_id = ?"); @@ -754,7 +769,9 @@ class TicketModel { foreach ($children as $sql) { try { $stmt = $this->conn->prepare($sql); - if (!$stmt) continue; + if (!$stmt) { + continue; + } // ticket_dependencies uses two placeholders if (strpos($sql, 'depends_on_id') !== false) { $stmt->bind_param('ss', $ticketId, $ticketId); @@ -772,7 +789,9 @@ class TicketModel { } $stmt = $this->conn->prepare("DELETE FROM tickets WHERE ticket_id = ?"); - if (!$stmt) return false; + if (!$stmt) { + return false; + } $stmt->bind_param('s', $ticketId); $result = $stmt->execute(); $affected = $stmt->affected_rows; @@ -802,7 +821,8 @@ class TicketModel { * Check whether the FULLTEXT index on tickets(title, description) exists. * Result is cached for the process lifetime (static). */ - private function hasFulltextIndex(): bool { + private function hasFulltextIndex(): bool + { static $result = null; if ($result !== null) { return $result; @@ -817,4 +837,4 @@ class TicketModel { $result = $r && (int)$r->fetch_assoc()['cnt'] > 0; return $result; } -} \ No newline at end of file +} diff --git a/models/UserModel.php b/models/UserModel.php index 80a5bfb..219af60 100644 --- a/models/UserModel.php +++ b/models/UserModel.php @@ -1,20 +1,24 @@ ['data' => ..., 'expires' => timestamp]] private static int $cacheTTL = 300; // 5 minutes - public function __construct(mysqli $conn) { + public function __construct(mysqli $conn) + { $this->conn = $conn; } /** * Get cached user data if not expired */ - private static function getCached(string $key): ?array { + private static function getCached(string $key): ?array + { if (isset(self::$userCache[$key])) { $cached = self::$userCache[$key]; if ($cached['expires'] > time()) { @@ -29,7 +33,8 @@ class UserModel { /** * Store user data in cache with expiration */ - private static function setCached(string $key, array $data): void { + private static function setCached(string $key, array $data): void + { self::$userCache[$key] = [ 'data' => $data, 'expires' => time() + self::$cacheTTL @@ -39,7 +44,8 @@ class UserModel { /** * Invalidate specific user cache entry */ - public static function invalidateCache(?int $userId = null, ?string $username = null): void { + public static function invalidateCache(?int $userId = null, ?string $username = null): void + { if ($userId !== null) { unset(self::$userCache["user_id_$userId"]); } @@ -57,7 +63,8 @@ class UserModel { * @param string $groups Comma-separated groups from Remote-Groups header * @return array User data array */ - public function syncUserFromAuthelia(string $username, string $displayName = '', string $email = '', string $groups = ''): array { + public function syncUserFromAuthelia(string $username, string $displayName = '', string $email = '', string $groups = ''): array + { // Check cache first $cacheKey = "user_$username"; $cached = self::getCached($cacheKey); @@ -122,7 +129,8 @@ class UserModel { * * @return array|null System user data or null if not found */ - public function getSystemUser(): ?array { + public function getSystemUser(): ?array + { // Check cache first $cached = self::getCached('system'); if ($cached !== null) { @@ -150,7 +158,8 @@ class UserModel { * @param int $userId User ID * @return array|null User data or null if not found */ - public function getUserById(int $userId): ?array { + public function getUserById(int $userId): ?array + { // Check cache first $cacheKey = "user_id_$userId"; $cached = self::getCached($cacheKey); @@ -180,7 +189,8 @@ class UserModel { * @param string $username Username * @return array|null User data or null if not found */ - public function getUserByUsername(string $username): ?array { + public function getUserByUsername(string $username): ?array + { // Check cache first $cacheKey = "user_$username"; $cached = self::getCached($cacheKey); @@ -210,7 +220,8 @@ class UserModel { * @param string $groups Comma-separated group names * @return bool True if user is in admin group */ - private function checkAdminStatus(string $groups): bool { + private function checkAdminStatus(string $groups): bool + { if (empty($groups)) { return false; } @@ -226,7 +237,8 @@ class UserModel { * @param array $user User data array * @return bool True if user is admin */ - public function isAdmin(array $user): bool { + public function isAdmin(array $user): bool + { return isset($user['is_admin']) && (int)$user['is_admin'] === 1; } @@ -237,7 +249,8 @@ class UserModel { * @param array $requiredGroups Array of required group names * @return bool True if user is in at least one required group */ - public function hasGroupAccess(array $user, array $requiredGroups = ['admin', 'employee']): bool { + public function hasGroupAccess(array $user, array $requiredGroups = ['admin', 'employee']): bool + { if (empty($user['groups'])) { return false; } @@ -253,7 +266,8 @@ class UserModel { * * @return array Array of user records */ - public function getAllUsers(): array { + public function getAllUsers(): array + { $stmt = $this->conn->prepare("SELECT * FROM users ORDER BY created_at DESC"); $stmt->execute(); $result = $stmt->get_result(); @@ -276,7 +290,8 @@ class UserModel { * * @return array Array of unique group names */ - public function getAllGroups(): array { + public function getAllGroups(): array + { $cacheKey = 'all_groups'; // Check cache first @@ -311,7 +326,8 @@ class UserModel { * Invalidate the groups cache * Call this when user groups are modified */ - public static function invalidateGroupsCache(): void { + public static function invalidateGroupsCache(): void + { unset(self::$userCache['all_groups']); } } diff --git a/models/UserPreferencesModel.php b/models/UserPreferencesModel.php index 20efdfe..74010de 100644 --- a/models/UserPreferencesModel.php +++ b/models/UserPreferencesModel.php @@ -1,16 +1,20 @@ conn = $conn; } @@ -19,8 +23,9 @@ class UserPreferencesModel { * @param int $userId User ID * @return array Associative array of preference_key => preference_value */ - public function getUserPreferences(int $userId): array { - return CacheHelper::remember(self::$CACHE_PREFIX, $userId, function() use ($userId) { + public function getUserPreferences(int $userId): array + { + return CacheHelper::remember(self::$CACHE_PREFIX, $userId, function () use ($userId) { $sql = "SELECT preference_key, preference_value FROM user_preferences WHERE user_id = ?"; @@ -45,7 +50,8 @@ class UserPreferencesModel { * @param string $value Preference value * @return bool Success status */ - public function setPreference(int $userId, string $key, string $value): bool { + public function setPreference(int $userId, string $key, string $value): bool + { $sql = "INSERT INTO user_preferences (user_id, preference_key, preference_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE preference_value = VALUES(preference_value)"; @@ -69,7 +75,8 @@ class UserPreferencesModel { * @param mixed $default Default value if preference doesn't exist * @return mixed Preference value or default */ - public function getPreference(int $userId, string $key, $default = null) { + public function getPreference(int $userId, string $key, $default = null) + { $prefs = $this->getUserPreferences($userId); return $prefs[$key] ?? $default; } @@ -80,7 +87,8 @@ class UserPreferencesModel { * @param string $key Preference key * @return bool Success status */ - public function deletePreference(int $userId, string $key): bool { + public function deletePreference(int $userId, string $key): bool + { $sql = "DELETE FROM user_preferences WHERE user_id = ? AND preference_key = ?"; $stmt = $this->conn->prepare($sql); @@ -101,7 +109,8 @@ class UserPreferencesModel { * @param int $userId User ID * @return bool Success status */ - public function deleteAllPreferences(int $userId): bool { + public function deleteAllPreferences(int $userId): bool + { $sql = "DELETE FROM user_preferences WHERE user_id = ?"; $stmt = $this->conn->prepare($sql); $stmt->bind_param("i", $userId); @@ -119,7 +128,8 @@ class UserPreferencesModel { /** * Clear all user preferences cache */ - public static function clearCache(): void { + public static function clearCache(): void + { CacheHelper::delete(self::$CACHE_PREFIX); } } diff --git a/models/WorkflowModel.php b/models/WorkflowModel.php index d457bf7..3906eff 100644 --- a/models/WorkflowModel.php +++ b/models/WorkflowModel.php @@ -1,17 +1,21 @@ conn = $conn; } @@ -20,8 +24,9 @@ class WorkflowModel { * * @return array All active transitions indexed by from_status */ - private function getAllTransitions(): array { - return CacheHelper::remember(self::$CACHE_PREFIX, 'all_transitions', function() { + private function getAllTransitions(): array + { + return CacheHelper::remember(self::$CACHE_PREFIX, 'all_transitions', function () { $sql = "SELECT from_status, to_status, requires_comment, requires_admin FROM status_transitions WHERE is_active = TRUE"; @@ -54,7 +59,8 @@ class WorkflowModel { * @param string $currentStatus Current ticket status * @return array Array of allowed transitions with requirements */ - public function getAllowedTransitions(string $currentStatus): array { + public function getAllowedTransitions(string $currentStatus): array + { $allTransitions = $this->getAllTransitions(); if (!isset($allTransitions[$currentStatus])) { @@ -72,7 +78,8 @@ class WorkflowModel { * @param bool $isAdmin Whether user is admin * @return bool True if transition is allowed */ - public function isTransitionAllowed(string $fromStatus, string $toStatus, bool $isAdmin = false): bool { + public function isTransitionAllowed(string $fromStatus, string $toStatus, bool $isAdmin = false): bool + { // Allow same status (no change) if ($fromStatus === $toStatus) { return true; @@ -98,8 +105,9 @@ class WorkflowModel { * * @return array Array of unique status values */ - public function getAllStatuses(): array { - return CacheHelper::remember(self::$CACHE_PREFIX, 'all_statuses', function() { + public function getAllStatuses(): array + { + return CacheHelper::remember(self::$CACHE_PREFIX, 'all_statuses', function () { $sql = "SELECT DISTINCT from_status as status FROM status_transitions UNION SELECT DISTINCT to_status as status FROM status_transitions @@ -126,7 +134,8 @@ class WorkflowModel { * @param string $toStatus Desired status * @return array|null Transition requirements or null if not found */ - public function getTransitionRequirements(string $fromStatus, string $toStatus): ?array { + public function getTransitionRequirements(string $fromStatus, string $toStatus): ?array + { $allTransitions = $this->getAllTransitions(); if (!isset($allTransitions[$fromStatus][$toStatus])) { @@ -143,7 +152,8 @@ class WorkflowModel { /** * Clear workflow cache (call when transitions are modified) */ - public static function clearCache(): void { + public static function clearCache(): void + { CacheHelper::delete(self::$CACHE_PREFIX); } } diff --git a/views/CreateTicketView.php b/views/CreateTicketView.php index 36a98f0..c668cce 100644 --- a/views/CreateTicketView.php +++ b/views/CreateTicketView.php @@ -1,4 +1,5 @@ "> - + @@ -55,12 +56,12 @@ include __DIR__ . '/layout_header.php';

Selecting a template pre-fills the form fields.

@@ -157,12 +158,12 @@ include __DIR__ . '/layout_header.php';

Leave blank to create as unassigned.

@@ -189,19 +190,19 @@ include __DIR__ . '/layout_header.php';
getAllGroups(); - foreach ($allGroups as $group): - ?> + require_once __DIR__ . '/../models/UserModel.php'; + $userModel = new UserModel($conn); + $allGroups = $userModel->getAllGroups(); + foreach ($allGroups as $group) : + ?> - - + + No groups available
diff --git a/views/DashboardView.php b/views/DashboardView.php index 61b607c..8acdf2e 100644 --- a/views/DashboardView.php +++ b/views/DashboardView.php @@ -1,4 +1,5 @@ 'type', 'value' => $_GET['type'], 'label' => 'Type: ' . htmlspecialchars($_GET['type'])]; } if (!empty($_GET['assigned_to'])) { - $label = match($_GET['assigned_to']) { 'unassigned' => 'Unassigned', 'me' => 'Me', default => 'User #' . htmlspecialchars($_GET['assigned_to']) }; + $label = match ($_GET['assigned_to']) { + 'unassigned' => 'Unassigned', 'me' => 'Me', default => 'User #' . htmlspecialchars($_GET['assigned_to']) + }; $activeFilters[] = ['type' => 'assigned_to', 'value' => $_GET['assigned_to'], 'label' => 'Assigned: ' . $label]; } if (!empty($_GET['created_from']) || !empty($_GET['created_to'])) { - $from = $_GET['created_from'] ?? ''; $to = $_GET['created_to'] ?? ''; + $from = $_GET['created_from'] ?? ''; + $to = $_GET['created_to'] ?? ''; $label = $from === $to && $from ? 'Created: ' . $from : 'Created: ' . ($from ?: '…') . ' – ' . ($to ?: '…'); $activeFilters[] = ['type' => 'created_from', 'value' => $from, 'label' => $label]; } if (!empty($_GET['updated_from']) || !empty($_GET['updated_to'])) { - $from = $_GET['updated_from'] ?? ''; $to = $_GET['updated_to'] ?? ''; + $from = $_GET['updated_from'] ?? ''; + $to = $_GET['updated_to'] ?? ''; $label = $from === $to && $from ? 'Updated: ' . $from : 'Updated: ' . ($from ?: '…') . ' – ' . ($to ?: '…'); $activeFilters[] = ['type' => 'updated_from', 'value' => $from, 'label' => $label]; } if (!empty($_GET['closed_from']) || !empty($_GET['closed_to'])) { - $from = $_GET['closed_from'] ?? ''; $to = $_GET['closed_to'] ?? ''; + $from = $_GET['closed_from'] ?? ''; + $to = $_GET['closed_to'] ?? ''; $label = $from === $to && $from ? 'Closed: ' . $from : 'Closed: ' . ($from ?: '…') . ' – ' . ($to ?: '…'); $activeFilters[] = ['type' => 'closed_from', 'value' => $from, 'label' => $label]; } @@ -99,19 +105,19 @@ include __DIR__ . '/layout_header.php'; - +
- $stats['created_today']) ? 'lt-dot-up' : + $trendOpen = ($stats['closed_today'] > $stats['created_today']) ? 'lt-dot-up' : ($stats['created_today'] > $stats['closed_today'] ? 'lt-dot-warn' : 'lt-dot-idle'); - $trendCrit = ($stats['critical'] > 0) ? 'lt-dot-warn' : 'lt-dot-up'; - $trendUnassi = ($stats['unassigned'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle'; - $trendToday = ($stats['created_today'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle'; - $trendClosed = ($stats['closed_today'] > 0) ? 'lt-dot-up' : 'lt-dot-idle'; - ?> + $trendCrit = ($stats['critical'] > 0) ? 'lt-dot-warn' : 'lt-dot-up'; + $trendUnassi = ($stats['unassigned'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle'; + $trendToday = ($stats['created_today'] > 0) ? 'lt-dot-warn' : 'lt-dot-idle'; + $trendClosed = ($stats['closed_today'] > 0) ? 'lt-dot-up' : 'lt-dot-idle'; + ?>
- 0 ? number_format($avgHours, 1) . ' hours' : 'No data'; - ?> + 0 ? number_format($avgHours, 1) . ' hours' : 'No data'; + ?>
@@ -332,7 +343,7 @@ include __DIR__ . '/layout_header.php'; })(); - + @@ -347,7 +358,7 @@ include __DIR__ . '/layout_header.php'; $maxLoad = max(array_column($byAssignee, 'open_count') ?: [1]); ?>
- 0 ? round(($count / $maxLoad) * 100) : 0; @@ -357,10 +368,10 @@ include __DIR__ . '/layout_header.php'; $avatarColors = ['lt-avatar--orange', 'lt-avatar--green', 'lt-avatar--purple', '']; $avatarColor = $avatarColors[abs(crc32($name)) % count($avatarColors)]; $userId = (int)($a['user_id'] ?? 0); - ?> + ?>