Implement bulk_delete operation; validate operation types
- TicketModel: add deleteTicket() that removes all child records (comments, watchers, dependencies, attachments, custom fields) then deletes the ticket and cleans up physical attachment files - BulkOperationsModel: add bulk_delete case to processBulkOperation() so the "Bulk Delete" UI button actually works instead of silently failing with N failures - bulk_operation.php: validate operation_type against whitelist to reject unknown operations early with a proper error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,8 @@ $ticketIds = $data['ticket_ids'] ?? [];
|
||||
$parameters = $data['parameters'] ?? null;
|
||||
|
||||
// Validate input
|
||||
if (!$operationType || empty($ticketIds)) {
|
||||
$validOperationTypes = ['bulk_close', 'bulk_assign', 'bulk_priority', 'bulk_status', 'bulk_delete'];
|
||||
if (!$operationType || !in_array($operationType, $validOperationTypes, true) || empty($ticketIds)) {
|
||||
echo json_encode(['success' => false, 'error' => 'Operation type and ticket IDs required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -173,6 +173,14 @@ class BulkOperationsModel {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -716,6 +716,76 @@ class TicketModel {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a ticket and all its associated records.
|
||||
* Admin-only operation. Removes comments, attachments, watchers, dependencies.
|
||||
*
|
||||
* @param string $ticketId Ticket ID
|
||||
* @return bool Success status
|
||||
*/
|
||||
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 = ?");
|
||||
if ($attStmt) {
|
||||
$attStmt->bind_param('s', $ticketId);
|
||||
$attStmt->execute();
|
||||
$attResult = $attStmt->get_result();
|
||||
while ($row = $attResult->fetch_assoc()) {
|
||||
$attachmentFiles[] = $row['filename'];
|
||||
}
|
||||
$attStmt->close();
|
||||
}
|
||||
|
||||
// Delete child records first to avoid FK constraint failures
|
||||
$children = [
|
||||
"DELETE FROM ticket_comments WHERE ticket_id = ?",
|
||||
"DELETE FROM ticket_watchers WHERE ticket_id = ?",
|
||||
"DELETE FROM ticket_dependencies WHERE ticket_id = ? OR depends_on_id = ?",
|
||||
"DELETE FROM ticket_attachments WHERE ticket_id = ?",
|
||||
"DELETE FROM ticket_custom_fields WHERE ticket_id = ?",
|
||||
];
|
||||
|
||||
foreach ($children as $sql) {
|
||||
$stmt = $this->conn->prepare($sql);
|
||||
if (!$stmt) continue;
|
||||
// ticket_dependencies uses two placeholders
|
||||
if (strpos($sql, 'depends_on_id') !== false) {
|
||||
$stmt->bind_param('ss', $ticketId, $ticketId);
|
||||
} else {
|
||||
$stmt->bind_param('s', $ticketId);
|
||||
}
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
$stmt = $this->conn->prepare("DELETE FROM tickets WHERE ticket_id = ?");
|
||||
if (!$stmt) return false;
|
||||
$stmt->bind_param('s', $ticketId);
|
||||
$result = $stmt->execute();
|
||||
$affected = $stmt->affected_rows;
|
||||
$stmt->close();
|
||||
|
||||
if ($result && $affected > 0) {
|
||||
// Clean up physical attachment files
|
||||
$uploadDir = defined('UPLOAD_DIR')
|
||||
? UPLOAD_DIR
|
||||
: (isset($GLOBALS['config']['UPLOAD_DIR']) ? $GLOBALS['config']['UPLOAD_DIR'] : dirname(__DIR__) . '/uploads');
|
||||
$ticketDir = rtrim($uploadDir, '/') . '/' . $ticketId;
|
||||
if (is_dir($ticketDir)) {
|
||||
foreach ($attachmentFiles as $filename) {
|
||||
$file = $ticketDir . '/' . basename($filename);
|
||||
if (file_exists($file)) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
@rmdir($ticketDir); // Remove dir only if empty
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the FULLTEXT index on tickets(title, description) exists.
|
||||
* Result is cached for the process lifetime (static).
|
||||
|
||||
Reference in New Issue
Block a user