Files
tinker_tickets/api/export_tickets.php
jared c90bdc8ac8
Lint / PHP (phpcs PSR-12) (push) Failing after 29s
Lint / JS (eslint) (push) Successful in 12s
style: auto-fix 1340 phpcs PSR-12 violations via phpcbf; exclude MissingNamespace and SideEffects
2026-04-13 20:56:10 -04:00

247 lines
9.6 KiB
PHP

<?php
/**
* Export Tickets API
*
* Exports tickets to CSV format with optional filtering
* Respects ticket visibility settings
*/
// Disable error display in the output
ini_set('display_errors', 0);
error_reporting(E_ALL);
// Apply rate limiting
require_once dirname(__DIR__) . '/middleware/RateLimitMiddleware.php';
RateLimitMiddleware::apply('api');
try {
// Include required files
require_once dirname(__DIR__) . '/config/config.php';
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/models/TicketModel.php';
require_once dirname(__DIR__) . '/models/CommentModel.php';
require_once dirname(__DIR__) . '/models/AuditLogModel.php';
// Check authentication via session
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);
echo json_encode(['success' => false, 'error' => 'Authentication required']);
exit;
}
$currentUser = $_SESSION['user'];
// Use centralized database connection
$conn = Database::getConnection();
// Get filter parameters
$status = isset($_GET['status']) ? $_GET['status'] : null;
$category = isset($_GET['category']) ? $_GET['category'] : null;
$type = isset($_GET['type']) ? $_GET['type'] : null;
$search = isset($_GET['search']) ? trim($_GET['search']) : null;
$format = isset($_GET['format']) ? $_GET['format'] : 'csv';
$ticketIds = isset($_GET['ticket_ids']) ? $_GET['ticket_ids'] : null;
$singleIdRaw = isset($_GET['ticket_id']) ? trim($_GET['ticket_id']) : null;
$singleId = ($singleIdRaw !== null && ctype_digit($singleIdRaw) && (int)$singleIdRaw > 0) ? $singleIdRaw : null;
// Initialize model
$ticketModel = new TicketModel($conn);
// Check if specific ticket IDs are provided
if ($ticketIds) {
// Parse and validate ticket IDs
$ticketIdArray = array_filter(array_map('trim', explode(',', $ticketIds)));
if (empty($ticketIdArray)) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'No valid ticket IDs provided']);
exit;
}
// Get specific tickets by IDs
$allTickets = $ticketModel->getTicketsByIds($ticketIdArray);
// Filter tickets based on visibility - only export tickets the user can access
$tickets = [];
foreach ($allTickets as $ticket) {
if ($ticketModel->canUserAccessTicket($ticket, $currentUser)) {
$tickets[] = $ticket;
}
}
} else {
// Get all tickets with filters (no pagination for export)
// Pass $currentUser so visibility filtering is applied correctly
$result = $ticketModel->getAllTickets(1, 10000, $status, 'created_at', 'desc', $category, $type, $search, [], $currentUser);
$tickets = $result['tickets'];
}
if ($format === 'csv') {
// CSV Export
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="tickets_export_' . date('Y-m-d_His') . '.csv"');
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
// Create output stream
$output = fopen('php://output', 'w');
// Add BOM for Excel UTF-8 compatibility
fprintf($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
// CSV Headers
$headers = [
'Ticket ID',
'Title',
'Status',
'Priority',
'Category',
'Type',
'Created By',
'Assigned To',
'Created At',
'Updated At',
'Description'
];
fputcsv($output, $headers);
// CSV Data
foreach ($tickets as $ticket) {
$row = [
$ticket['ticket_id'],
$ticket['title'],
$ticket['status'],
'P' . $ticket['priority'],
$ticket['category'],
$ticket['type'],
$ticket['creator_display_name'] ?? $ticket['creator_username'] ?? 'System',
$ticket['assigned_display_name'] ?? $ticket['assigned_username'] ?? 'Unassigned',
$ticket['created_at'],
$ticket['updated_at'],
$ticket['description']
];
fputcsv($output, $row);
}
fclose($output);
exit;
} elseif ($format === 'json') {
// JSON Export
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="tickets_export_' . date('Y-m-d_His') . '.json"');
echo json_encode([
'exported_at' => date('c'),
'total_tickets' => count($tickets),
'tickets' => array_map(function ($t) {
return [
'ticket_id' => $t['ticket_id'],
'title' => $t['title'],
'status' => $t['status'],
'priority' => $t['priority'],
'category' => $t['category'],
'type' => $t['type'],
'description' => $t['description'],
'created_by' => $t['creator_display_name'] ?? $t['creator_username'],
'assigned_to' => $t['assigned_display_name'] ?? $t['assigned_username'],
'created_at' => $t['created_at'],
'updated_at' => $t['updated_at']
];
}, $tickets)
], JSON_PRETTY_PRINT);
exit;
} elseif ($format === 'full') {
// Full single-ticket export: ticket + all comments + audit timeline
if (!$singleId) {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'format=full requires a ticket_id parameter']);
exit;
}
$ticket = $ticketModel->getTicketById($singleId);
if (!$ticket || !$ticketModel->canUserAccessTicket($ticket, $currentUser)) {
header('Content-Type: application/json');
http_response_code(404);
echo json_encode(['success' => false, 'error' => 'Ticket not found']);
exit;
}
$commentModel = new CommentModel($conn);
$auditLogModel = new AuditLogModel($conn);
// Load flat comment list (no threading nesting in export)
$rawComments = $commentModel->getCommentsByTicketId($ticket['ticket_id'], false);
$timeline = $auditLogModel->getTicketTimeline((string)$ticket['ticket_id']);
$comments = array_map(function ($c) {
return [
'comment_id' => $c['comment_id'],
'author' => $c['display_name'] ?? $c['username'] ?? 'Unknown',
'created_at' => $c['created_at'],
'updated_at' => $c['updated_at'] ?? null,
'comment_text' => $c['comment_text'],
'parent_comment_id' => $c['parent_comment_id'] ?? null,
];
}, $rawComments);
$timelineOut = array_map(function ($row) {
$details = $row['details'];
if (is_string($details)) {
$details = json_decode($details, true) ?? $details;
}
return [
'action' => $row['action_type'],
'entity' => $row['entity_type'],
'actor' => $row['display_name'] ?? $row['username'] ?? 'System',
'details' => $details,
'created_at' => $row['created_at'],
];
}, $timeline);
header('Content-Type: application/json');
header('Content-Disposition: attachment; filename="ticket_' . $ticket['ticket_id'] . '_full_' . date('Y-m-d_His') . '.json"');
header('Cache-Control: no-cache, must-revalidate');
echo json_encode([
'exported_at' => date('c'),
'ticket' => [
'ticket_id' => $ticket['ticket_id'],
'title' => $ticket['title'],
'status' => $ticket['status'],
'priority' => 'P' . $ticket['priority'],
'category' => $ticket['category'],
'type' => $ticket['type'],
'visibility' => $ticket['visibility'] ?? 'public',
'description' => $ticket['description'],
'created_by' => $ticket['creator_display_name'] ?? $ticket['creator_username'] ?? 'System',
'assigned_to' => $ticket['assigned_display_name'] ?? $ticket['assigned_username'] ?? 'Unassigned',
'created_at' => $ticket['created_at'],
'updated_at' => $ticket['updated_at'],
'closed_at' => $ticket['closed_at'] ?? null,
],
'comments' => $comments,
'comment_count' => count($comments),
'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');
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'An internal error occurred'
]);
}