style: auto-fix 1340 phpcs PSR-12 violations via phpcbf; exclude MissingNamespace and SideEffects
This commit is contained in:
+7
-4
@@ -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,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
@@ -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
@@ -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'];
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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,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,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,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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Get Comments API
|
||||
* Returns paginated comments for a ticket (used by "Load more" on ticket view)
|
||||
|
||||
@@ -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
@@ -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,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Health Check Endpoint
|
||||
*
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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'] ?? '?');
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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,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,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* User Avatar API
|
||||
*
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user