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;
|
$parameters = $data['parameters'] ?? null;
|
||||||
|
|
||||||
// Validate input
|
// 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']);
|
echo json_encode(['success' => false, 'error' => 'Operation type and ticket IDs required']);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,14 @@ class BulkOperationsModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
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) {
|
if ($success) {
|
||||||
|
|||||||
@@ -716,6 +716,76 @@ class TicketModel {
|
|||||||
return $result;
|
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.
|
* Check whether the FULLTEXT index on tickets(title, description) exists.
|
||||||
* Result is cached for the process lifetime (static).
|
* Result is cached for the process lifetime (static).
|
||||||
|
|||||||
Reference in New Issue
Block a user