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:
@@ -16,6 +16,7 @@ chdir(dirname(__DIR__));
|
||||
|
||||
// Include required files
|
||||
require_once 'config/config.php';
|
||||
require_once 'helpers/Database.php';
|
||||
require_once 'models/RecurringTicketModel.php';
|
||||
require_once 'models/TicketModel.php';
|
||||
require_once 'models/AuditLogModel.php';
|
||||
@@ -29,17 +30,9 @@ function logMessage($message)
|
||||
logMessage("Starting recurring tickets cron job");
|
||||
|
||||
try {
|
||||
// Create database connection
|
||||
$conn = new mysqli(
|
||||
$GLOBALS['config']['DB_HOST'],
|
||||
$GLOBALS['config']['DB_USER'],
|
||||
$GLOBALS['config']['DB_PASS'],
|
||||
$GLOBALS['config']['DB_NAME']
|
||||
);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
throw new Exception("Database connection failed: " . $conn->connect_error);
|
||||
}
|
||||
// Create database connection (Database::getConnection sets utf8mb4 so
|
||||
// non-ASCII titles/descriptions aren't corrupted on insert).
|
||||
$conn = Database::getConnection();
|
||||
|
||||
// Initialize models
|
||||
$recurringModel = new RecurringTicketModel($conn);
|
||||
@@ -57,6 +50,14 @@ try {
|
||||
logMessage("Processing recurring ticket ID: " . $recurring['recurring_id']);
|
||||
|
||||
try {
|
||||
// Claim the schedule FIRST (atomic advance of next_run_at). If another
|
||||
// cron run already claimed it, or it's no longer due, skip it — this
|
||||
// prevents duplicate-ticket floods if a later step throws.
|
||||
if (!$recurringModel->claimForRun($recurring['recurring_id'])) {
|
||||
logMessage("Skipped (already claimed or not due): " . $recurring['recurring_id']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prepare ticket data
|
||||
$ticketData = [
|
||||
'title' => processTemplate($recurring['title_template']),
|
||||
@@ -74,9 +75,12 @@ try {
|
||||
$ticketId = $result['ticket_id'];
|
||||
logMessage("Created ticket: " . $ticketId);
|
||||
|
||||
// Assign to user if specified
|
||||
if ($recurring['assigned_to']) {
|
||||
$ticketModel->assignTicket($ticketId, $recurring['assigned_to'], $recurring['created_by']);
|
||||
// Assign to user if specified. assignTicket() requires a non-null
|
||||
// "assigned_by"; fall back to the assignee when created_by is null
|
||||
// (recurring schedules may have no creator).
|
||||
if (!empty($recurring['assigned_to'])) {
|
||||
$assignedBy = (int)($recurring['created_by'] ?? $recurring['assigned_to']);
|
||||
$ticketModel->assignTicket($ticketId, (int)$recurring['assigned_to'], $assignedBy);
|
||||
}
|
||||
|
||||
// Log to audit
|
||||
@@ -88,9 +92,6 @@ try {
|
||||
['source' => 'recurring', 'recurring_id' => $recurring['recurring_id']]
|
||||
);
|
||||
|
||||
// Update the recurring ticket's next run time
|
||||
$recurringModel->updateAfterRun($recurring['recurring_id']);
|
||||
|
||||
$created++;
|
||||
} else {
|
||||
logMessage("ERROR: Failed to create ticket - " . ($result['error'] ?? 'Unknown error'));
|
||||
@@ -104,7 +105,7 @@ try {
|
||||
|
||||
logMessage("Completed: Created $created tickets, $errors errors");
|
||||
|
||||
$conn->close();
|
||||
Database::close();
|
||||
} catch (Exception $e) {
|
||||
logMessage("FATAL ERROR: " . $e->getMessage());
|
||||
exit(1);
|
||||
|
||||
Reference in New Issue
Block a user