Verified, high-confidence fixes from a project-wide review: - markdown.js: escape " and ' in the HTML-escape step. User-controlled image/link URLs and alt text were interpolated into "..." attributes without quote escaping, allowing attribute breakout and injected event handlers (stored XSS, only mitigated by CSP). Flagged independently by two reviewers. - cron/create_recurring_tickets.php & cron/cleanup_ratelimit.php: a mangled crontab example inside the docblock contained */ which closed the comment early, causing a fatal parse error — both cron jobs never ran. Rewrote the docblocks without a literal */. - update_ticket.php: validate visibility BEFORE the core DB write so an invalid payload can't leave the ticket updated while the request reports failure (which also skipped the audit delta and stats cache invalidation). - watch_ticket.php: GET watcher_count was capped at 6 (count of a LIMIT 6 list); use an unbounded COUNT(*) so it matches the POST path. - notifications.php: "assigned to me" LIKE pattern lacked a trailing delimiter, so user 12 also matched 120/123/etc.; anchor with }. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
137 lines
4.1 KiB
PHP
137 lines
4.1 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* Recurring Tickets Cron Job
|
|
*
|
|
* Run this script via cron to automatically create tickets from recurring schedules.
|
|
* Recommended: run every 5-15 minutes.
|
|
*
|
|
* Example crontab entry (minute 10 of every hour):
|
|
* 10 * * * * /usr/bin/php /path/to/cron/create_recurring_tickets.php >> /var/log/recurring_tickets.log 2>&1
|
|
*/
|
|
|
|
// Change to project root directory
|
|
chdir(dirname(__DIR__));
|
|
|
|
// Include required files
|
|
require_once 'config/config.php';
|
|
require_once 'models/RecurringTicketModel.php';
|
|
require_once 'models/TicketModel.php';
|
|
require_once 'models/AuditLogModel.php';
|
|
|
|
// Log function
|
|
function logMessage($message)
|
|
{
|
|
echo "[" . date('Y-m-d H:i:s') . "] " . $message . "\n";
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// Initialize models
|
|
$recurringModel = new RecurringTicketModel($conn);
|
|
$ticketModel = new TicketModel($conn);
|
|
$auditLog = new AuditLogModel($conn);
|
|
|
|
// Get all due recurring tickets
|
|
$dueTickets = $recurringModel->getDueRecurringTickets();
|
|
logMessage("Found " . count($dueTickets) . " recurring tickets due for creation");
|
|
|
|
$created = 0;
|
|
$errors = 0;
|
|
|
|
foreach ($dueTickets as $recurring) {
|
|
logMessage("Processing recurring ticket ID: " . $recurring['recurring_id']);
|
|
|
|
try {
|
|
// Prepare ticket data
|
|
$ticketData = [
|
|
'title' => processTemplate($recurring['title_template']),
|
|
'description' => processTemplate($recurring['description_template']),
|
|
'category' => $recurring['category'],
|
|
'type' => $recurring['type'],
|
|
'priority' => $recurring['priority'],
|
|
'status' => 'Open'
|
|
];
|
|
|
|
// Create the ticket
|
|
$result = $ticketModel->createTicket($ticketData, $recurring['created_by']);
|
|
|
|
if ($result['success']) {
|
|
$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']);
|
|
}
|
|
|
|
// Log to audit
|
|
$auditLog->log(
|
|
$recurring['created_by'],
|
|
'create',
|
|
'ticket',
|
|
$ticketId,
|
|
['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'));
|
|
$errors++;
|
|
}
|
|
} catch (Exception $e) {
|
|
logMessage("ERROR: Exception processing recurring ticket - " . $e->getMessage());
|
|
$errors++;
|
|
}
|
|
}
|
|
|
|
logMessage("Completed: Created $created tickets, $errors errors");
|
|
|
|
$conn->close();
|
|
} catch (Exception $e) {
|
|
logMessage("FATAL ERROR: " . $e->getMessage());
|
|
exit(1);
|
|
}
|
|
|
|
/**
|
|
* Process template variables
|
|
*/
|
|
function processTemplate($template)
|
|
{
|
|
if (empty($template)) {
|
|
return $template;
|
|
}
|
|
|
|
$replacements = [
|
|
'{{date}}' => date('Y-m-d'),
|
|
'{{time}}' => date('H:i:s'),
|
|
'{{datetime}}' => date('Y-m-d H:i:s'),
|
|
'{{week}}' => date('W'),
|
|
'{{month}}' => date('F'),
|
|
'{{year}}' => date('Y'),
|
|
'{{day_of_week}}' => date('l'),
|
|
'{{day}}' => date('d'),
|
|
];
|
|
|
|
return str_replace(array_keys($replacements), array_values($replacements), $template);
|
|
}
|
|
|
|
logMessage("Cron job finished");
|