|
|
@@ -0,0 +1,230 @@
|
|
|
+<?php
|
|
|
+/**
|
|
|
+ * pdf-files/headlessChrome_pdf.php
|
|
|
+ *
|
|
|
+ * Generic headless Chrome PDF generator.
|
|
|
+ * Renders any registered report type to a native Chrome PDF.
|
|
|
+ *
|
|
|
+ * GET params:
|
|
|
+ * type string Report type key (see $reportTypes below)
|
|
|
+ * rid int Primary record ID
|
|
|
+ * rand string Security token (soil_records.rand / plant_records.rand etc.)
|
|
|
+ * cid int Client ID (optional, passed through to print page)
|
|
|
+ * stid string Soil/crop type (optional, passed through)
|
|
|
+ *
|
|
|
+ * Usage examples:
|
|
|
+ * /pdf-files/headlessChrome_pdf.php?type=soil&rid=123&rand=abc
|
|
|
+ * /pdf-files/headlessChrome_pdf.php?type=soil-combined&rid=123&rand=abc
|
|
|
+ * /pdf-files/headlessChrome_pdf.php?type=plant&rid=456&rand=def&cid=12
|
|
|
+ *
|
|
|
+ * To add a new report type: add an entry to $reportTypes below.
|
|
|
+ */
|
|
|
+
|
|
|
+require_once __DIR__ . '/../config/database.php';
|
|
|
+require_once __DIR__ . '/../lib/auth.php';
|
|
|
+require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
+
|
|
|
+use daandesmedt\PHPHeadlessChrome\HeadlessChrome;
|
|
|
+
|
|
|
+if (session_status() === PHP_SESSION_NONE) {
|
|
|
+ session_start();
|
|
|
+}
|
|
|
+
|
|
|
+requireLogin();
|
|
|
+
|
|
|
+// ── Input ─────────────────────────────────────────────────────────────────────
|
|
|
+$type = trim($_GET['type'] ?? 'soil');
|
|
|
+$recordId = (int) ($_GET['rid'] ?? 0);
|
|
|
+$randId = trim( $_GET['rand'] ?? '');
|
|
|
+$clientId = (int) ($_GET['cid'] ?? 0);
|
|
|
+$stid = trim( $_GET['stid'] ?? '');
|
|
|
+
|
|
|
+// ── Report type registry ──────────────────────────────────────────────────────
|
|
|
+//
|
|
|
+// 'print_page' — path relative to site root; {rid}, {rand}, {cid}, {stid}
|
|
|
+// placeholders are substituted at runtime
|
|
|
+// 'verify_table' — DB table used to verify the rand token before generating
|
|
|
+// 'filename' — prefix for the downloaded PDF filename
|
|
|
+// 'label' — human-readable name (for error messages)
|
|
|
+//
|
|
|
+$reportTypes = [
|
|
|
+ // Individual pages
|
|
|
+ 'soil-analysis' => [
|
|
|
+ 'print_page' => '/dashboard/crop-analysis/soil-test-data/soil-analysis.php?rid={rid}&rand={rand}&cid={cid}&stid={stid}&print=1',
|
|
|
+ 'verify_table' => 'soil_records',
|
|
|
+ 'filename' => 'soil-analysis',
|
|
|
+ 'label' => 'Soil Analysis',
|
|
|
+ ],
|
|
|
+ 'soil-report' => [
|
|
|
+ 'print_page' => '/dashboard/crop-analysis/soil-test-data/soil-report-pdf.php?rid={rid}&rand={rand}',
|
|
|
+ 'verify_table' => 'soil_records',
|
|
|
+ 'filename' => 'soil-report',
|
|
|
+ 'label' => 'Soil Report',
|
|
|
+ ],
|
|
|
+ // Combined: analysis + AI report in one PDF
|
|
|
+ 'soil' => [
|
|
|
+ 'print_page' => '/dashboard/crop-analysis/soil-test-data/soil-print-combined.php?rid={rid}&rand={rand}&cid={cid}&stid={stid}',
|
|
|
+ 'verify_table' => 'soil_records',
|
|
|
+ 'filename' => 'soil-analysis-report',
|
|
|
+ 'label' => 'Soil Analysis & Report',
|
|
|
+ ],
|
|
|
+ // Plant
|
|
|
+ 'plant-analysis' => [
|
|
|
+ 'print_page' => '/dashboard/crop-analysis/plant-test-data/plant-analysis-print.php?rid={rid}&rand={rand}&cid={cid}',
|
|
|
+ 'verify_table' => 'plant_records',
|
|
|
+ 'filename' => 'plant-analysis',
|
|
|
+ 'label' => 'Plant Analysis',
|
|
|
+ ],
|
|
|
+ 'plant-report' => [
|
|
|
+ 'print_page' => '/dashboard/crop-analysis/plant-test-data/plant-report-pdf.php?rid={rid}&rand={rand}',
|
|
|
+ 'verify_table' => 'plant_records',
|
|
|
+ 'filename' => 'plant-report',
|
|
|
+ 'label' => 'Plant Report',
|
|
|
+ ],
|
|
|
+ 'plant' => [
|
|
|
+ 'print_page' => '/dashboard/crop-analysis/plant-test-data/plant-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
|
|
|
+ 'verify_table' => 'plant_records',
|
|
|
+ 'filename' => 'plant-analysis-report',
|
|
|
+ 'label' => 'Plant Analysis & Report',
|
|
|
+ ],
|
|
|
+ // Water
|
|
|
+ 'water' => [
|
|
|
+ 'print_page' => '/dashboard/crop-analysis/water-test-data/water-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
|
|
|
+ 'verify_table' => 'water_records',
|
|
|
+ 'filename' => 'water-analysis-report',
|
|
|
+ 'label' => 'Water Analysis & Report',
|
|
|
+ ],
|
|
|
+ // Animal
|
|
|
+ 'animal' => [
|
|
|
+ 'print_page' => '/dashboard/crop-analysis/animal-dietary-balance/animal-print-combined.php?rid={rid}&rand={rand}&cid={cid}',
|
|
|
+ 'verify_table' => 'animal_records',
|
|
|
+ 'filename' => 'animal-dietary-report',
|
|
|
+ 'label' => 'Animal Dietary Report',
|
|
|
+ ],
|
|
|
+];
|
|
|
+
|
|
|
+if (!isset($reportTypes[$type])) {
|
|
|
+ http_response_code(400);
|
|
|
+ $valid = implode(', ', array_keys($reportTypes));
|
|
|
+ die("Unknown report type \"" . htmlspecialchars($type, ENT_QUOTES, 'UTF-8') . "\". Valid types: $valid");
|
|
|
+}
|
|
|
+
|
|
|
+$config = $reportTypes[$type];
|
|
|
+
|
|
|
+if ($recordId <= 0 || $randId === '') {
|
|
|
+ http_response_code(400);
|
|
|
+ die('Missing required parameters: rid, rand');
|
|
|
+}
|
|
|
+
|
|
|
+// ── Verify record exists via its table ───────────────────────────────────────
|
|
|
+$pdo = getDBConnection();
|
|
|
+$table = $config['verify_table'];
|
|
|
+
|
|
|
+// Allowlist the table name against known tables to prevent SQL injection
|
|
|
+$allowedTables = ['soil_records', 'plant_records', 'water_records', 'animal_records'];
|
|
|
+if (!in_array($table, $allowedTables, true)) {
|
|
|
+ http_response_code(500);
|
|
|
+ die('Invalid verify_table in report type config.');
|
|
|
+}
|
|
|
+
|
|
|
+$stmt = $pdo->prepare("SELECT lab_no FROM `{$table}` WHERE id = ? AND rand = ? LIMIT 1");
|
|
|
+$stmt->execute([$recordId, $randId]);
|
|
|
+$record = $stmt->fetch();
|
|
|
+
|
|
|
+if (!$record) {
|
|
|
+ http_response_code(404);
|
|
|
+ die('Record not found or access denied.');
|
|
|
+}
|
|
|
+
|
|
|
+// ── Write one-time print token ────────────────────────────────────────────────
|
|
|
+$token = bin2hex(random_bytes(16));
|
|
|
+$expires = time() + 120;
|
|
|
+
|
|
|
+$tokenDir = __DIR__ . '/tokens';
|
|
|
+if (!is_dir($tokenDir)) {
|
|
|
+ mkdir($tokenDir, 0750, true);
|
|
|
+}
|
|
|
+
|
|
|
+file_put_contents($tokenDir . '/' . $token . '.tmp', json_encode([
|
|
|
+ 'rid' => $recordId,
|
|
|
+ 'rand' => $randId,
|
|
|
+ 'expires' => $expires,
|
|
|
+ 'type' => $type,
|
|
|
+]));
|
|
|
+
|
|
|
+// ── Build print page URL ──────────────────────────────────────────────────────
|
|
|
+$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
|
|
+$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
|
|
+$baseUrl = $scheme . '://' . $host;
|
|
|
+
|
|
|
+$printPath = str_replace(
|
|
|
+ ['{rid}', '{rand}', '{cid}', '{stid}'],
|
|
|
+ [$recordId, urlencode($randId), $clientId, urlencode($stid)],
|
|
|
+ $config['print_page']
|
|
|
+);
|
|
|
+
|
|
|
+$printUrl = $baseUrl . $printPath . '&ptoken=' . urlencode($token);
|
|
|
+
|
|
|
+// ── Output directory + filename ───────────────────────────────────────────────
|
|
|
+$outputDir = __DIR__;
|
|
|
+$today = date('Y-m-d');
|
|
|
+$labNo = preg_replace('/[^A-Za-z0-9\-_]/', '_', $record['lab_no'] ?? $recordId);
|
|
|
+$filename = $config['filename'] . '-' . $labNo . '-' . $today;
|
|
|
+
|
|
|
+// ── Headless Chrome ───────────────────────────────────────────────────────────
|
|
|
+$chromeBinary = '/usr/bin/google-chrome';
|
|
|
+foreach (['/usr/bin/google-chrome', '/usr/bin/chromium-browser', '/usr/bin/chromium'] as $bin) {
|
|
|
+ if (file_exists($bin)) { $chromeBinary = $bin; break; }
|
|
|
+}
|
|
|
+
|
|
|
+$arguments = [
|
|
|
+ '--headless' => '',
|
|
|
+ '--disable-gpu' => '',
|
|
|
+ '--hide-scrollbars' => '',
|
|
|
+ '--enable-viewport' => '',
|
|
|
+ '--disable-web-security' => '',
|
|
|
+ '--run-all-compositor-stages-before-draw' => '',
|
|
|
+ '--virtual-time-budget' => '60000',
|
|
|
+ '--timeout=' => '8000',
|
|
|
+ '--no-sandbox' => '',
|
|
|
+ '--disable-setuid-sandbox' => '',
|
|
|
+];
|
|
|
+
|
|
|
+try {
|
|
|
+ $chrome = new HeadlessChrome();
|
|
|
+ $chrome->disablePDFHeader();
|
|
|
+ $chrome->setUrl($printUrl);
|
|
|
+ $chrome->setBinaryPath($chromeBinary);
|
|
|
+ $chrome->setOutputDirectory($outputDir);
|
|
|
+ $chrome->setChromeArguments($arguments);
|
|
|
+ $chrome->toPDF($filename . '.pdf');
|
|
|
+
|
|
|
+ $pdfPath = $chrome->getFilePath();
|
|
|
+} catch (Exception $e) {
|
|
|
+ error_log('HeadlessChrome PDF error [' . $type . ']: ' . $e->getMessage());
|
|
|
+ @unlink($tokenDir . '/' . $token . '.tmp');
|
|
|
+ http_response_code(500);
|
|
|
+ die('PDF generation failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'));
|
|
|
+}
|
|
|
+
|
|
|
+@unlink($tokenDir . '/' . $token . '.tmp');
|
|
|
+
|
|
|
+// ── Stream PDF ────────────────────────────────────────────────────────────────
|
|
|
+if (!file_exists($pdfPath)) {
|
|
|
+ http_response_code(500);
|
|
|
+ die('PDF file was not created. Ensure Chrome is installed at: ' . $chromeBinary);
|
|
|
+}
|
|
|
+
|
|
|
+chmod($pdfPath, 0644);
|
|
|
+
|
|
|
+header('Content-Type: application/pdf');
|
|
|
+header('Content-Disposition: attachment; filename="' . $filename . '.pdf"');
|
|
|
+header('Content-Length: ' . filesize($pdfPath));
|
|
|
+header('Expires: 0');
|
|
|
+header('Cache-Control: must-revalidate');
|
|
|
+header('Pragma: public');
|
|
|
+
|
|
|
+readfile($pdfPath);
|
|
|
+
|
|
|
+@unlink($pdfPath);
|
|
|
+exit;
|