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 @@
">
-
+
Error: = htmlspecialchars($error, ENT_QUOTES, 'UTF-8') ?>
@@ -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);
- ?>
+ ?>
- 0): ?>
+ 0) : ?>
= htmlspecialchars($initials) ?>
@@ -415,7 +426,7 @@ include __DIR__ . '/layout_header.php';