/dev/null 2>&1 // // No shell? Hit it over HTTP with the shared secret from .env: // */5 * * * * curl -fsS "https://fitme3070.pt/cron.php?token=" // // What it does, in order: // 1. Releases rows stuck in 'sending' (a worker died mid-send). // 2. Drains the message queue (email via SMTP, SMS/WhatsApp via Twilio). // 3. Soft-deletes messages sent more than 90 days ago, then hard-deletes // rows that have been soft-deleted for another 90. // // Every run is serialised with a lock file, so a slow SMTP server cannot cause // two runs to drain the same rows. flush_message() claims each row atomically // as well, so an inline send racing the cron is also safe — the lock is about // not piling up runs, not about correctness. // // Deliberately NOT a copy of fotografia's cron: that one also queues session // reminders, emails a download digest and compresses session photos. fitme3070 // has none of those modules, so the steps would be dead code. // ───────────────────────────────────────────────────────────────────────────── $cli = (PHP_SAPI === 'cli'); require __DIR__ . '/config/db.php'; require __DIR__ . '/config/functions.php'; // ─── Access control (HTTP only) ────────────────────────────────────────────── // Over the web the token is the only thing standing between the internet and a // job that sends mail. Missing or wrong token = 404, not 403: an attacker // probing for cron endpoints learns nothing from a 404. if (!$cli) { $expected = (string) env('CRON_TOKEN', ''); $supplied = (string) ($_GET['token'] ?? ''); if ($expected === '' || $expected === 'replace_me' || !hash_equals($expected, $supplied)) { http_response_code(404); exit("Not Found\n"); } header('Content-Type: text/plain; charset=utf-8'); } // ─── Single-run lock ───────────────────────────────────────────────────────── $lockPath = __DIR__ . '/logs/cron.lock'; @mkdir(dirname($lockPath), 0775, true); $lock = @fopen($lockPath, 'c'); if ($lock === false) { echo "cron: não foi possível abrir o ficheiro de lock\n"; exit(1); } if (!flock($lock, LOCK_EX | LOCK_NB)) { echo "cron: já existe uma execução a decorrer — saltada.\n"; exit(0); } $started = microtime(true); $log = []; function cron_log(string $line): void { global $log; $log[] = $line; echo $line . "\n"; } cron_log('── cron ' . date('Y-m-d H:i:s') . ' ──'); // ─── 1. Release rows stuck in 'sending' ────────────────────────────────────── try { $released = requeue_stuck_messages($pdo, 15); if ($released > 0) cron_log("mensagens libertadas de 'sending': {$released}"); } catch (Throwable $e) { cron_log('ERRO ao libertar mensagens presas: ' . $e->getMessage()); } // ─── 2. Drain the queue ────────────────────────────────────────────────────── try { $r = process_queue(50); if ($r['total'] > 0) { cron_log("fila: {$r['total']} processada(s), {$r['sent']} enviada(s), {$r['failed']} falhada(s)"); } } catch (Throwable $e) { cron_log('ERRO ao drenar a fila: ' . $e->getMessage()); } // ─── 3. Retention ──────────────────────────────────────────────────────────── // Once a day is plenty. Gated on the first 5-minute slot of 04:00 — the hour // alone would still fire this twelve times. Bodies are longtext, so this is // what stops the table growing without bound. if ((int) date('G') === 4 && (int) date('i') < 5) { try { $soft = db_soft_delete_sent_older_than($pdo, 90); $hard = db_purge_deleted_messages($pdo, 90); if ($soft || $hard) cron_log("retenção: {$soft} arquivada(s), {$hard} eliminada(s)"); } catch (Throwable $e) { cron_log('ERRO na retenção: ' . $e->getMessage()); } } cron_log(sprintf('── fim (%.2fs) ──', microtime(true) - $started)); flock($lock, LOCK_UN); fclose($lock);