diff --git a/app/Config/Routes.php b/app/Config/Routes.php index ba57143..7ff831c 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -180,6 +180,7 @@ $routes->get('administrator/trophy/final', 'View\TrophyController::final' // Certificates $routes->get('administrator/certificates', 'View\CertificateController::index', ['filter' => 'auth:admin']); +$routes->get('administrator/certificates/csrf-token', 'View\CertificateController::csrfToken', ['filter' => 'auth:admin']); $routes->post('administrator/certificates/generate', 'View\CertificateController::generate', ['filter' => 'auth:admin']); $routes->get('administrator/certificates/log', 'View\CertificateController::auditLog', ['filter' => 'auth:admin']); $routes->get('verify/(:segment)', 'View\CertificateController::verify/$1'); diff --git a/app/Controllers/View/CertificateController.php b/app/Controllers/View/CertificateController.php index 8a89ce8..2c56125 100644 --- a/app/Controllers/View/CertificateController.php +++ b/app/Controllers/View/CertificateController.php @@ -77,6 +77,17 @@ class CertificateController extends BaseController ]); } + public function csrfToken() + { + return $this->response + ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') + ->setHeader('Pragma', 'no-cache') + ->setJSON([ + 'csrf_token' => csrf_token(), + 'csrf_hash' => csrf_hash(), + ]); + } + // ─── Public verification page ────────────────────────────────────────────── public function verify(string $certNumber) @@ -101,6 +112,16 @@ class CertificateController extends BaseController $schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear; if (empty($studentIds)) { + if ($this->request->isAJAX()) { + return $this->response + ->setStatusCode(422) + ->setJSON([ + 'ok' => false, + 'error' => 'Please select at least one student.', + 'csrf_token' => csrf_token(), + 'csrf_hash' => csrf_hash(), + ]); + } return redirect()->to('administrator/certificates')->with('error', 'Please select at least one student.'); } @@ -108,6 +129,16 @@ class CertificateController extends BaseController $certDate = preg_replace('/[^0-9\/\-]/', '', $certDate); if (empty($studentIds)) { + if ($this->request->isAJAX()) { + return $this->response + ->setStatusCode(422) + ->setJSON([ + 'ok' => false, + 'error' => 'Invalid student selection.', + 'csrf_token' => csrf_token(), + 'csrf_hash' => csrf_hash(), + ]); + } return redirect()->to('administrator/certificates')->with('error', 'Invalid student selection.'); } @@ -140,6 +171,16 @@ class CertificateController extends BaseController } if (empty($students)) { + if ($this->request->isAJAX()) { + return $this->response + ->setStatusCode(422) + ->setJSON([ + 'ok' => false, + 'error' => 'No valid students found.', + 'csrf_token' => csrf_token(), + 'csrf_hash' => csrf_hash(), + ]); + } return redirect()->to('administrator/certificates')->with('error', 'No valid students found.'); } @@ -170,6 +211,8 @@ class CertificateController extends BaseController return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"') + ->setHeader('X-CSRF-TOKEN-NAME', csrf_token()) + ->setHeader('X-CSRF-TOKEN', csrf_hash()) ->setBody($pdfData); } @@ -204,30 +247,6 @@ class CertificateController extends BaseController return $clean; } - /** - * Generates a QR code PNG for $url and returns its temporary file path. - * Caller must unlink() the file when done. - */ - private function makeQrTempFile(string $url): ?string - { - try { - $result = \Endroid\QrCode\Builder\Builder::create() - ->writer(new \Endroid\QrCode\Writer\PngWriter()) - ->data($url) - ->encoding(new \Endroid\QrCode\Encoding\Encoding('UTF-8')) - ->size(300) - ->margin(2) - ->build(); - - $tmp = tempnam(sys_get_temp_dir(), 'cert_qr_') . '.png'; - $result->saveToFile($tmp); - return $tmp; - } catch (\Throwable $e) { - log_message('error', 'Certificate QR generation failed: ' . $e->getMessage()); - return null; - } - } - // ─── PDF rendering ───────────────────────────────────────────────────────── private function buildPdf(array $students, string $certDate): string @@ -252,8 +271,6 @@ class CertificateController extends BaseController $W = $pdf->getPageWidth(); $H = $pdf->getPageHeight(); - $tmpFiles = []; - foreach ($students as $student) { $pdf->AddPage(); @@ -261,28 +278,18 @@ class CertificateController extends BaseController $grade = $this->formatGrade($student['grade'] ?? ''); $certNumber = $student['cert_number'] ?? ''; - // Build verification URL and generate QR temp file - $verifyUrl = base_url('verify/' . rawurlencode($certNumber)); - $qrFile = $certNumber !== '' ? $this->makeQrTempFile($verifyUrl) : null; - if ($qrFile) { - $tmpFiles[] = $qrFile; - } + // Build verification URL for an inline TCPDF QR code. + $verifyUrl = site_url('verify/' . rawurlencode($certNumber)); $this->drawCertificate( $pdf, $W, $H, $name, $grade, $certDate, $certNumber, - $qrFile, + $verifyUrl, $imgDir, $edwardianFont, $garamondBold, $ebGaramond ); } - $output = $pdf->Output('', 'S'); - - foreach ($tmpFiles as $f) { - @unlink($f); - } - - return $output; + return $pdf->Output('', 'S'); } /** @@ -297,7 +304,7 @@ class CertificateController extends BaseController string $grade, string $certDate, string $certNumber, - ?string $qrFile, + ?string $verifyUrl, string $imgDir, string $edwardianFont, string $garamondBold, @@ -310,10 +317,16 @@ class CertificateController extends BaseController // ── QR code — bottom-right corner: 1.5 cm from bottom, 2.5 cm from right $qrSize = 42; // pt - if ($qrFile !== null && file_exists($qrFile)) { + if (!empty($verifyUrl)) { $qrX = $W - 70.87 - $qrSize; // 2.5 cm from right edge $qrY = $H - 42.52 - $qrSize; // 1.5 cm from bottom edge - $pdf->Image($qrFile, $qrX, $qrY, $qrSize, $qrSize); + $style = [ + 'border' => false, + 'padding' => 0, + 'fgcolor' => [0, 0, 0], + 'bgcolor' => false, + ]; + $pdf->write2DBarcode($verifyUrl, 'QRCODE,L', $qrX, $qrY, $qrSize, $qrSize, $style, 'N'); } // ── "Presented to:" diff --git a/app/Views/admin/certificates/index.php b/app/Views/admin/certificates/index.php index 924aa95..9c975aa 100644 --- a/app/Views/admin/certificates/index.php +++ b/app/Views/admin/certificates/index.php @@ -51,7 +51,7 @@ -
+ @@ -122,6 +122,103 @@