Harden recurring cron, bulk delete, error handling; fix semgrep CI
Continued fixes from the multi-agent review: - recurring tickets cron: now that the parse error is fixed the job runs, exposing two latent bugs. (1) next_run_at was only advanced after the full success path, so any failure (e.g. a NULL created_by passed to the non-nullable assignTicket() $assignedBy -> TypeError) left it in the past and re-created a duplicate ticket every cron cycle. Added an atomic claimForRun() (conditional UPDATE gated on still-due) called BEFORE creation, which also prevents overlapping runs from double-creating. (2) The cron used a raw mysqli with no utf8mb4, corrupting non-ASCII content; it now uses Database::getConnection(). Also guard the assignment so created_by NULL falls back to the assignee. - bulk delete: attachment files were unlinked inside the DB transaction, so an atomic-mode rollback restored rows but the files were already gone. deleteTicket() can now defer file removal to the caller, and BulkOperationsModel deletes files only after a successful commit. - UserModel: back-tick the `groups` column (reserved word on MySQL 8.0.2+). - create_ticket_api.php: stop leaking raw DB/exception messages to callers; log server-side and return a generic error. (Also includes a pre-existing working-tree tweak that adds title to the manual-ticket dedupe hash.) - CI: semgrep install failed under PEP 668; add --break-system-packages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+18
-6
@@ -740,9 +740,13 @@ class TicketModel
|
||||
* Admin-only operation. Removes comments, attachments, watchers, dependencies.
|
||||
*
|
||||
* @param string $ticketId Ticket ID
|
||||
* @param array|null &$deferredFiles When provided, attachment file paths to
|
||||
* remove are appended here instead of being unlinked immediately, so a
|
||||
* caller running inside a DB transaction can delete them only AFTER a
|
||||
* successful commit (avoids destroying files for a rolled-back delete).
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function deleteTicket(string $ticketId): bool
|
||||
public function deleteTicket(string $ticketId, ?array &$deferredFiles = null): bool
|
||||
{
|
||||
// Collect attachment filenames before deleting DB rows
|
||||
$attachmentFiles = [];
|
||||
@@ -804,13 +808,21 @@ class TicketModel
|
||||
: (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);
|
||||
if ($deferredFiles !== null) {
|
||||
// Defer physical deletion to the caller (post-commit).
|
||||
foreach ($attachmentFiles as $filename) {
|
||||
$deferredFiles[] = $ticketDir . '/' . basename($filename);
|
||||
}
|
||||
$deferredFiles[] = $ticketDir; // dir removed last, only if empty
|
||||
} else {
|
||||
foreach ($attachmentFiles as $filename) {
|
||||
$file = $ticketDir . '/' . basename($filename);
|
||||
if (file_exists($file)) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
@rmdir($ticketDir); // Remove dir only if empty
|
||||
}
|
||||
@rmdir($ticketDir); // Remove dir only if empty
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user