style: auto-fix 1340 phpcs PSR-12 violations via phpcbf; exclude MissingNamespace and SideEffects
Lint / PHP (phpcs PSR-12) (push) Failing after 29s
Lint / JS (eslint) (push) Successful in 12s

This commit is contained in:
2026-04-13 20:56:10 -04:00
parent b6df647921
commit c90bdc8ac8
80 changed files with 1674 additions and 1092 deletions
+7 -4
View File
@@ -1,4 +1,5 @@
<?php
// Disable error display in the output
ini_set('display_errors', 0);
error_reporting(E_ALL);
@@ -146,13 +147,16 @@ try {
// Notify watchers of the new comment
NotificationHelper::notifyWatchers(
$conn, $ticketId, $ticketTitle, 'comment_added',
$conn,
$ticketId,
$ticketTitle,
'comment_added',
['author' => $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'
]);
}
}
+1
View File
@@ -1,4 +1,5 @@
<?php
require_once __DIR__ . '/bootstrap.php';
require_once dirname(__DIR__) . '/models/TicketModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
+43 -14
View File
@@ -1,4 +1,5 @@
<?php
/**
* Audit Log API Endpoint
* Handles fetching filtered audit logs and CSV export
@@ -23,13 +24,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
if (isset($_GET['export']) && $_GET['export'] === 'csv') {
// 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 all matching logs (no limit for CSV export)
$result = $auditLogModel->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);
+3 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* API Bootstrap - Common setup for API endpoints
*
@@ -54,7 +55,8 @@ $conn = Database::getConnection();
* Output a JSON response, appending the rotated CSRF token so the
* client-side lt.api interceptor can update window.CSRF_TOKEN.
*/
function apiRespond(array $data): void {
function apiRespond(array $data): void
{
if (!empty($GLOBALS['_new_csrf_token'])) {
$data['csrf_token'] = $GLOBALS['_new_csrf_token'];
}
+2 -1
View File
@@ -1,4 +1,5 @@
<?php
// Apply rate limiting
require_once dirname(__DIR__) . '/middleware/RateLimitMiddleware.php';
RateLimitMiddleware::apply('api');
@@ -51,7 +52,7 @@ if (!$operationType || !in_array($operationType, $validOperationTypes, true) ||
}
// Validate ticket IDs: must be non-empty numeric strings (allows leading zeros)
$ticketIds = array_values(array_filter(array_map(function($id) {
$ticketIds = array_values(array_filter(array_map(function ($id) {
$s = trim((string)$id);
return (ctype_digit($s) && (int)$s > 0) ? $s : null;
}, $ticketIds)));
+2 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* Check for duplicate tickets API
*
@@ -91,7 +92,7 @@ while ($row = $result->fetch_assoc()) {
$stmt->close();
// Sort by similarity descending
usort($duplicates, function($a, $b) {
usort($duplicates, function ($a, $b) {
return $b['similarity'] - $a['similarity'];
});
+1 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* Clone Ticket API
* Creates a copy of an existing ticket with the same properties
@@ -126,7 +127,6 @@ try {
'error' => $result['error'] ?? 'Failed to create cloned ticket'
]);
}
} catch (Exception $e) {
error_log("Clone ticket API error: " . $e->getMessage());
http_response_code(500);
+4 -2
View File
@@ -1,4 +1,5 @@
<?php
/**
* Custom Fields Management API
* CRUD operations for custom field definitions
@@ -16,7 +17,9 @@ try {
require_once dirname(__DIR__) . '/models/CustomFieldModel.php';
// Check authentication
if (session_status() === PHP_SESSION_NONE) session_start();
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
http_response_code(401);
echo json_encode(['success' => 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);
+1 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* Delete Attachment API
*
@@ -114,7 +115,6 @@ try {
);
ResponseHelper::success([], 'Attachment deleted successfully');
} catch (Exception $e) {
ResponseHelper::serverError('Failed to delete attachment');
}
+1 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* API endpoint for deleting a comment
*/
@@ -111,7 +112,6 @@ try {
header('Content-Type: application/json');
echo json_encode($result);
} catch (Exception $e) {
ob_end_clean();
error_log("Delete comment API error: " . $e->getMessage());
+1 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* Download Attachment API
*
@@ -131,7 +132,6 @@ try {
fclose($handle);
exit;
} catch (Exception $e) {
http_response_code(500);
header('Content-Type: application/json');
+7 -8
View File
@@ -1,4 +1,5 @@
<?php
/**
* Export Tickets API
*
@@ -23,7 +24,9 @@ try {
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
// Check authentication via session
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
header('Content-Type: application/json');
http_response_code(401);
@@ -126,7 +129,6 @@ try {
fclose($output);
exit;
} elseif ($format === 'json') {
// JSON Export
header('Content-Type: application/json');
@@ -135,7 +137,7 @@ try {
echo json_encode([
'exported_at' => 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');
+4 -2
View File
@@ -1,4 +1,5 @@
<?php
// API endpoint for generating API keys (Admin only)
error_reporting(E_ALL);
ini_set('display_errors', 0);
@@ -19,7 +20,9 @@ try {
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
// Check authentication via session
if (session_status() === PHP_SESSION_NONE) session_start();
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
throw new Exception("Authentication required");
}
@@ -105,7 +108,6 @@ try {
'key_id' => $result['key_id'],
'expires_at' => $result['expires_at']
]);
} catch (Exception $e) {
ob_end_clean();
error_log("Generate API key error: " . $e->getMessage());
+1
View File
@@ -1,4 +1,5 @@
<?php
/**
* Get Comments API
* Returns paginated comments for a ticket (used by "Load more" on ticket view)
+4 -2
View File
@@ -1,4 +1,5 @@
<?php
/**
* Get Template API
* Returns a ticket template by ID
@@ -11,7 +12,9 @@ require_once dirname(__DIR__) . '/helpers/ErrorHandler.php';
ErrorHandler::init();
try {
if (session_status() === PHP_SESSION_NONE) session_start();
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/TemplateModel.php';
@@ -43,7 +46,6 @@ try {
} else {
ErrorHandler::sendNotFoundError('Template not found');
}
} catch (Exception $e) {
ErrorHandler::log($e->getMessage(), E_ERROR);
ErrorHandler::sendErrorResponse('Failed to retrieve template', 500, $e);
+1 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* Get Users API
* Returns list of users for @mentions autocomplete
@@ -24,7 +25,6 @@ try {
}
echo json_encode(['success' => true, 'users' => $users]);
} catch (Exception $e) {
error_log("Get users API error: " . $e->getMessage());
http_response_code(500);
+1
View File
@@ -1,4 +1,5 @@
<?php
/**
* Health Check Endpoint
*
+6 -3
View File
@@ -1,4 +1,5 @@
<?php
/**
* Recurring Tickets Management API
* CRUD operations for recurring_tickets table
@@ -16,7 +17,9 @@ try {
require_once dirname(__DIR__) . '/models/RecurringTicketModel.php';
// Check authentication
if (session_status() === PHP_SESSION_NONE) session_start();
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
http_response_code(401);
echo json_encode(['success' => 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';
+8 -4
View File
@@ -1,4 +1,5 @@
<?php
/**
* Template Management API
* CRUD operations for ticket_templates table
@@ -15,7 +16,9 @@ try {
require_once dirname(__DIR__) . '/helpers/Database.php';
// Check authentication
if (session_status() === PHP_SESSION_NONE) session_start();
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
http_response_code(401);
echo json_encode(['success' => 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);
+4 -2
View File
@@ -1,4 +1,5 @@
<?php
/**
* Workflow/Status Transitions Management API
* CRUD operations for status_transitions table
@@ -17,7 +18,9 @@ try {
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
// Check authentication
if (session_status() === PHP_SESSION_NONE) session_start();
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
http_response_code(401);
echo json_encode(['success' => 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);
+18 -6
View File
@@ -1,4 +1,5 @@
<?php
/**
* Notifications API
*
@@ -11,6 +12,7 @@
* - Status changes on watched (via ticket_watchers)
* - @mentions in comments (action_type='comment', details.mentions[] contains username)
*/
require_once __DIR__ . '/bootstrap.php';
require_once dirname(__DIR__) . '/models/UserPreferencesModel.php';
@@ -75,7 +77,10 @@ $stmt = $conn->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'] ?? '?');
+4 -2
View File
@@ -1,4 +1,5 @@
<?php
// API endpoint for revoking API keys (Admin only)
error_reporting(E_ALL);
ini_set('display_errors', 0);
@@ -19,7 +20,9 @@ try {
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
// Check authentication via session
if (session_status() === PHP_SESSION_NONE) session_start();
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user']) || !isset($_SESSION['user']['user_id'])) {
throw new Exception("Authentication required");
}
@@ -98,7 +101,6 @@ try {
'success' => true,
'message' => 'API key revoked successfully'
]);
} catch (Exception $e) {
ob_end_clean();
error_log("Revoke API key error: " . $e->getMessage());
+2 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* Saved Filters API Endpoint
* Handles GET (fetch filters), POST (create filter), PUT (update filter), DELETE (delete filter)
@@ -22,7 +23,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
http_response_code(404);
apiRespond(['success' => 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]);
+134 -133
View File
@@ -1,4 +1,5 @@
<?php
/**
* Ticket Dependencies API
*/
@@ -8,7 +9,7 @@ ob_start();
header('Content-Type: application/json');
// Register shutdown function to catch fatal errors
register_shutdown_function(function() {
register_shutdown_function(function () {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
// Log detailed error server-side
@@ -27,7 +28,7 @@ ini_set('display_errors', 0);
error_reporting(E_ALL);
// Custom error handler
set_error_handler(function($errno, $errstr, $errfile, $errline) {
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
// Log detailed error server-side
error_log("PHP Error in ticket_dependencies.php: $errstr in $errfile:$errline");
ob_end_clean();
@@ -41,7 +42,7 @@ set_error_handler(function($errno, $errstr, $errfile, $errline) {
});
// Custom exception handler
set_exception_handler(function($e) {
set_exception_handler(function ($e) {
// Log detailed error server-side
error_log('Exception in ticket_dependencies.php: ' . $e->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());
+1 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* API endpoint for updating a comment
*/
@@ -100,7 +101,6 @@ try {
header('Content-Type: application/json');
echo json_encode($result);
} catch (Exception $e) {
ob_end_clean();
error_log("Update comment API error: " . $e->getMessage());
+17 -12
View File
@@ -1,4 +1,5 @@
<?php
// Enable error reporting for debugging
error_reporting(E_ALL);
ini_set('display_errors', 0); // Don't display errors in the response
@@ -53,7 +54,8 @@ try {
$isAdmin = $currentUser['is_admin'] ?? false;
// Updated controller class that handles partial updates
class ApiTicketController {
class ApiTicketController
{
private $conn;
private $ticketModel;
private $commentModel;
@@ -63,7 +65,8 @@ try {
private $isAdmin;
private $currentUser;
public function __construct($conn, $userId = null, $isAdmin = false, $currentUser = []) {
public function __construct($conn, $userId = null, $isAdmin = false, $currentUser = [])
{
$this->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'
]);
}
?>
+1 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* Upload Attachment API
*
@@ -229,7 +230,6 @@ try {
'uploaded_by' => $_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)) {
+1
View File
@@ -1,4 +1,5 @@
<?php
/**
* User Avatar API
*
+4 -1
View File
@@ -1,4 +1,5 @@
<?php
/**
* User Preferences API Endpoint
* Handles GET (fetch preferences) and POST (update preference)
@@ -42,7 +43,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
foreach ($data['preferences'] as $key => $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']);
+2
View File
@@ -1,10 +1,12 @@
<?php
/**
* Watch / Unwatch Ticket API
*
* GET ?ticket_id=N → returns { watching: bool, watcher_count: int }
* POST { ticket_id, action: 'watch'|'unwatch' } → toggles watcher row
*/
require_once __DIR__ . '/bootstrap.php';
require_once dirname(__DIR__) . '/models/TicketModel.php';