Audit fixes: security, dead code removal, API consolidation, JS dedup

Security:
- Fix IDOR in delete/update comment (add ticket visibility check)
- XSS defense-in-depth in DashboardView active filters
- Replace innerHTML with DOM construction in toast.js
- Remove redundant real_escape_string in check_duplicates
- Add rate limiting to get_template, download_attachment, audit_log,
  saved_filters, user_preferences endpoints

Bug fixes:
- Session timeout now reads from config instead of hardcoded 18000
- TicketController uses $GLOBALS['config'] instead of duplicate .env parsing
- Add DISCORD_WEBHOOK_URL to centralized config
- Cleanup script uses hashmap for O(1) ticket ID lookups

Dead code removal (~100 lines):
- Remove dead getTicketComments() from TicketModel (wrong bind_param type)
- Remove dead getCategories()/getTypes() from DashboardController
- Remove ~80 lines dead Discord webhook code from update_ticket API

Consolidation:
- Create api/bootstrap.php for shared API setup (auth, CSRF, rate limit)
- Convert 6 API endpoints to use bootstrap
- Extract escapeHtml/getTicketIdFromUrl into shared utils.js
- Batch save for user preferences (1 request instead of 7)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 14:50:06 -05:00
parent 15063838bd
commit bcc163bc77
26 changed files with 197 additions and 389 deletions

View File

@@ -15,28 +15,7 @@ try {
$configPath = dirname(__DIR__) . '/config/config.php';
require_once $configPath;
require_once dirname(__DIR__) . '/helpers/Database.php';
require_once dirname(__DIR__) . '/helpers/UrlHelper.php';
// Load environment variables (for Discord webhook)
$envPath = dirname(__DIR__) . '/.env';
$envVars = [];
if (file_exists($envPath)) {
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos($line, '=') !== false && strpos($line, '#') !== 0) {
list($key, $value) = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
// Remove surrounding quotes if present
if ((substr($value, 0, 1) === '"' && substr($value, -1) === '"') ||
(substr($value, 0, 1) === "'" && substr($value, -1) === "'")) {
$value = substr($value, 1, -1);
}
$envVars[$key] = $value;
}
}
}
// Load models directly with absolute paths
$ticketModelPath = dirname(__DIR__) . '/models/TicketModel.php';
$commentModelPath = dirname(__DIR__) . '/models/CommentModel.php';
@@ -76,16 +55,14 @@ try {
private $commentModel;
private $auditLog;
private $workflowModel;
private $envVars;
private $userId;
private $isAdmin;
public function __construct($conn, $envVars = [], $userId = null, $isAdmin = false) {
public function __construct($conn, $userId = null, $isAdmin = false) {
$this->ticketModel = new TicketModel($conn);
$this->commentModel = new CommentModel($conn);
$this->auditLog = new AuditLogModel($conn);
$this->workflowModel = new WorkflowModel($conn);
$this->envVars = $envVars;
$this->userId = $userId;
$this->isAdmin = $isAdmin;
}
@@ -184,9 +161,6 @@ try {
$this->auditLog->logTicketUpdate($this->userId, $id, $data);
}
// Discord webhook disabled for updates - only send for new tickets
// $this->sendDiscordWebhook($id, $currentTicket, $updateData, $data);
return [
'success' => true,
'status' => $updateData['status'],
@@ -194,87 +168,6 @@ try {
'message' => 'Ticket updated successfully'
];
}
private function sendDiscordWebhook($ticketId, $oldData, $newData, $changedFields) {
if (!isset($this->envVars['DISCORD_WEBHOOK_URL']) || empty($this->envVars['DISCORD_WEBHOOK_URL'])) {
return;
}
$webhookUrl = $this->envVars['DISCORD_WEBHOOK_URL'];
// Determine what fields actually changed
$changes = [];
foreach ($changedFields as $field => $newValue) {
if ($field === 'ticket_id') continue; // Skip ticket_id
$oldValue = $oldData[$field] ?? 'N/A';
if ($oldValue != $newValue) {
$changes[] = [
'name' => ucfirst($field),
'value' => "$oldValue$newValue",
'inline' => true
];
}
}
if (empty($changes)) {
return;
}
// Create ticket URL using validated host
$ticketUrl = UrlHelper::ticketUrl($ticketId);
// Determine embed color based on priority
$colors = [
1 => 0xff4d4d, // Red
2 => 0xffa726, // Orange
3 => 0x42a5f5, // Blue
4 => 0x66bb6a, // Green
5 => 0x9e9e9e // Gray
];
$color = $colors[$newData['priority']] ?? 0x3498db;
$embed = [
'title' => '🔄 Ticket Updated',
'description' => "**#{$ticketId}** - " . $newData['title'],
'color' => $color,
'fields' => array_merge($changes, [
[
'name' => '🔗 View Ticket',
'value' => "[Click here to view]($ticketUrl)",
'inline' => false
]
]),
'footer' => [
'text' => 'Tinker Tickets'
],
'timestamp' => date('c')
];
$payload = [
'embeds' => [$embed]
];
// Send webhook
$ch = curl_init($webhookUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$webhookResult = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// Log webhook errors instead of silencing them
if ($curlError) {
error_log("Discord webhook cURL error for ticket #{$ticketId}: {$curlError}");
} elseif ($httpCode !== 204 && $httpCode !== 200) {
error_log("Discord webhook failed for ticket #{$ticketId}. HTTP Code: {$httpCode}, Response: " . substr($webhookResult, 0, 200));
}
}
}
// Use centralized database connection
@@ -300,7 +193,7 @@ try {
$ticketId = (int)$data['ticket_id'];
// Initialize controller
$controller = new ApiTicketController($conn, $envVars, $userId, $isAdmin);
$controller = new ApiTicketController($conn, $userId, $isAdmin);
// Update ticket
$result = $controller->update($ticketId, $data);